Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c4b6e390a | ||
|
|
d0bd86513e | ||
|
|
b5c803df87 | ||
|
|
983e6f7b1a | ||
|
|
68edcc0bfb | ||
|
|
b705c4e4a0 | ||
|
|
31d582d29e | ||
|
|
ad2fa2c92e | ||
|
|
796d03f8a9 | ||
|
|
be68b706f0 | ||
|
|
23a7c73331 | ||
|
|
82fe8ce3a1 | ||
|
|
00cd1262b4 | ||
|
|
7bc734a9a8 | ||
|
|
a19ccaf035 | ||
|
|
60625103f8 | ||
|
|
aa02f2cacb | ||
|
|
6a1e39efa9 | ||
|
|
7242c3f775 | ||
|
|
a43f6441b4 | ||
|
|
2f5ae373a1 | ||
|
|
b978361180 | ||
|
|
93b8055085 | ||
|
|
70ee338fc3 |
No files matched your search
+10
-1
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(KSwitcher VERSION 0.6.77 LANGUAGES CXX RC)
|
||||
project(KSwitcher VERSION 0.6.83 LANGUAGES CXX RC)
|
||||
if(NOT WIN32)
|
||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||
endif()
|
||||
@@ -117,6 +117,15 @@ 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(KSwitcherFilesActivationFixture tests/files_activation_fixture.cpp)
|
||||
if(NOT MSVC)
|
||||
target_link_options(KSwitcherFilesActivationFixture PRIVATE -municode -static)
|
||||
endif()
|
||||
add_test(NAME files-activation-and-refresh
|
||||
COMMAND "${KSWITCHER_POWERSHELL}" -NoProfile -ExecutionPolicy Bypass
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/tests/files_activation.ps1"
|
||||
-Executable "$<TARGET_FILE:KSwitcherFiles>" -Fixture "$<TARGET_FILE:KSwitcherFilesActivationFixture>")
|
||||
set_tests_properties(files-activation-and-refresh PROPERTIES RUN_SERIAL TRUE TIMEOUT 120)
|
||||
add_executable(KSwitcherLanInputTests tests/lan_input.cpp)
|
||||
target_link_libraries(KSwitcherLanInputTests PRIVATE kscore)
|
||||
if(MSVC)
|
||||
|
||||
+56
-10
@@ -4,14 +4,28 @@ Far Manager є зразком для оформлення файлового м
|
||||
|
||||
## Скорочення
|
||||
|
||||
- У командному рядку Ctrl+V і Shift+Insert вставляють текст із буфера обміну
|
||||
в позицію курсора або замінюють виділення. Якщо фокус на файловій панелі,
|
||||
ці клавіші переводять його до командного рядка й вставляють текст там.
|
||||
Enter запускає команду. Escape очищає поле й залишає фокус у командному рядку.
|
||||
- Enter і подвійний клік відкривають папку в поточній панелі, а локальний
|
||||
документ — у програмі, призначеній для його типу у Windows. EXE запускається
|
||||
окремим процесом. BAT і CMD виконуються в поточній сесії вбудованого
|
||||
термінала, а їхній вивід з’являється під панелями. Щоб відкрити файл із
|
||||
панелі LAN, спочатку скопіюй його до локальної папки.
|
||||
|
||||
- У головному вікні клавіатурний фокус залишається в командному рядку.
|
||||
Tab перемикає активну панель, клік вибирає рядок, а стрілки вгору й вниз,
|
||||
Page Up і Page Down пересувають курсор у панелі навіть під час набору команди.
|
||||
Меню й діалоги тимчасово приймають фокус. Ctrl+V і Shift+Insert вставляють
|
||||
текст із буфера обміну в позицію курсора або замінюють виділення.
|
||||
Enter запускає введену команду або відкриває поточний елемент, якщо рядок
|
||||
порожній. Escape очищає рядок.
|
||||
Промпт і текст команди вирівняні по спільній базовій лінії.
|
||||
|
||||
- Ctrl+E підставляє останню команду та переводить фокус із файлової панелі
|
||||
до командного рядка. Повторне натискання
|
||||
- Ctrl+F вставляє повний шлях елемента під курсором активної панелі в позицію
|
||||
курсора командного рядка або замінює виділення. Подвійні лапки додаються
|
||||
лише до шляхів із пробілами. Для `..` вставляється шлях батьківської папки.
|
||||
- Ctrl+C копіює весь введений у командний рядок текст, навіть коли виділено
|
||||
лише його частину.
|
||||
|
||||
- Ctrl+E підставляє останню команду. Повторне натискання
|
||||
підставляє старішу, доки не досягне початку історії. Alt+F8 відкриває історію
|
||||
з групуванням за датами. Enter або подвійний клік у списку підставляє вибрану
|
||||
команду в поле. Для запуску натисни Enter у самому полі.
|
||||
@@ -41,9 +55,15 @@ Far Manager є зразком для оформлення файлового м
|
||||
Висота списку залежить від кількості результатів. Коли місця бракує,
|
||||
прокручується список.
|
||||
|
||||
- Короткий Alt перемикає головне меню Files при відпусканні. F9 також перемикає меню. Після закриття фокус повертається до активної панелі. Alt з літерою верхнього рівня спрацьовує одразу при натисканні літери. Alt+O відкриває Options, а відпускання Alt після комбінації не перемикає меню.
|
||||
- Alt із символом шукає початок назви файлу чи папки. Після першого збігу
|
||||
внизу активної панелі відкривається компактне поле Search у подвійній рамці.
|
||||
Наступні символи можна набирати без Alt. Пошук враховує поточну розкладку
|
||||
та ігнорує регістр. Backspace прибирає останній символ, Escape закриває пошук,
|
||||
а Enter відкриває поточний елемент.
|
||||
- F9 перемикає головне меню Files. Після закриття фокус повертається в командний рядок. Коли меню відкрите, Alt з літерою верхнього рівня відкриває відповідний пункт.
|
||||
- Коли головне меню відкрите, L, F, C, O та R відкривають Left, Files, Commands, Options та Right. У підменю підсвічена літера викликає відповідну команду. Літери унікальні в межах поточного рівня. Усталені мнемоніки мають пріоритет, зокрема X для Exit.
|
||||
- Глобальне скорочення **Open KSwitcher Files** можна змінити або очистити в налаштуваннях. Для профілю без цього поля пропонується Ctrl+Alt+F, якщо Windows реєструє комбінацію і вона вільна серед прив’язок KSwitcher.
|
||||
- Глобальне скорочення повертає до відкритого вікна Files у поточній сесії Windows або запускає Files, якщо такого вікна немає. Пункт **Files** у треї відкриває нове вікно.
|
||||
|
||||
- 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.
|
||||
@@ -53,7 +73,7 @@ Far Manager є зразком для оформлення файлового м
|
||||
- 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 та повертають фокус до панелі.
|
||||
- Apply доступна, коли чернетка відрізняється від застосованих налаштувань. Після Apply або повернення всіх значень до застосованих вона вимикається. Apply одразу зберігає зміни й перемальовує панелі за відкритим діалогом. OK робить те саме й закриває діалог. Cancel і Escape відкидають зміни після останнього Apply та повертають фокус до командного рядка.
|
||||
|
||||
- Colors → File type styles налаштовує текст, фон, підкреслення й закреслення для правил за розширенням і групи System and hidden. Розширення можна вибрати зі списку Windows або додати вручну з крапкою чи без неї. Вони зберігаються в KSwitcher, незалежно від асоціацій Windows. Курсор, позначки й результат передачі зберігають свої сигнальні кольори. Кольори правила можна перевизначити окремо для звичайного рядка, позначки Insert, курсора у фокусі та курсора без фокуса. Позначка має пріоритет над курсором, а підсвічування передачі має пріоритет над кольорами курсора. Без перевизначення стан зберігає звичайний вигляд панелі. Мініпанель унизу редактора одразу показує всі чотири стани на синтетичних рядках до Apply.
|
||||
|
||||
@@ -65,6 +85,32 @@ Far Manager є зразком для оформлення файлового м
|
||||
|
||||
## Панелі
|
||||
|
||||
- Обидві панелі автоматично підхоплюють створення, зміну й видалення файлів
|
||||
у відкритих локальних папках. Фокус залишається там, де був, а курсор
|
||||
лишається на поточному елементі, якщо той ще існує.
|
||||
|
||||
- Ctrl+8 відкриває список усіх ярликів папок. Кожен рядок показує групу
|
||||
й назву або шлях. Enter відкриває папку в активній панелі, F4 редагує
|
||||
ярлик, а Delete прибирає його зі списку.
|
||||
|
||||
- У Change drive повідомлення No paired computers з’являється після
|
||||
успішної відповіді з порожнім списком. Збої локального запиту до
|
||||
KSwitcher відображаються окремо.
|
||||
|
||||
- Якщо команда у вбудованому PowerShell змінює поточну папку,
|
||||
панель, з якої надіслано команду, переходить до цієї папки.
|
||||
|
||||
- Коли список LAN-дисків змінює розмір меню Change drive, рамка й список
|
||||
змінюють позицію разом. Рамка повністю перемальовується після зміни
|
||||
розміру, а список має однаковий внутрішній відступ зліва і справа.
|
||||
- Тайм-аут запиту дисків відображається як Connection timed out. Якщо
|
||||
ввімкнено Write log, Files записує помилки запитів пар і дисків в окремий
|
||||
журнал `data/kswitcher-files.log`. Системні помилки містять код Windows.
|
||||
|
||||
- Застосовані налаштування Files одразу підхоплюють інші відкриті вікна.
|
||||
Apply зберігає поля, змінені в поточній чернетці. Якщо два вікна одночасно
|
||||
редагували набір стилів файлів, відкрий Options заново, щоб побачити свіжі правила.
|
||||
|
||||
- Типовий колір тексту для ZIP-архівів — світлий коралово-рожевий.
|
||||
|
||||
- Меню Change drive показує літеру, тип, загальний обсяг і вільне місце. Відкриття меню запускає нове LAN-виявлення. Секція KSwitcher LAN показує збережені пари, зокрема офлайн. Після кожного відкриття меню асинхронно запитує диски й показує їх під назвою відповідного ПК. Поки триває запит, видно стан завантаження. Якщо запит не вдався, видно помилку. Повторне відкриття запускає новий запит. Кожна панель має власний диск і шлях, а в заголовку віддаленої панелі перед шляхом показано назву ПК.
|
||||
@@ -77,7 +123,7 @@ Far Manager є зразком для оформлення файлового м
|
||||
|
||||
- У широкому режимі ширина під вирівняні розширення залежить від найдовшого розширення в поточній панелі. За вузької панелі залишай місце для імені та колонки Size.
|
||||
|
||||
- Бірюзовий курсор поточного рядка видно лише в активній панелі, зокрема коли клавіатурний фокус переходить у командний рядок. Кожна панель зберігає свою позицію курсора. Коли панелі видимі, F5 і F6 з командного рядка діють на активну панель.
|
||||
- Бірюзовий курсор поточного рядка видно в активній панелі. Кожна панель зберігає свою позицію курсора. Коли панелі видимі, F5 і F6 з командного рядка діють на активну панель.
|
||||
- Клік у панелі нижче списку ставить курсор на останній рядок, а клік вище списку — на перший.
|
||||
|
||||
- Поточний шлях відображається по центру верхньої подвійної лінії панелі. Лінію видно з обох боків напису. Бірюзове тло активної панелі охоплює лише напис шляху з невеликими відступами. Довгий шлях скорочується в межах панелі.
|
||||
@@ -129,7 +175,7 @@ Far Manager є зразком для оформлення файлового м
|
||||
## Видалення
|
||||
|
||||
- F8 показує сіре підтвердження з дією Move. Shift+Delete і підтвердження непорожньої папки мають темно-червоне тло. Вікно прогресу видалення має сіре тло, як у Copy.
|
||||
- Escape у початковому підтвердженні повертає фокус до панелі, з якої викликано видалення. Поточний рядок зберігається, а стрілки одразу працюють.
|
||||
- Escape у початковому підтвердженні повертає фокус до командного рядка. Поточний рядок активної панелі зберігається, а стрілки одразу працюють.
|
||||
- F8 переміщує підтверджені елементи в кошик Windows. Shift+Delete видаляє підтверджене дерево від вкладених елементів до батьківської папки. Прогрес і підсумок окремо показують успішні, пропущені й невдалі дії.
|
||||
- Cancel і Esc у вікні прогресу відкривають спільне червоне підтвердження скасування. Yes зупиняє операцію, а No або Esc у підтвердженні повертає до прогресу. Початковий фокус стоїть на No.
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,5 +1,53 @@
|
||||
# Latest Windows release
|
||||
|
||||
## v0.6.83 — 2026-11-15
|
||||
|
||||
In KSwitcher Files, the command line keeps keyboard focus while Tab switches panes and Up, Down, Page Up, and Page Down move the active pane's cursor. Ctrl+F inserts the full path under the cursor, using double quotes only for paths with spaces. Ctrl+C copies the entire command line. Alt plus a character starts a filename or folder search. After the first match, type without Alt in the Search popup at the bottom of the active pane. F9 opens the menu.
|
||||
|
||||
**Українською.** У KSwitcher Files клавіатурний фокус залишається в командному рядку. Tab перемикає панелі, а стрілки вгору й вниз, Page Up і Page Down пересувають курсор активної панелі. Ctrl+F вставляє повний шлях під курсором і додає подвійні лапки лише до шляхів із пробілами. Ctrl+C копіює весь командний рядок. Alt із символом починає пошук файлу чи папки за назвою. Після першого збігу уточнюйте запит без Alt у полі Search внизу активної панелі. F9 відкриває меню.
|
||||
|
||||
## v0.6.82 — 2026-11-14
|
||||
|
||||
Enter and double-click open folders in the current pane, open local documents with their Windows default apps, and launch EXE files in separate processes. BAT and CMD files run in the existing embedded PowerShell session, and their output appears below the panes. Both local panes refresh when files are created, modified, or deleted externally and preserve focus.
|
||||
|
||||
**Українською.** Enter і подвійний клік відкривають папки в поточній панелі та локальні документи у стандартних програмах Windows. EXE запускаються окремими процесами, а BAT і CMD виконуються в поточній сесії вбудованого PowerShell із виводом під панелями. Обидві локальні панелі підхоплюють створення, зміну й видалення файлів ззовні та зберігають фокус.
|
||||
|
||||
## v0.6.81 — 2026-11-14
|
||||
|
||||
Ctrl+8 lists all folder shortcuts. Press Enter to open a folder, F4 to edit the shortcut, or Delete to remove it. Ctrl+Alt+F brings an open Files window forward.
|
||||
|
||||
**Українською.** Ctrl+8 показує всі ярлики папок. Enter відкриває папку, F4 редагує ярлик, Delete видаляє його. Ctrl+Alt+F повертає до відкритого вікна Files.
|
||||
|
||||
## v0.6.80 — 2026-11-14
|
||||
|
||||
Includes the Files updates from v0.6.79. Change drive now explains why the local peer lookup failed and shows No paired computers only when the response is empty.
|
||||
|
||||
**Українською.** Випуск включає зміни Files із v0.6.79. Change drive показує конкретну причину збою запиту пар. No paired computers з’являється лише після успішної порожньої відповіді.
|
||||
|
||||
## v0.6.79 — 2026-11-14
|
||||
|
||||
In KSwitcher Files, changing directory in PowerShell updates the panel
|
||||
that sent the command. The global Files shortcut brings an open window to
|
||||
the foreground. Change drive redraws its frame when LAN drives arrive and
|
||||
shows a short timeout message. Files writes LAN lookup errors to its own log.
|
||||
|
||||
**Українською.** Зміна каталогу в PowerShell оновлює панель, з якої
|
||||
надіслано команду. Глобальна клавіша Files повертає до відкритого вікна.
|
||||
Change drive перемальовує рамку після появи LAN-дисків і показує коротке
|
||||
повідомлення про тайм-аут. Files записує помилки LAN у власний журнал.
|
||||
|
||||
## v0.6.78 — 2026-11-14
|
||||
|
||||
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.
|
||||
|
||||
**Українською.** До пакета для Windows входить KSwitcher Files, двопанельний
|
||||
файловий менеджер із локальними дисками й дисками спарених ПК, ярликами папок,
|
||||
вбудованим PowerShell та пошуком в історії команд. Застосовані налаштування
|
||||
Files одразу підхоплюють інші відкриті вікна.
|
||||
|
||||
## v0.6.77 — 2026-11-13
|
||||
|
||||
The screenshot editor remembers the local pencil smoothing default across
|
||||
|
||||
@@ -1 +1 @@
|
||||
51d95639527590b36453a5ef485532a7d2135950222fa197eb431a5309f1a45c KSwitcher-win-x64.zip
|
||||
6f5669ed42ffc70ecdf5e3efe6a1ec549abfca07d3d1b1d9c384f2d3dc99d0c0 KSwitcher-win-x64.zip
|
||||
+59
-14
@@ -1,20 +1,65 @@
|
||||
{
|
||||
"version": "0.6.77",
|
||||
"schema": 1,
|
||||
"files": [
|
||||
{
|
||||
"path": "KSwitcher.exe",
|
||||
"size": 2249216,
|
||||
"sha256": "f1c79d337d746efcda03946cc833b7b5588ef4e739f1dff7d3afa15d0465469f"
|
||||
"size": 2341376,
|
||||
"sha256": "b6ed0279b8e92427f6e8414e5dc7da4e2a77635106e71573cc55b299820c8ba7"
|
||||
},
|
||||
{
|
||||
"path": "KSwitcher.Config.exe",
|
||||
"size": 2251776,
|
||||
"sha256": "016edc389007e0d15ee365fff7115fc91a820b46080cb39a3fb362f3b463bae7"
|
||||
"size": 2341888,
|
||||
"sha256": "6f22e2df935cea2ce5400f22d69c42008513946d926b0ffff116de5dac705210"
|
||||
},
|
||||
{
|
||||
"path": "KSwitcher.Screenshot.exe",
|
||||
"size": 3247616,
|
||||
"sha256": "c8ef070ad01acc410802c62e0df6a96cb299fe036fe8e99f0cf6a398b7324ec6"
|
||||
"size": 3319808,
|
||||
"sha256": "af443bcf13f913aed6cbff59e9a34ce701552d4e4ce065b7b24700e3ed1347d4"
|
||||
},
|
||||
{
|
||||
"path": "KSwitcher.Files.exe",
|
||||
"size": 1833472,
|
||||
"sha256": "c6bf0a744c46cbfd1cbfb52bd4b70503ef0a799f378bad97cdd01ddf3b21737f"
|
||||
},
|
||||
{
|
||||
"path": "WebView2Loader.dll",
|
||||
"size": 194912,
|
||||
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6"
|
||||
},
|
||||
{
|
||||
"path": "terminal/addon-fit-LICENSE.txt",
|
||||
"size": 1103,
|
||||
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f"
|
||||
},
|
||||
{
|
||||
"path": "terminal/addon-fit.js",
|
||||
"size": 1521,
|
||||
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527"
|
||||
},
|
||||
{
|
||||
"path": "terminal/index.html",
|
||||
"size": 527,
|
||||
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef"
|
||||
},
|
||||
{
|
||||
"path": "terminal/terminal.js",
|
||||
"size": 4167,
|
||||
"sha256": "d60e6ff8948b6616695297695b05ef6e74b497a8e32e6031fc2e4fe2cca39ca7"
|
||||
},
|
||||
{
|
||||
"path": "terminal/xterm-LICENSE.txt",
|
||||
"size": 1261,
|
||||
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd"
|
||||
},
|
||||
{
|
||||
"path": "terminal/xterm.css",
|
||||
"size": 7112,
|
||||
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6"
|
||||
},
|
||||
{
|
||||
"path": "terminal/xterm.js",
|
||||
"size": 488663,
|
||||
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972"
|
||||
},
|
||||
{
|
||||
"path": "LICENSE",
|
||||
@@ -48,8 +93,8 @@
|
||||
},
|
||||
{
|
||||
"path": "dictionaries/README.md",
|
||||
"size": 2230,
|
||||
"sha256": "2df17fe4e17d527c67ab57269b07dce59a49c1fb128c429a1408decbb47109ec"
|
||||
"size": 2265,
|
||||
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311"
|
||||
},
|
||||
{
|
||||
"path": "dictionaries/ru.txt",
|
||||
@@ -58,13 +103,13 @@
|
||||
},
|
||||
{
|
||||
"path": "dictionaries/sources.json",
|
||||
"size": 1279,
|
||||
"sha256": "d6e11a3aaba22a8a7eaba19f778d8a5bd66653ff306810e85a1b0e7fff3a6b86"
|
||||
"size": 1280,
|
||||
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408"
|
||||
},
|
||||
{
|
||||
"path": "dictionaries/uk.txt",
|
||||
"size": 898248,
|
||||
"sha256": "caeb097749d4afc84b68ad96b3d7dfb6a6569561cd32ccce0bee94a13b43ba86"
|
||||
"size": 940799,
|
||||
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf"
|
||||
},
|
||||
{
|
||||
"path": "tools/README.txt",
|
||||
@@ -82,5 +127,5 @@
|
||||
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413"
|
||||
}
|
||||
],
|
||||
"schema": 1
|
||||
"version": "0.6.83"
|
||||
}
|
||||
+3
-3
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
|
||||
1 RT_MANIFEST "app.manifest"
|
||||
201 RCDATA "update.ps1"
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,6,77,0
|
||||
PRODUCTVERSION 0,6,77,0
|
||||
FILEVERSION 0,6,83,0
|
||||
PRODUCTVERSION 0,6,83,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.77"
|
||||
VALUE "FileVersion", "0.6.83"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -7,6 +7,16 @@ const terminal = new Terminal({
|
||||
theme: { background: '#181818', foreground: '#e8e8e8' },
|
||||
});
|
||||
const fit = new FitAddon.FitAddon();
|
||||
terminal.parser.registerOscHandler(633, data => {
|
||||
const prefix = 'ks-directory:';
|
||||
if (!data.startsWith(prefix)) return false;
|
||||
try {
|
||||
bridge.postMessage('directory:' + decodeURIComponent(data.slice(prefix.length)));
|
||||
} catch {
|
||||
bridge.postMessage('directory:');
|
||||
}
|
||||
return true;
|
||||
});
|
||||
terminal.loadAddon(fit);
|
||||
const host = document.getElementById('terminal');
|
||||
terminal.open(host);
|
||||
@@ -14,6 +24,8 @@ host.addEventListener('pointerup', () => bridge.postMessage('focus-input'), true
|
||||
const screen = host.querySelector('.xterm-screen');
|
||||
let traceEnabled = false;
|
||||
let traceBudget = 4096;
|
||||
let outputTraceBudget = 256;
|
||||
let traceOutput = false;
|
||||
let currentOffset = 0;
|
||||
function trace(phase) {
|
||||
if (!traceEnabled || traceBudget <= 0) return;
|
||||
@@ -39,10 +51,13 @@ function anchorOutput() {
|
||||
trace(2);
|
||||
}
|
||||
terminal.attachCustomKeyEventHandler(event => {
|
||||
if (event.type !== 'keydown' || !event.ctrlKey || event.altKey || event.shiftKey) return true;
|
||||
if (event.type !== 'keydown' || !event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) 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 if (event.code === 'KeyF' || event.code === 'KeyC') {
|
||||
if (!event.repeat) bridge.postMessage(event.code === 'KeyF' ? 'insert-path' : 'copy-command-line');
|
||||
}
|
||||
else return true;
|
||||
event.preventDefault();
|
||||
return false;
|
||||
@@ -54,8 +69,9 @@ terminal.onScroll(() => {
|
||||
anchorOutput();
|
||||
});
|
||||
bridge.addEventListener('message', ({ data }) => {
|
||||
if (data === 'trace:on') {
|
||||
if (data === 'trace:on' || data === 'trace:output') {
|
||||
traceEnabled = true;
|
||||
traceOutput = data === 'trace:output';
|
||||
trace(2);
|
||||
return;
|
||||
}
|
||||
@@ -73,7 +89,16 @@ bridge.addEventListener('message', ({ data }) => {
|
||||
}
|
||||
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);
|
||||
terminal.write(bytes, () => {
|
||||
anchorOutput();
|
||||
if (traceOutput && outputTraceBudget > 0) {
|
||||
outputTraceBudget--;
|
||||
const buffer = terminal.buffer.active;
|
||||
const lines = Array.from({ length: terminal.rows }, (_, row) =>
|
||||
buffer.getLine(buffer.viewportY + row)?.translateToString(true) ?? '');
|
||||
bridge.postMessage('output:' + lines.join('\n').trimEnd());
|
||||
}
|
||||
});
|
||||
});
|
||||
new ResizeObserver(() => {
|
||||
const buffer = terminal.buffer.active;
|
||||
|
||||
+23
-1
@@ -143,6 +143,28 @@ ULONG_PTR snippetReplayMarker()
|
||||
}
|
||||
constexpr int OpenFiles = 25, FilesHotkeyFirst = 0x4B10, FilesHotkeySecond = 0x4B11;
|
||||
|
||||
bool activateFilesWindow()
|
||||
{
|
||||
HWND files{};
|
||||
EnumWindows([](HWND candidate, LPARAM context) -> BOOL
|
||||
{
|
||||
wchar_t name[32]{};
|
||||
if(!IsWindowVisible(candidate) || !GetClassNameW(candidate,name,std::size(name)) ||
|
||||
wcscmp(name,L"KSwitcher.Files"))
|
||||
return TRUE;
|
||||
*reinterpret_cast<HWND *>(context)=candidate;
|
||||
return FALSE;
|
||||
},reinterpret_cast<LPARAM>(&files));
|
||||
if(!files) return false;
|
||||
if(IsIconic(files)) ShowWindowAsync(files,SW_RESTORE);
|
||||
if(!SetForegroundWindow(files))
|
||||
{
|
||||
FLASHWINFO flash{sizeof(flash),files,FLASHW_TRAY,3,0};
|
||||
FlashWindowEx(&flash);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::pair<int, std::wstring>> shortcuts(const Settings &s)
|
||||
{
|
||||
std::vector<std::pair<int, std::wstring>> out{{Toggle, s.toggle},
|
||||
@@ -3037,7 +3059,7 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
case WM_HOTKEY:
|
||||
if (app->filesHotkeyId && wp == static_cast<WPARAM>(app->filesHotkeyId))
|
||||
{
|
||||
app->dispatch(OpenFiles);
|
||||
if(!activateFilesWindow()) app->dispatch(OpenFiles);
|
||||
}
|
||||
else if (wp != FilesHotkeyFirst && wp != FilesHotkeySecond) app->dispatch((int)wp);
|
||||
return 0;
|
||||
|
||||
@@ -11,7 +11,8 @@ bool validHistoryKeys(const std::array<WORD,2> &keys)
|
||||
const bool letter=key>='A' && key<='Z';
|
||||
const bool function=key>=VK_F1 && key<=VK_F12;
|
||||
if(!letter && !function) return false;
|
||||
if(modifiers==HOTKEYF_CONTROL) return (letter && key!='C' && key!='V' && key!='X' && key!='A' && key!='Z' && key!='Y') || function;
|
||||
if(modifiers==HOTKEYF_CONTROL)
|
||||
return (letter && key!='C' && key!='F' && key!='V' && key!='X' && key!='A' && key!='Z' && key!='Y') || function;
|
||||
if(modifiers==HOTKEYF_ALT) return function && key!=VK_F1 && key!=VK_F2 && key!=VK_F4 && key!=VK_F10;
|
||||
return false;
|
||||
});
|
||||
@@ -29,11 +30,15 @@ void setOptionHistoryKeys(HWND dialog,const std::array<WORD,2> &keys)
|
||||
SendDlgItemMessageW(dialog,323,HKM_SETHOTKEY,keys[1],0);
|
||||
}
|
||||
|
||||
void loadCommandHistory()
|
||||
void loadCommandHistoryKeys()
|
||||
{
|
||||
const std::array<WORD,2> keys{static_cast<WORD>(preference(L"PreviousCommandKey",defaultHistoryKeys[0])),
|
||||
static_cast<WORD>(preference(L"CommandHistoryKey",defaultHistoryKeys[1]))};
|
||||
if(validHistoryKeys(keys)) commandHistoryKeys=keys;
|
||||
commandHistoryKeys=validHistoryKeys(keys)?keys:defaultHistoryKeys;
|
||||
}
|
||||
|
||||
void loadCommandHistory()
|
||||
{
|
||||
|
||||
DWORD bytes{};
|
||||
if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,L"CommandHistory",RRF_RT_REG_BINARY,nullptr,nullptr,&bytes)
|
||||
|
||||
@@ -139,6 +139,7 @@ INT_PTR CALLBACK folderShortcutEditDialog(HWND dialog,UINT message,WPARAM wp,LPA
|
||||
return TRUE;
|
||||
}
|
||||
folderShortcuts=std::move(updated);
|
||||
notifyFilesPreferences(GetAncestor(dialog,GA_ROOTOWNER));
|
||||
finishFilesDialog(dialog,IDOK);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -186,13 +187,21 @@ struct FolderShortcutView
|
||||
std::vector<int> digits;
|
||||
int rowHeight{};
|
||||
Panel *panel{};
|
||||
bool all{};
|
||||
std::vector<std::pair<int,size_t>> entries;
|
||||
};
|
||||
|
||||
std::wstring shortcutLabel(const FolderShortcut &entry)
|
||||
{
|
||||
return entry.title.empty()?(entry.peerId.empty()?L"":entry.peerName+L" · ")+entry.path.native():entry.title;
|
||||
}
|
||||
|
||||
void layoutQuickFolders(HWND dialog,FolderShortcutView &view,int selection)
|
||||
{
|
||||
const auto list=GetDlgItem(dialog,shortcutList);
|
||||
SendMessageW(list,LB_RESETCONTENT,0,0);
|
||||
view.digits.clear();
|
||||
view.entries.clear();
|
||||
const auto owner=GetWindow(dialog,GW_OWNER);
|
||||
const auto dc=GetDC(dialog);
|
||||
const auto font=SelectObject(dc,mono);
|
||||
@@ -210,14 +219,23 @@ void layoutQuickFolders(HWND dialog,FolderShortcutView &view,int selection)
|
||||
GetTextExtentPoint32W(dc,label.data(),static_cast<int>(label.size()),&size);
|
||||
return static_cast<int>(size.cx);
|
||||
};
|
||||
int width=std::max(measure(L"Folder shortcuts"),measure(view.digit<0?L"Enter Esc":quickFolderHints))+3*cellWidth;
|
||||
int width=std::max(measure(L"Folder shortcuts"),measure(view.all?L"Del F4":view.digit<0?L"Enter Esc":quickFolderHints))+3*cellWidth;
|
||||
const int textInset=dialogRowTextInset(cellWidth);
|
||||
const auto add=[&](const std::wstring &label)
|
||||
{
|
||||
width=std::max(width,measure(label)+2*textInset);
|
||||
SendMessageW(list,LB_ADDSTRING,0,reinterpret_cast<LPARAM>(label.c_str()));
|
||||
};
|
||||
if(view.digit<0)
|
||||
if(view.all)
|
||||
{
|
||||
for(int digit=0;digit<static_cast<int>(folderShortcuts.size());++digit)
|
||||
for(size_t index=0;index<folderShortcuts[digit].size();++index)
|
||||
{
|
||||
view.entries.emplace_back(digit,index);
|
||||
add(std::to_wstring(digit)+L" "+shortcutLabel(folderShortcuts[digit][index]));
|
||||
}
|
||||
}
|
||||
else if(view.digit<0)
|
||||
{
|
||||
for(int digit=0;digit<10;++digit)
|
||||
if(!folderShortcuts[digit].empty())
|
||||
@@ -227,7 +245,7 @@ void layoutQuickFolders(HWND dialog,FolderShortcutView &view,int selection)
|
||||
}
|
||||
}
|
||||
else for(const auto &entry:folderShortcuts[view.digit])
|
||||
add(entry.title.empty()?(entry.peerId.empty()?L"":entry.peerName+L" · ")+entry.path.native():entry.title);
|
||||
add(shortcutLabel(entry));
|
||||
SelectObject(dc,font);
|
||||
ReleaseDC(dialog,dc);
|
||||
MONITORINFO monitor{sizeof(monitor)};
|
||||
@@ -303,7 +321,7 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
const auto bounds=paintDialogFrame(dc,client,dialogFrameMetrics(GetDpiForWindow(dialog)),teal,frame);
|
||||
DeleteObject(frame);
|
||||
paintDialogFrameCaptions(dc,client,bounds,L"Folder shortcuts",teal,colors[Text],mono,
|
||||
view->digit<0?L"Enter Esc":quickFolderHints);
|
||||
view->all?L"Del F4":view->digit<0?L"Enter Esc":quickFolderHints);
|
||||
EndPaint(dialog,&ps);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -330,7 +348,7 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
if(action!=quickRemove && action!=quickEdit && action!=quickInsert && action!=quickMoveUp &&
|
||||
action!=quickMoveDown && action!=IDOK && !(action==shortcutList && HIWORD(wp)==LBN_DBLCLK)) return FALSE;
|
||||
const auto row=SendDlgItemMessageW(dialog,shortcutList,LB_GETCURSEL,0,0);
|
||||
if(view->digit<0)
|
||||
if(!view->all && view->digit<0)
|
||||
{
|
||||
if((action==IDOK || action==shortcutList) && row>=0 && static_cast<size_t>(row)<view->digits.size())
|
||||
{
|
||||
@@ -339,7 +357,17 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
const auto &group=folderShortcuts[view->digit];
|
||||
int digit=view->digit;
|
||||
size_t index=static_cast<size_t>(row);
|
||||
if(view->all)
|
||||
{
|
||||
if(index>=view->entries.size() || action==quickInsert ||
|
||||
action==quickMoveUp || action==quickMoveDown) return TRUE;
|
||||
const auto &entry=view->entries[index];
|
||||
digit=entry.first;
|
||||
index=entry.second;
|
||||
}
|
||||
const auto &group=folderShortcuts[digit];
|
||||
if(action==quickInsert)
|
||||
{
|
||||
const auto &panel=*view->panel;
|
||||
@@ -350,7 +378,7 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
SetFocus(GetDlgItem(dialog,shortcutList));
|
||||
return TRUE;
|
||||
}
|
||||
if(row<0 || static_cast<size_t>(row)>=group.size()) return TRUE;
|
||||
if(index>=group.size()) return TRUE;
|
||||
if(action==quickMoveUp || action==quickMoveDown)
|
||||
{
|
||||
const auto target=row+(action==quickMoveUp?-1:1);
|
||||
@@ -362,13 +390,14 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
else
|
||||
{
|
||||
folderShortcuts=std::move(updated);
|
||||
notifyFilesPreferences(GetAncestor(dialog,GA_ROOTOWNER));
|
||||
layoutQuickFolders(dialog,*view,static_cast<int>(target));
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
if(action==quickEdit)
|
||||
{
|
||||
FolderShortcutEdit edit{group[row],view->digit,static_cast<size_t>(row)};
|
||||
FolderShortcutEdit edit{group[index],digit,index};
|
||||
if(editFolderShortcut(dialog,edit)) layoutQuickFolders(dialog,*view,static_cast<int>(row));
|
||||
SetFocus(GetDlgItem(dialog,shortcutList));
|
||||
return TRUE;
|
||||
@@ -376,24 +405,25 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
if(action==quickRemove)
|
||||
{
|
||||
auto updated=folderShortcuts;
|
||||
updated[view->digit].erase(updated[view->digit].begin()+row);
|
||||
updated[digit].erase(updated[digit].begin()+index);
|
||||
if(!saveFolderShortcuts(updated))
|
||||
{
|
||||
MessageBoxW(dialog,L"Could not save folder shortcuts.",L"Folder shortcuts",MB_OK|MB_ICONWARNING);
|
||||
return TRUE;
|
||||
}
|
||||
folderShortcuts=std::move(updated);
|
||||
if(folderShortcuts[view->digit].empty()) finishFilesDialog(dialog,IDCANCEL);
|
||||
notifyFilesPreferences(GetAncestor(dialog,GA_ROOTOWNER));
|
||||
if(view->all?view->entries.size()==1:folderShortcuts[digit].empty()) finishFilesDialog(dialog,IDCANCEL);
|
||||
else layoutQuickFolders(dialog,*view,static_cast<int>(row));
|
||||
return TRUE;
|
||||
}
|
||||
view->chosen=group[row];
|
||||
view->chosen=group[index];
|
||||
finishFilesDialog(dialog,IDOK);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
void showFolderShortcuts(Panel &panel,int digit)
|
||||
void showFolderShortcuts(Panel &panel,int digit,bool all)
|
||||
{
|
||||
if(digit>=0)
|
||||
{
|
||||
@@ -416,6 +446,7 @@ void showFolderShortcuts(Panel &panel,int digit)
|
||||
}
|
||||
FolderShortcutView view{digit};
|
||||
view.panel=&panel;
|
||||
view.all=all;
|
||||
const auto result=runFilesDialog(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(109),GetParent(panel.list),
|
||||
quickFolderDialog,reinterpret_cast<LPARAM>(&view));
|
||||
SetFocus(panel.list);
|
||||
|
||||
+535
-117
File diff suppressed because it is too large.
Load diff
+100
-45
@@ -52,8 +52,22 @@ void resetPanelProperty(HWND dialog,const PanelProperty &property)
|
||||
else CheckDlgButton(dialog,property.control,property.defaultValue);
|
||||
}
|
||||
|
||||
struct OptionsBaseline
|
||||
{
|
||||
LOGFONTW font=displayFont;
|
||||
int points=fontPoints;
|
||||
DialogFramePadding padding=dialogFramePadding;
|
||||
std::array<COLORREF,ColorCount> palette=colors;
|
||||
std::vector<FileStyle> styles=fileStyles;
|
||||
std::array<WORD,2> keys=commandHistoryKeys;
|
||||
DWORD width=static_cast<DWORD>(sizeWidth), fields=statusFields;
|
||||
bool extensions=alignExtensions, folders=alignFolders, details=showDetails;
|
||||
std::array<bool,2> wide{panels[0].wide,panels[1].wide};
|
||||
};
|
||||
|
||||
struct OptionsDraft
|
||||
{
|
||||
OptionsBaseline baseline;
|
||||
LOGFONTW font=displayFont;
|
||||
int points=fontPoints;
|
||||
DialogFramePadding framePadding=dialogFramePadding;
|
||||
@@ -81,15 +95,28 @@ struct OptionsDraft
|
||||
|
||||
bool optionsFontChanged(const OptionsDraft &draft)
|
||||
{
|
||||
auto pending=draft.font, applied=displayFont;
|
||||
auto pending=draft.font, applied=draft.baseline.font;
|
||||
// Height follows the selected point size and DPI. applyFont always uses natural width.
|
||||
pending.lfHeight=applied.lfHeight=0;
|
||||
pending.lfWidth=applied.lfWidth=0;
|
||||
return draft.points!=fontPoints ||
|
||||
return draft.points!=draft.baseline.points ||
|
||||
memcmp(&pending,&applied,offsetof(LOGFONTW,lfFaceName))!=0 ||
|
||||
_wcsicmp(pending.lfFaceName,applied.lfFaceName)!=0;
|
||||
}
|
||||
|
||||
struct OptionsWriteLock
|
||||
{
|
||||
HANDLE mutex=CreateMutexW(nullptr,FALSE,L"Local\\KSwitcher.Files.OptionsWrite");
|
||||
DWORD result=mutex?WaitForSingleObject(mutex,2000):WAIT_FAILED;
|
||||
|
||||
bool acquired() const { return result==WAIT_OBJECT_0 || result==WAIT_ABANDONED; }
|
||||
~OptionsWriteLock()
|
||||
{
|
||||
if(acquired()) ReleaseMutex(mutex);
|
||||
if(mutex) CloseHandle(mutex);
|
||||
}
|
||||
};
|
||||
|
||||
void updateOptionsApply(HWND dialog,const OptionsDraft &draft)
|
||||
{
|
||||
BOOL valid{};
|
||||
@@ -97,12 +124,13 @@ void updateOptionsApply(HWND dialog,const OptionsDraft &draft)
|
||||
const auto checked=[&](int id){return IsDlgButtonChecked(dialog,id)==BST_CHECKED;};
|
||||
DWORD fields=0;
|
||||
for(int i=0;i<4;++i) if(checked(205+i)) fields|=1u<<i;
|
||||
const bool dirty=!valid || width!=static_cast<UINT>(sizeWidth) ||
|
||||
checked(273)!=panels[0].wide || checked(274)!=panels[1].wide ||
|
||||
checked(202)!=alignExtensions || checked(204)!=alignFolders || checked(203)!=showDetails ||
|
||||
fields!=statusFields || draft.palette!=colors || draft.styles!=fileStyles ||
|
||||
draft.framePadding!=dialogFramePadding || optionsFontChanged(draft) ||
|
||||
optionHistoryKeys(dialog)!=commandHistoryKeys;
|
||||
const auto &base=draft.baseline;
|
||||
const bool dirty=!valid || width!=base.width ||
|
||||
checked(273)!=base.wide[0] || checked(274)!=base.wide[1] ||
|
||||
checked(202)!=base.extensions || checked(204)!=base.folders || checked(203)!=base.details ||
|
||||
fields!=base.fields || draft.palette!=base.palette || draft.styles!=base.styles ||
|
||||
draft.framePadding!=base.padding || optionsFontChanged(draft) ||
|
||||
optionHistoryKeys(dialog)!=base.keys;
|
||||
EnableWindow(GetDlgItem(dialog,optionsApply),dirty);
|
||||
InvalidateRect(GetDlgItem(dialog,fileStylePreview),nullptr,FALSE);
|
||||
}
|
||||
@@ -521,14 +549,31 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
SendDlgItemMessageW(dialog,201,EM_SETSEL,0,-1);
|
||||
return TRUE;
|
||||
}
|
||||
const auto keys=optionHistoryKeys(dialog);
|
||||
const auto owner=GetWindow(dialog,GW_OWNER);
|
||||
const OptionsWriteLock writeLock;
|
||||
if(!writeLock.acquired())
|
||||
{
|
||||
setStatus(L"Another window is saving Files settings. Try Apply again.");
|
||||
return TRUE;
|
||||
}
|
||||
const auto &base=draft->baseline;
|
||||
auto keys=optionHistoryKeys(dialog);
|
||||
loadCommandHistoryKeys();
|
||||
for(size_t i=0;i<keys.size();++i)
|
||||
if(keys[i]==base.keys[i]) keys[i]=commandHistoryKeys[i];
|
||||
if(!validHistoryKeys(keys))
|
||||
{
|
||||
MessageBoxW(dialog,L"Choose distinct shortcuts using Ctrl or Alt that are free in Files.",L"Command history",MB_OK|MB_ICONWARNING);
|
||||
return TRUE;
|
||||
}
|
||||
const auto owner=GetWindow(dialog,GW_OWNER);
|
||||
const bool stylesChanged=draft->styles!=fileStyles;
|
||||
const bool stylesChanged=draft->styles!=base.styles;
|
||||
loadFileStyles();
|
||||
if(stylesChanged && fileStyles!=base.styles && fileStyles!=draft->styles)
|
||||
{
|
||||
MessageBoxW(dialog,L"File type styles changed in another window. Reopen Options before editing them again.",
|
||||
L"KSwitcher Files Options",MB_OK|MB_ICONWARNING);
|
||||
return TRUE;
|
||||
}
|
||||
if(stylesChanged && !saveFileStyles(draft->styles))
|
||||
{
|
||||
MessageBoxW(dialog,L"Could not save the file type styles.",L"KSwitcher Files Options",MB_OK|MB_ICONWARNING);
|
||||
@@ -541,47 +586,57 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
MessageBoxW(dialog,L"Could not apply the selected font.",optionsTitles[Appearance],MB_OK|MB_ICONWARNING);
|
||||
return TRUE;
|
||||
}
|
||||
commandHistoryKeys=keys;
|
||||
savePreference(L"PreviousCommandKey",keys[0]);
|
||||
savePreference(L"CommandHistoryKey",keys[1]);
|
||||
if(stylesChanged) fileStyles=draft->styles;
|
||||
sizeWidth=static_cast<int>(width);
|
||||
alignExtensions=IsDlgButtonChecked(dialog,202)==BST_CHECKED;
|
||||
showDetails=IsDlgButtonChecked(dialog,203)==BST_CHECKED;
|
||||
alignFolders=IsDlgButtonChecked(dialog,204)==BST_CHECKED;
|
||||
statusFields=0;
|
||||
for(int i=0;i<4;++i)
|
||||
if(IsDlgButtonChecked(dialog,205+i)==BST_CHECKED) statusFields|=1u<<i;
|
||||
savePreference(L"SizeWidth",sizeWidth);
|
||||
savePreference(L"AlignExtensions",alignExtensions);
|
||||
savePreference(L"ShowDetails",showDetails);
|
||||
savePreference(L"AlignFolders",alignFolders);
|
||||
savePreference(L"StatusFields",statusFields);
|
||||
for(int i=0;i<ColorCount;++i)
|
||||
if(colors[i]!=draft->palette[i])
|
||||
{
|
||||
colors[i]=draft->palette[i];
|
||||
savePreference(std::to_wstring(i).c_str(),colors[i]);
|
||||
}
|
||||
refreshColors();
|
||||
if(draft->framePadding!=dialogFramePadding)
|
||||
const auto saveChanged=[](const wchar_t *name,DWORD value,DWORD initial)
|
||||
{
|
||||
dialogFramePadding=draft->framePadding;
|
||||
savePreference(dialogFrameOuterKey,dialogFramePadding.outer);
|
||||
savePreference(dialogFrameInnerKey,dialogFramePadding.inner);
|
||||
savePreference(dialogRowTextKey,dialogFramePadding.rowText);
|
||||
if(value!=initial) savePreference(name,value);
|
||||
};
|
||||
const auto checked=[&](int id){return IsDlgButtonChecked(dialog,id)==BST_CHECKED;};
|
||||
const auto inputKeys=optionHistoryKeys(dialog);
|
||||
saveChanged(L"PreviousCommandKey",inputKeys[0],base.keys[0]);
|
||||
saveChanged(L"CommandHistoryKey",inputKeys[1],base.keys[1]);
|
||||
saveChanged(L"SizeWidth",width,base.width);
|
||||
saveChanged(L"AlignExtensions",checked(202),base.extensions);
|
||||
saveChanged(L"AlignFolders",checked(204),base.folders);
|
||||
saveChanged(L"ShowDetails",checked(203),base.details);
|
||||
DWORD fields=preference(L"StatusFields",defaultStatusFields)&15;
|
||||
const auto initialFields=fields;
|
||||
for(int i=0;i<4;++i)
|
||||
{
|
||||
const DWORD bit=1u<<i;
|
||||
if(checked(205+i)!=((base.fields&bit)!=0)) fields=checked(205+i)?fields|bit:fields&~bit;
|
||||
}
|
||||
saveChanged(L"StatusFields",fields,initialFields);
|
||||
for(int i=0;i<ColorCount;++i)
|
||||
saveChanged(std::to_wstring(i).c_str(),draft->palette[i],base.palette[i]);
|
||||
saveChanged(dialogFrameOuterKey,draft->framePadding.outer,base.padding.outer);
|
||||
saveChanged(dialogFrameInnerKey,draft->framePadding.inner,base.padding.inner);
|
||||
saveChanged(dialogRowTextKey,draft->framePadding.rowText,base.padding.rowText);
|
||||
if(fontChanged)
|
||||
{
|
||||
savePreference(L"FontPoints",fontPoints);
|
||||
RegSetKeyValueW(HKEY_CURRENT_USER,schemeKey,L"DisplayFont",REG_BINARY,&displayFont,sizeof(displayFont));
|
||||
draft->font=displayFont;
|
||||
}
|
||||
for(int i=0;i<2;++i)
|
||||
{
|
||||
const bool wide=IsDlgButtonChecked(dialog,273+i)==BST_CHECKED;
|
||||
if(panels[i].wide!=wide) setMode(i,wide);
|
||||
}
|
||||
saveChanged(L"LeftWide",checked(273),base.wide[0]);
|
||||
saveChanged(L"RightWide",checked(274),base.wide[1]);
|
||||
loadFilesPreferences(owner);
|
||||
notifyFilesPreferences(owner);
|
||||
draft->baseline=OptionsBaseline{};
|
||||
draft->font=displayFont;
|
||||
draft->points=fontPoints;
|
||||
draft->styles=fileStyles;
|
||||
draft->palette=colors;
|
||||
draft->framePadding=dialogFramePadding;
|
||||
SetDlgItemInt(dialog,201,sizeWidth,FALSE);
|
||||
CheckDlgButton(dialog,202,alignExtensions);
|
||||
CheckDlgButton(dialog,203,showDetails);
|
||||
CheckDlgButton(dialog,204,alignFolders);
|
||||
CheckDlgButton(dialog,273,panels[0].wide);
|
||||
CheckDlgButton(dialog,274,panels[1].wide);
|
||||
for(int i=0;i<4;++i) CheckDlgButton(dialog,205+i,(statusFields>>i)&1u);
|
||||
setOptionHistoryKeys(dialog,commandHistoryKeys);
|
||||
if(draft->page==PanelColors && draft->section==fileStyleSection)
|
||||
updateFileStyleEditor(dialog,*draft,true);
|
||||
showOptionsPage(dialog,*draft,draft->page,draft->section);
|
||||
layout(owner);
|
||||
showFileDetails(panels[active]);
|
||||
RedrawWindow(owner,nullptr,nullptr,RDW_INVALIDATE|RDW_ERASE|RDW_ALLCHILDREN|RDW_UPDATENOW);
|
||||
|
||||
@@ -49,7 +49,7 @@ public:
|
||||
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 command[]=L"powershell.exe -NoLogo -NoProfile -NoExit -Command \"Remove-Module PSReadLine -ErrorAction SilentlyContinue; function global:prompt { $location = Get-Location; $path = if ($location.Provider.Name -eq 'FileSystem') { [Uri]::EscapeDataString($location.ProviderPath) }; ([string][char]27) + ']633;ks-directory:' + $path + [char]7 + [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";
|
||||
|
||||
@@ -56,7 +56,10 @@ class FilesTerminalView
|
||||
int viewportHeight{};
|
||||
public:
|
||||
bool initialized() const { return ready; }
|
||||
void enableTrace() { if(ready) web->PostWebMessageAsString(L"trace:on"); }
|
||||
void enableTrace(bool output)
|
||||
{
|
||||
if(ready) web->PostWebMessageAsString(output?L"trace:output":L"trace:on");
|
||||
}
|
||||
void setFont(const LOGFONTW &font,int points)
|
||||
{
|
||||
if(!ready) return;
|
||||
|
||||
+7
-1
@@ -737,6 +737,7 @@ namespace
|
||||
{
|
||||
std::recursive_mutex logGate;
|
||||
bool loggingEnabled = false;
|
||||
std::wstring logFilename = L"kswitcher.log";
|
||||
DiagnosticBuffer diagnosticBuffer;
|
||||
}
|
||||
size_t DiagnosticBuffer::expire(ULONGLONG now)
|
||||
@@ -832,6 +833,11 @@ void diagnosticOperation(const std::string &json)
|
||||
record["context"] = utf8(diagnosticBuffer.record(L"Operation complete", true, GetTickCount64()));
|
||||
log(L"Operation " + wide(record.dump(-1, ' ', true, nlohmann::json::error_handler_t::replace)));
|
||||
}
|
||||
void setLogFilename(std::wstring filename)
|
||||
{
|
||||
std::lock_guard lock(logGate);
|
||||
logFilename = std::move(filename);
|
||||
}
|
||||
void log(const std::wstring &message)
|
||||
{
|
||||
std::lock_guard lock(logGate);
|
||||
@@ -839,7 +845,7 @@ void log(const std::wstring &message)
|
||||
return;
|
||||
try
|
||||
{
|
||||
auto path = dataDirectory() / L"kswitcher.log";
|
||||
auto path = dataDirectory() / logFilename;
|
||||
std::error_code ec;
|
||||
auto size = std::filesystem::file_size(path, ec);
|
||||
if (!ec && size > 256 * 1024)
|
||||
|
||||
@@ -94,6 +94,7 @@ class DiagnosticBuffer
|
||||
void clear() { entries.clear(); }
|
||||
};
|
||||
void setLoggingEnabled(bool enabled);
|
||||
void setLogFilename(std::wstring filename);
|
||||
void expireDiagnostics(ULONGLONG now);
|
||||
void diagnosticEvent(const std::wstring &message, bool problem = false);
|
||||
void diagnosticOperation(const std::string &json);
|
||||
|
||||
+3
-1
@@ -1514,7 +1514,9 @@ Settings collectSettings(HWND w, SettingsView *v)
|
||||
s.undo = value(w, 213);
|
||||
s.clipboard = value(w, 214);
|
||||
s.screenshot = value(w, 215);
|
||||
s.filesHotkey = value(w, 216);
|
||||
auto filesHotkey = value(w, 216);
|
||||
if (s.filesHotkey || !filesHotkey.empty())
|
||||
s.filesHotkey = std::move(filesHotkey);
|
||||
s.maxItems = integer(w, 240);
|
||||
s.maxMiB = integer(w, 241);
|
||||
s.maxImageMiB = integer(w, 242);
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.77";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.83";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
@@ -292,11 +292,16 @@ int main()
|
||||
HWND picker = GetDlgItem(dialog, 728);
|
||||
RECT dialogBounds{};
|
||||
GetWindowRect(dialog, &dialogBounds);
|
||||
MONITORINFO monitor{sizeof(monitor)};
|
||||
require(GetMonitorInfoW(MonitorFromWindow(dialog, MONITOR_DEFAULTTONEAREST), &monitor),
|
||||
"monitor work area available");
|
||||
const auto targetX = std::clamp(dialogBounds.left + 32, monitor.rcWork.left, monitor.rcWork.right - 120);
|
||||
const auto targetY = std::clamp(dialogBounds.top + 100, monitor.rcWork.top, monitor.rcWork.bottom - 48);
|
||||
HWND target = CreateWindowExW(WS_EX_TOPMOST | WS_EX_NOACTIVATE, L"EDIT", L"Picker fixture",
|
||||
WS_POPUP | WS_VISIBLE, dialogBounds.left + 32, dialogBounds.top + 100, 120, 48,
|
||||
WS_POPUP | WS_VISIBLE, targetX, targetY, 120, 48,
|
||||
nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
|
||||
require(picker && target, "window picker and target available");
|
||||
POINT drop{dialogBounds.left + 33, dialogBounds.top + 101};
|
||||
POINT drop{targetX + 1, targetY + 1};
|
||||
require(GetAncestor(WindowFromPoint(drop), GA_ROOT) == target, "fixture window is under the drop point");
|
||||
ScreenToClient(picker, &drop);
|
||||
SendMessageW(picker, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(8, 8));
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Executable,
|
||||
[Parameter(Mandatory)][string]$Fixture
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class FilesActivationUi {
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Rect { public int Left, Top, Right, Bottom; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct GuiInfo {
|
||||
public int Size, Flags;
|
||||
public IntPtr Active, Focus, Capture, MenuOwner, MoveSize, Caret;
|
||||
public Rect CaretRect;
|
||||
}
|
||||
[DllImport("user32.dll")] public static extern IntPtr GetDlgItem(IntPtr window, int id);
|
||||
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string type, string title);
|
||||
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr window);
|
||||
[DllImport("user32.dll")] public static extern bool PostMessage(IntPtr window, uint msg, IntPtr w, IntPtr l);
|
||||
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr window, IntPtr process);
|
||||
[DllImport("user32.dll")] static extern bool GetGUIThreadInfo(uint thread, ref GuiInfo info);
|
||||
[DllImport("user32.dll", EntryPoint="SendMessageW")] public static extern IntPtr Send(IntPtr window, uint msg, IntPtr w, IntPtr l);
|
||||
[DllImport("user32.dll", EntryPoint="SendMessageW")] static extern IntPtr SendRect(IntPtr window, uint msg, IntPtr w, out Rect rect);
|
||||
[DllImport("user32.dll", EntryPoint="SendMessageW", CharSet=CharSet.Unicode)] public static extern IntPtr Read(IntPtr window, uint msg, IntPtr w, StringBuilder text);
|
||||
[DllImport("user32.dll", EntryPoint="SendMessageW", CharSet=CharSet.Unicode)] public static extern IntPtr Write(IntPtr window, uint msg, IntPtr w, string text);
|
||||
[DllImport("shell32.dll")] public static extern void SHChangeNotify(int eventId, uint flags, IntPtr first, IntPtr second);
|
||||
|
||||
public static IntPtr Focus(IntPtr window) {
|
||||
var info = new GuiInfo { Size = Marshal.SizeOf(typeof(GuiInfo)) };
|
||||
if (!GetGUIThreadInfo(GetWindowThreadProcessId(window, IntPtr.Zero), ref info))
|
||||
throw new InvalidOperationException("Could not read Files focus.");
|
||||
return info.Focus;
|
||||
}
|
||||
|
||||
public static void ClickRow(IntPtr list, int row, bool doubleClick) {
|
||||
Rect rect;
|
||||
if (SendRect(list, 0x198, new IntPtr(row), out rect).ToInt32() == -1)
|
||||
throw new InvalidOperationException("Missing listbox row.");
|
||||
var point = new IntPtr(((rect.Top + rect.Bottom) / 2 << 16) | (rect.Left + 6));
|
||||
PostMessage(list, 0x201, new IntPtr(1), point);
|
||||
PostMessage(list, 0x202, IntPtr.Zero, point);
|
||||
if (doubleClick) {
|
||||
PostMessage(list, 0x203, new IntPtr(1), point);
|
||||
PostMessage(list, 0x202, IntPtr.Zero, point);
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
function Wait-Until([string]$Description, [scriptblock]$Condition) {
|
||||
$timer = [Diagnostics.Stopwatch]::StartNew()
|
||||
while ($timer.Elapsed.TotalSeconds -lt 15) {
|
||||
if (& $Condition) { return }
|
||||
if ($process -and $process.HasExited) { throw "Files exited while waiting for $Description" }
|
||||
Start-Sleep -Milliseconds 50
|
||||
}
|
||||
throw "Timed out waiting for $Description"
|
||||
}
|
||||
|
||||
function Read-Text($Control, $Message = 0x0D, $Parameter = 32768) {
|
||||
$buffer = [Text.StringBuilder]::new(32768)
|
||||
[void][FilesActivationUi]::Read($Control, $Message, [IntPtr]$Parameter, $buffer)
|
||||
$buffer.ToString()
|
||||
}
|
||||
|
||||
function Find-Row($List, [string]$Name) {
|
||||
$count = [FilesActivationUi]::Send($List, 0x18B, [IntPtr]::Zero, [IntPtr]::Zero).ToInt32()
|
||||
for ($row = 0; $row -lt $count; $row++) {
|
||||
if ((Read-Text $List 0x189 $row) -ceq $Name) { return $row }
|
||||
}
|
||||
-1
|
||||
}
|
||||
|
||||
function Select-Row($List, [string]$Name, [switch]$DoubleClick) {
|
||||
Wait-Until "row $Name" { (Find-Row $List $Name) -ge 0 }
|
||||
$row = Find-Row $List $Name
|
||||
[void][FilesActivationUi]::Send($List, 0x197, [IntPtr]$row, [IntPtr]::Zero)
|
||||
[FilesActivationUi]::ClickRow($List, $row, $DoubleClick.IsPresent)
|
||||
if (!$DoubleClick) {
|
||||
Wait-Until "selection of $Name" {
|
||||
[FilesActivationUi]::Focus($window) -eq $inputBox -and
|
||||
[FilesActivationUi]::Send($List, 0x188, [IntPtr]::Zero, [IntPtr]::Zero).ToInt32() -eq (Find-Row $List $Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Press-Enter($Control) {
|
||||
[void][FilesActivationUi]::PostMessage($Control, 0x100, [IntPtr]13, [IntPtr]::Zero)
|
||||
}
|
||||
|
||||
function Invoke-Row($List, [string]$Name, [bool]$DoubleClick) {
|
||||
Select-Row $List $Name -DoubleClick:$DoubleClick
|
||||
if (!$DoubleClick) { Press-Enter $inputBox }
|
||||
}
|
||||
|
||||
function Send-Terminal([string]$Command) {
|
||||
[void][FilesActivationUi]::Write($inputBox, 0x0C, [IntPtr]::Zero, $Command)
|
||||
Press-Enter $inputBox
|
||||
}
|
||||
|
||||
function Assert-Open($List, $Target, [bool]$DoubleClick) {
|
||||
$report = "$Target.opened"
|
||||
Remove-Item -LiteralPath $report -ErrorAction SilentlyContinue
|
||||
Invoke-Row $List ([IO.Path]::GetFileName($Target)) $DoubleClick
|
||||
Wait-Until "launch of $Target (double click: $DoubleClick)" {
|
||||
(Test-Path -LiteralPath $report) -and [IO.File]::ReadAllText($report) -eq 'opened'
|
||||
}
|
||||
}
|
||||
|
||||
$unique = [Guid]::NewGuid().ToString('N')
|
||||
$root = Join-Path ([IO.Path]::GetTempPath()) "KSwitcher test $unique"
|
||||
$profile = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Software\KSwitcher\Files')
|
||||
$classes = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Software\Classes')
|
||||
$extension = ".ksfiles$unique"
|
||||
$progId = "KSwitcher.Files.Test.$unique"
|
||||
$saved = @{}
|
||||
$profileValues = 'LeftPath', 'RightPath', 'LeftCursor', 'RightCursor', 'LeftWide', 'RightWide',
|
||||
'ShowDetails', 'StatusFields', 'CommandHistory', 'WindowPlacement'
|
||||
$process = $null
|
||||
$trace = $null
|
||||
try {
|
||||
foreach ($name in $profileValues) {
|
||||
if ($name -in $profile.GetValueNames()) {
|
||||
$saved[$name] = @($profile.GetValue($name), $profile.GetValueKind($name))
|
||||
}
|
||||
}
|
||||
$leftDir = Join-Path $root 'Left panel'
|
||||
$rightDir = Join-Path $root 'Right panel'
|
||||
$app = Join-Path $root 'Application'
|
||||
New-Item -ItemType Directory -Path $leftDir, $rightDir, $app, "$leftDir\Test folder", "$rightDir\Test folder" | Out-Null
|
||||
$source = Split-Path (Resolve-Path -LiteralPath $Executable)
|
||||
$testExe = Join-Path $app 'KSwitcher.Files.exe'
|
||||
Copy-Item -LiteralPath $Executable -Destination $testExe
|
||||
Copy-Item -LiteralPath "$source\WebView2Loader.dll", "$source\terminal" -Destination $app -Recurse
|
||||
$program = Join-Path $leftDir 'Test program.exe'
|
||||
Copy-Item -LiteralPath $Fixture -Destination $program
|
||||
$document = Join-Path $rightDir "Test document$extension"
|
||||
[IO.File]::WriteAllText($document, 'Document fixture')
|
||||
$association = $classes.CreateSubKey($extension)
|
||||
$association.SetValue('', $progId)
|
||||
$association.Dispose()
|
||||
$handler = $classes.CreateSubKey("$progId\shell\open\command")
|
||||
$handler.SetValue('', ('"{0}" "%1"' -f (Resolve-Path -LiteralPath $Fixture).Path))
|
||||
$handler.Dispose()
|
||||
[FilesActivationUi]::SHChangeNotify(0x08000000, 0, [IntPtr]::Zero, [IntPtr]::Zero)
|
||||
|
||||
$batch = Join-Path $leftDir "Test's script.BAT"
|
||||
$cmd = Join-Path $rightDir 'Test script.CMD'
|
||||
[IO.File]::WriteAllLines($batch, @('@echo off', 'echo BAT=%KS_TEST_STATE%'))
|
||||
[IO.File]::WriteAllLines($cmd, @('@echo off', 'echo CMD=%KS_TEST_STATE%'))
|
||||
$profile.SetValue('LeftPath', $leftDir)
|
||||
$profile.SetValue('RightPath', $rightDir)
|
||||
foreach ($name in 'LeftCursor', 'RightCursor') { $profile.DeleteValue($name, $false) }
|
||||
foreach ($name in 'LeftWide', 'RightWide', 'ShowDetails') { $profile.SetValue($name, 1) }
|
||||
$profile.SetValue('StatusFields', 3)
|
||||
|
||||
$process = Start-Process -FilePath $testExe -ArgumentList '--terminal-output-trace' -PassThru
|
||||
$trace = Join-Path ([IO.Path]::GetTempPath()) "KSwitcher.Files.terminal-trace-$($process.Id).log"
|
||||
Wait-Until 'Files window' { $process.Refresh(); $process.MainWindowHandle -ne [IntPtr]::Zero }
|
||||
$window = $process.MainWindowHandle
|
||||
$left = [FilesActivationUi]::GetDlgItem($window, 102)
|
||||
$right = [FilesActivationUi]::GetDlgItem($window, 103)
|
||||
$status = [FilesActivationUi]::GetDlgItem($window, 144)
|
||||
$output = [FilesActivationUi]::GetDlgItem($window, 145)
|
||||
$inputBox = [FilesActivationUi]::FindWindowEx($window, [IntPtr]::Zero, 'Edit', $null)
|
||||
|
||||
Select-Row $left '..'
|
||||
[void][FilesActivationUi]::Send($right, 0x186, [IntPtr]::Zero, [IntPtr]::Zero)
|
||||
[void][FilesActivationUi]::Write($inputBox, 0x0C, [IntPtr]::Zero, 'draft command')
|
||||
[void][FilesActivationUi]::PostMessage($inputBox, 0x100, [IntPtr]9, [IntPtr]::Zero)
|
||||
[void][FilesActivationUi]::PostMessage($inputBox, 0x100, [IntPtr]40, [IntPtr]::Zero)
|
||||
Wait-Until 'Tab and Down navigating the right pane with command-line focus' {
|
||||
[FilesActivationUi]::Focus($window) -eq $inputBox -and
|
||||
[FilesActivationUi]::Send($right, 0x188, [IntPtr]::Zero, [IntPtr]::Zero).ToInt32() -eq 1 -and
|
||||
[FilesActivationUi]::Send($left, 0x188, [IntPtr]::Zero, [IntPtr]::Zero).ToInt32() -eq (Find-Row $left '..')
|
||||
}
|
||||
if ((Read-Text $inputBox) -cne 'draft command') { throw 'Pane navigation changed the command line.' }
|
||||
[void][FilesActivationUi]::Write($inputBox, 0x0C, [IntPtr]::Zero, '')
|
||||
Write-Output 'Tab and Down navigated the panes while preserving command-line focus and text.'
|
||||
|
||||
foreach ($double in $false, $true) {
|
||||
Assert-Open $left $program $double
|
||||
Assert-Open $right $document $double
|
||||
}
|
||||
Invoke-Row $left '[Test folder]' $false
|
||||
Wait-Until 'folder navigation by Enter' {
|
||||
(Read-Text ([FilesActivationUi]::GetDlgItem($window, 142))) -eq "$leftDir\Test folder"
|
||||
}
|
||||
Invoke-Row $left '..' $false
|
||||
Invoke-Row $right '[Test folder]' $true
|
||||
Wait-Until 'folder navigation by double click' {
|
||||
(Read-Text ([FilesActivationUi]::GetDlgItem($window, 143))) -eq "$rightDir\Test folder"
|
||||
}
|
||||
Invoke-Row $right '..' $false
|
||||
Write-Output 'Enter and double-click opened folders, the registered document, and the standalone EXE.'
|
||||
|
||||
$sessionValue = $unique.Substring(0, 8)
|
||||
Send-Terminal ('$global:ksSession = "{0}"; $env:KS_TEST_STATE = $ksSession; "READY=" + $ksSession' -f $sessionValue)
|
||||
Wait-Until 'initialized terminal session and rendered test marker' {
|
||||
(Get-Content -LiteralPath $trace -ErrorAction SilentlyContinue) -contains "READY=$sessionValue"
|
||||
}
|
||||
Invoke-Row $left ([IO.Path]::GetFileName($batch)) $false
|
||||
Wait-Until 'visible BAT output with the existing session environment' {
|
||||
[FilesActivationUi]::IsWindowVisible($output) -and
|
||||
(Get-Content -LiteralPath $trace) -contains "BAT=$sessionValue"
|
||||
}
|
||||
Invoke-Row $right ([IO.Path]::GetFileName($cmd)) $true
|
||||
Wait-Until 'visible CMD output with the existing session environment' {
|
||||
[FilesActivationUi]::IsWindowVisible($output) -and
|
||||
(Get-Content -LiteralPath $trace) -contains "CMD=$sessionValue"
|
||||
}
|
||||
Send-Terminal '"AFTER=" + $global:ksSession'
|
||||
Wait-Until 'unchanged PowerShell session state' {
|
||||
(Get-Content -LiteralPath $trace) -contains "AFTER=$sessionValue"
|
||||
}
|
||||
Assert-Open $left $program $false
|
||||
Write-Output 'BAT and CMD ran in the existing terminal and rendered their output.'
|
||||
|
||||
$external = 'External change.txt'
|
||||
Select-Row $left ([IO.Path]::GetFileName($program))
|
||||
foreach ($dir in $leftDir, $rightDir) { [IO.File]::WriteAllText((Join-Path $dir $external), 'one') }
|
||||
Wait-Until 'external creation in both panels' {
|
||||
(Find-Row $left $external) -ge 0 -and (Find-Row $right $external) -ge 0
|
||||
}
|
||||
if ([FilesActivationUi]::Focus($window) -ne $inputBox -or
|
||||
[FilesActivationUi]::Send($left, 0x188, [IntPtr]::Zero, [IntPtr]::Zero).ToInt32() -ne
|
||||
(Find-Row $left ([IO.Path]::GetFileName($program)))) { throw 'Creation moved focus or selection.' }
|
||||
|
||||
foreach ($panel in @(@{ List = $left; Dir = $leftDir }, @{ List = $right; Dir = $rightDir })) {
|
||||
Select-Row $panel.List $external
|
||||
Wait-Until 'initial file size' { (Read-Text $status) -match ' 3 B$' }
|
||||
[IO.File]::AppendAllText((Join-Path $panel.Dir $external), 'two!')
|
||||
Wait-Until 'updated file size' { (Read-Text $status) -match ' 7 B$' }
|
||||
if ([FilesActivationUi]::Focus($window) -ne $inputBox) { throw 'Modification moved focus.' }
|
||||
}
|
||||
|
||||
foreach ($dir in $leftDir, $rightDir) { [IO.File]::Delete((Join-Path $dir $external)) }
|
||||
Wait-Until 'external deletion in both panels' {
|
||||
(Find-Row $left $external) -eq -1 -and (Find-Row $right $external) -eq -1
|
||||
}
|
||||
if ([FilesActivationUi]::Focus($window) -ne $inputBox) { throw 'Deletion moved focus.' }
|
||||
Write-Output 'Both panels refreshed external creation, modification and deletion while preserving focus.'
|
||||
} finally {
|
||||
if ($process -and !$process.HasExited) {
|
||||
[void]$process.CloseMainWindow()
|
||||
if (!$process.WaitForExit(4000)) { $process.Kill(); $process.WaitForExit() }
|
||||
}
|
||||
foreach ($name in $profileValues) {
|
||||
if ($saved.ContainsKey($name)) { $profile.SetValue($name, $saved[$name][0], $saved[$name][1]) }
|
||||
else { $profile.DeleteValue($name, $false) }
|
||||
}
|
||||
$profile.Dispose()
|
||||
$classes.DeleteSubKeyTree($extension, $false)
|
||||
$classes.DeleteSubKeyTree($progId, $false)
|
||||
$classes.Dispose()
|
||||
[FilesActivationUi]::SHChangeNotify(0x08000000, 0, [IntPtr]::Zero, [IntPtr]::Zero)
|
||||
if ($trace) { Remove-Item -LiteralPath $trace -ErrorAction SilentlyContinue }
|
||||
$resolvedRoot = Resolve-Path -LiteralPath $root -ErrorAction SilentlyContinue
|
||||
$tempPrefix = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + '\'
|
||||
if ($resolvedRoot -and $resolvedRoot.Path.StartsWith($tempPrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
Remove-Item -LiteralPath $resolvedRoot.Path -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#include <windows.h>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
int wmain(int argc,wchar_t **argv)
|
||||
{
|
||||
// A launch through the embedded terminal would inherit its console.
|
||||
DWORD consoleProcesses[1]{};
|
||||
if(GetConsoleProcessList(consoleProcesses,1)>1) return 1;
|
||||
wchar_t executable[32768]{};
|
||||
const auto length=GetModuleFileNameW(nullptr,executable,static_cast<DWORD>(std::size(executable)));
|
||||
if(!length || length>=std::size(executable)) return 1;
|
||||
const std::filesystem::path target=argc==2?argv[1]:executable;
|
||||
auto report=target;
|
||||
report+=L".opened";
|
||||
std::ofstream result(report,std::ios::binary);
|
||||
result<<"opened";
|
||||
return result?0:1;
|
||||
}
|
||||
Reference in new issue
Block a user