Compare commits

...
12 changed files with 366 additions and 140 deletions

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.24)
project(KSwitcher VERSION 0.6.82 LANGUAGES CXX RC)
project(KSwitcher VERSION 0.6.83 LANGUAGES CXX RC)
if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif()
+23 -10
View File
@@ -10,14 +10,22 @@ Far Manager є зразком для оформлення файлового м
термінала, а їхній вивід з’являється під панелями. Щоб відкрити файл із
панелі LAN, спочатку скопіюй його до локальної папки.
- У командному рядку Ctrl+V і Shift+Insert вставляють текст із буфера обміну
в позицію курсора або замінюють виділення. Якщо фокус на файловій панелі,
ці клавіші переводять його до командного рядка й вставляють текст там.
Enter запускає команду. Escape очищає поле й залишає фокус у командному рядку.
- У головному вікні клавіатурний фокус залишається в командному рядку.
Tab перемикає активну панель, клік вибирає рядок, а стрілки вгору й вниз,
Page Up і Page Down пересувають курсор у панелі навіть під час набору команди.
Меню й діалоги тимчасово приймають фокус. Ctrl+V і Shift+Insert вставляють
текст із буфера обміну в позицію курсора або замінюють виділення.
Enter запускає введену команду або відкриває поточний елемент, якщо рядок
порожній. Escape очищає рядок.
Промпт і текст команди вирівняні по спільній базовій лінії.
- Ctrl+E підставляє останню команду та переводить фокус із файлової панелі
до командного рядка. Повторне натискання
- Ctrl+F вставляє повний шлях елемента під курсором активної панелі в позицію
курсора командного рядка або замінює виділення. Подвійні лапки додаються
лише до шляхів із пробілами. Для `..` вставляється шлях батьківської папки.
- Ctrl+C копіює весь введений у командний рядок текст, навіть коли виділено
лише його частину.
- Ctrl+E підставляє останню команду. Повторне натискання
підставляє старішу, доки не досягне початку історії. Alt+F8 відкриває історію
з групуванням за датами. Enter або подвійний клік у списку підставляє вибрану
команду в поле. Для запуску натисни Enter у самому полі.
@@ -47,7 +55,12 @@ 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** у треї відкриває нове вікно.
@@ -60,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.
@@ -110,7 +123,7 @@ Far Manager є зразком для оформлення файлового м
- У широкому режимі ширина під вирівняні розширення залежить від найдовшого розширення в поточній панелі. За вузької панелі залишай місце для імені та колонки Size.
- Бірюзовий курсор поточного рядка видно лише в активній панелі, зокрема коли клавіатурний фокус переходить у командний рядок. Кожна панель зберігає свою позицію курсора. Коли панелі видимі, F5 і F6 з командного рядка діють на активну панель.
- Бірюзовий курсор поточного рядка видно в активній панелі. Кожна панель зберігає свою позицію курсора. Коли панелі видимі, F5 і F6 з командного рядка діють на активну панель.
- Клік у панелі нижче списку ставить курсор на останній рядок, а клік вище списку — на перший.
- Поточний шлях відображається по центру верхньої подвійної лінії панелі. Лінію видно з обох боків напису. Бірюзове тло активної панелі охоплює лише напис шляху з невеликими відступами. Довгий шлях скорочується в межах панелі.
@@ -162,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.
+6
View File
@@ -1,5 +1,11 @@
# 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.
+1 -1
View File
@@ -1 +1 @@
84ba82bdf179419c6cc7a612b12c6adb962a47c55aacee311861e9843be1e683 KSwitcher-win-x64.zip
6f5669ed42ffc70ecdf5e3efe6a1ec549abfca07d3d1b1d9c384f2d3dc99d0c0 KSwitcher-win-x64.zip
+54 -54
View File
@@ -1,131 +1,131 @@
{
"schema": 1,
"version": "0.6.82",
"files": [
{
"path": "KSwitcher.exe",
"size": 2341376,
"sha256": "8017fa3d840d4e89871b926ceea6c53d99238ba8145a8158930098c3fd7b8485",
"path": "KSwitcher.exe"
"sha256": "b6ed0279b8e92427f6e8414e5dc7da4e2a77635106e71573cc55b299820c8ba7"
},
{
"path": "KSwitcher.Config.exe",
"size": 2341888,
"sha256": "cbb6624c0382d3a9bd1aa65542f9c2475a171cd11d55372b1c3a3c21fe65d86c",
"path": "KSwitcher.Config.exe"
"sha256": "6f22e2df935cea2ce5400f22d69c42008513946d926b0ffff116de5dac705210"
},
{
"path": "KSwitcher.Screenshot.exe",
"size": 3319808,
"sha256": "bc87038c9b090cea36475d3921e6d8a7567d407a447903291f45301fd0e4394d",
"path": "KSwitcher.Screenshot.exe"
"sha256": "af443bcf13f913aed6cbff59e9a34ce701552d4e4ce065b7b24700e3ed1347d4"
},
{
"size": 1829888,
"sha256": "487e51da766bab3c1e2fbcb88d374ea3a03cda6a5736f1972b861cfff68af82f",
"path": "KSwitcher.Files.exe"
"path": "KSwitcher.Files.exe",
"size": 1833472,
"sha256": "c6bf0a744c46cbfd1cbfb52bd4b70503ef0a799f378bad97cdd01ddf3b21737f"
},
{
"path": "WebView2Loader.dll",
"size": 194912,
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6",
"path": "WebView2Loader.dll"
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6"
},
{
"path": "terminal/addon-fit-LICENSE.txt",
"size": 1103,
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f",
"path": "terminal/addon-fit-LICENSE.txt"
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f"
},
{
"path": "terminal/addon-fit.js",
"size": 1521,
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527",
"path": "terminal/addon-fit.js"
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527"
},
{
"path": "terminal/index.html",
"size": 527,
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef",
"path": "terminal/index.html"
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef"
},
{
"size": 3978,
"sha256": "cc6c9cbb3684dbcda1ff587492b5a2047dd62d79b16a037bbc1d9b261afa3f78",
"path": "terminal/terminal.js"
"path": "terminal/terminal.js",
"size": 4167,
"sha256": "d60e6ff8948b6616695297695b05ef6e74b497a8e32e6031fc2e4fe2cca39ca7"
},
{
"path": "terminal/xterm-LICENSE.txt",
"size": 1261,
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd",
"path": "terminal/xterm-LICENSE.txt"
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd"
},
{
"path": "terminal/xterm.css",
"size": 7112,
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6",
"path": "terminal/xterm.css"
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6"
},
{
"path": "terminal/xterm.js",
"size": 488663,
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972",
"path": "terminal/xterm.js"
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972"
},
{
"path": "LICENSE",
"size": 4401,
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
"path": "LICENSE"
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c"
},
{
"path": "json-LICENSE.txt",
"size": 1075,
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86",
"path": "json-LICENSE.txt"
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86"
},
{
"path": "README.md",
"size": 93885,
"sha256": "df6d40d0eb66ad7da8db1cffe6bd211de70dfee474cc228e041c821b3940718b",
"path": "README.md"
"sha256": "df6d40d0eb66ad7da8db1cffe6bd211de70dfee474cc228e041c821b3940718b"
},
{
"path": "LAN-PREVIEW.md",
"size": 11418,
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada",
"path": "LAN-PREVIEW.md"
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada"
},
{
"path": "dictionaries/en.txt",
"size": 682867,
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21",
"path": "dictionaries/en.txt"
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21"
},
{
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
"size": 20138,
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5",
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt"
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5"
},
{
"path": "dictionaries/README.md",
"size": 2265,
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
"path": "dictionaries/README.md"
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311"
},
{
"path": "dictionaries/ru.txt",
"size": 1060364,
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b",
"path": "dictionaries/ru.txt"
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b"
},
{
"path": "dictionaries/sources.json",
"size": 1280,
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
"path": "dictionaries/sources.json"
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408"
},
{
"path": "dictionaries/uk.txt",
"size": 940799,
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
"path": "dictionaries/uk.txt"
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf"
},
{
"path": "tools/README.txt",
"size": 993,
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
"path": "tools/README.txt"
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a"
},
{
"path": "tools/StayHere.exe",
"size": 134656,
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376",
"path": "tools/StayHere.exe"
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376"
},
{
"path": "tools/StayHere-README.md",
"size": 2796,
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
"path": "tools/StayHere-README.md"
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413"
}
]
],
"version": "0.6.83"
}
+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,82,0
PRODUCTVERSION 0,6,82,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.82"
VALUE "FileVersion", "0.6.83"
END
END
BLOCK "VarFileInfo"
+4 -1
View File
@@ -51,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;
+2 -1
View File
@@ -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;
});
+252 -63
View File
@@ -22,6 +22,7 @@
#include "files_terminal_view.h"
#include "files_lan_ipc.h"
#include "lan_files.h"
#include "platform.h"
namespace
{
@@ -336,7 +337,8 @@ bool refreshDriveAddresses{};
ULONGLONG driveOpened{};
int drivePickerWidth{};
int pickerPanel{};
bool altAlone{};
std::wstring panelSearchPrefix;
HWND panelSearchPopup{};
enum Color { PanelBlue, Text, Frame, Dialog, Selection, Footer, PanelCursor, ColorCount };
constexpr std::array<COLORREF,ColorCount> defaultColors{RGB(0,0,128), RGB(230,255,255), RGB(0,255,255),
RGB(0,128,128), RGB(0,0,0), RGB(0,128,128), RGB(0,128,128)};
@@ -738,6 +740,24 @@ void paintScrollPosition(HWND parent, HDC dc, HWND list, COLORREF background)
FillRect(dc, &track, static_cast<HBRUSH>(GetStockObject(DC_BRUSH)));
}
void placePanelSearch(HWND window)
{
RECT panel{};
GetWindowRect(panels[active].list,&panel);
MapWindowPoints(nullptr,window,reinterpret_cast<POINT *>(&panel),2);
const auto frame=dialogFrameMetrics(GetDpiForWindow(window));
const int width=std::min(static_cast<int>(panel.right-panel.left),24*cellWidth+2*frame.content());
const int height=textHeight+2*frame.content();
SetWindowPos(panelSearchPopup,HWND_TOP,(panel.left+panel.right-width)/2,
std::max(panel.top,panel.bottom+textHeight-height),width,height,SWP_NOACTIVATE);
}
void closePanelSearch()
{
panelSearchPrefix.clear();
if(IsWindowVisible(panelSearchPopup)) ShowWindow(panelSearchPopup,SW_HIDE);
}
void layout(HWND window)
{
RECT rect{};
@@ -804,6 +824,7 @@ void layout(HWND window)
const int count=static_cast<int>(std::size(commands));
place(commands[i],i*width/count,height-textHeight,(i+1)*width/count-i*width/count,textHeight);
}
if(IsWindowVisible(panelSearchPopup)) placePanelSearch(window);
RedrawWindow(window,nullptr,nullptr,RDW_INVALIDATE|RDW_ERASE|RDW_ALLCHILDREN);
}
@@ -822,6 +843,77 @@ void showTerminal(HWND owner,bool show)
}
void open(Panel &panel,const fs::path &directory,bool offerElevation);
void restoreCommandFocus();
void activatePanel(int index)
{
const auto previous=active;
active=index;
showFileDetails(panels[index]);
if(previous==active) return;
for(const auto &panel:panels)
{
InvalidateRect(panel.path,nullptr,FALSE);
InvalidateRect(panel.list,nullptr,FALSE);
}
layout(GetParent(panels[index].list));
}
std::wstring terminalInputText()
{
std::wstring value(GetWindowTextLengthW(terminalInput)+1,L'\0');
value.resize(GetWindowTextW(terminalInput,value.data(),static_cast<int>(value.size())));
return value;
}
void copyTerminalInput(HWND owner)
{
const auto value=terminalInputText();
if(value.empty()) return;
try { ks::writeClipboard(owner,ks::textClip(value)); }
catch(const std::exception &) { setStatus(L"Could not copy the command line."); }
}
LRESULT currentPanelRow(const Panel &panel)
{
const auto row=SendMessageW(panel.list,LB_GETCURSEL,0,0);
return row>=0 && static_cast<size_t>(row)<panel.rows.size()?row:LB_ERR;
}
std::wstring powerShellPath(const fs::path &path)
{
std::wstring quoted=L"'";
for(const auto character:path.native())
{
// PowerShell recognizes typographic apostrophes as quote delimiters too.
if(character==L'\'' || character==L'\u2018' || character==L'\u2019') quoted+=character;
quoted+=character;
}
return quoted+L"'";
}
void insertCurrentPanelPath()
{
const auto &panel=panels[active];
const auto row=currentPanelRow(panel);
if(row==LB_ERR) return;
const auto &path=panel.rows[row].native();
std::wstring quoted;
if(path.find(L' ')!=std::wstring::npos)
{
quoted=L"\"";
for(const auto character:path)
{
if(character==L'`' || character==L'$' || character==L'"' ||
character==L'\u201c' || character==L'\u201d') quoted+=L'`';
quoted+=character;
}
quoted+=L'"';
}
SetFocus(terminalInput);
SendMessageW(terminalInput,EM_REPLACESEL,TRUE,
reinterpret_cast<LPARAM>(quoted.empty()?path.c_str():quoted.c_str()));
}
bool startTerminal(HWND owner)
{
@@ -865,6 +957,8 @@ bool startTerminal(HWND owner)
{
if(GetForegroundWindow()==owner) SetFocus(terminalInput);
}
else if(message==L"insert-path") insertCurrentPanelPath();
else if(message==L"copy-command-line") SendMessageW(terminalInput,WM_COPY,0,0);
else if(message==L"toggle") showTerminal(owner,!terminalVisible);
else if(!terminalVisible && (message==L"grow" || message==L"shrink"))
{
@@ -928,6 +1022,12 @@ bool submitTerminalCommand(const Panel &panel,const std::wstring &value)
LRESULT CALLBACK terminalInputKeys(HWND input,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR)
{
if(message==WM_COPY || (message==WM_CHAR && wp==3))
{
copyTerminalInput(GetParent(input));
return 0;
}
if(message==WM_CHAR && wp==VK_TAB && !terminalVisible) return 0;
if(message==WM_CHAR && wp==VK_ESCAPE)
{
recallCommand(terminalHistory.size());
@@ -962,31 +1062,24 @@ LRESULT CALLBACK terminalInputKeys(HWND input,UINT message,WPARAM wp,LPARAM lp,U
SendMessageW(panels[active].list,WM_KEYDOWN,VK_RETURN,lp);
return 0;
}
std::wstring value(GetWindowTextLengthW(input)+1,L'\0');
value.resize(GetWindowTextW(input,value.data(),static_cast<int>(value.size())));
const auto value=terminalInputText();
if(!submitTerminalCommand(panels[active],value)) return 0;
if(value.find_first_not_of(L" \t\r\n")!=std::wstring::npos) rememberCommand(value);
terminalHistoryIndex=terminalHistory.size();
SetWindowTextW(input,L"");
return 0;
}
if(message==WM_CHAR && wp==3 && terminal.alive())
if(message==WM_KEYDOWN && !terminalVisible && wp==VK_TAB)
{
DWORD first{},last{};
SendMessageW(input,EM_GETSEL,reinterpret_cast<WPARAM>(&first),reinterpret_cast<LPARAM>(&last));
if(first==last)
{
terminal.write(std::wstring(1,L'\x03'));
return 0;
}
activatePanel(1-active);
return 0;
}
if(message==WM_KEYDOWN && !terminalVisible &&
(wp==VK_TAB || (wp>=VK_F1 && wp<=VK_F12) ||
((wp>=VK_F1 && wp<=VK_F12) || wp==VK_UP || wp==VK_DOWN || wp==VK_PRIOR || wp==VK_NEXT ||
(!GetWindowTextLengthW(input) &&
((wp>=VK_PRIOR && wp<=VK_DOWN) || wp==VK_INSERT || wp==VK_DELETE))))
(wp==VK_HOME || wp==VK_END || wp==VK_LEFT || wp==VK_RIGHT || wp==VK_INSERT || wp==VK_DELETE))))
{
SendMessageW(panels[active].list,WM_KEYDOWN,wp,lp);
if(IsWindow(input)) SetFocus(input);
return 0;
}
if(message==WM_KEYDOWN && (wp==VK_UP || wp==VK_DOWN))
@@ -1586,14 +1679,7 @@ void openPanelFile(const Panel &panel, const fs::path &file, const wchar_t *verb
};
if(!verb && (isExtension(L".bat") || isExtension(L".cmd")))
{
std::wstring command=L"& '";
for(const auto character:file.native())
{
// PowerShell recognizes typographic apostrophes as quote delimiters too.
if(character==L'\'' || character==L'\u2018' || character==L'\u2019') command+=character;
command+=character;
}
command+=L"'";
const auto command=L"& "+powerShellPath(file);
if(submitTerminalCommand(panel,command) && terminalRows<5)
{
terminalRows=5;
@@ -1609,8 +1695,8 @@ void openPanelFile(const Panel &panel, const fs::path &file, const wchar_t *verb
void enter(Panel &panel)
{
auto row = SendMessageW(panel.list, LB_GETCURSEL, 0, 0);
if (row >= 0 && static_cast<size_t>(row) < panel.rows.size())
const auto row=currentPanelRow(panel);
if(row!=LB_ERR)
{
auto target = panel.rows[row];
if(panel.sizes[row]==L"Folder" || panel.sizes[row]==L"Up") open(panel,target,true);
@@ -3207,7 +3293,7 @@ void deleteSelection(HWND owner,Panel &panel,bool permanent)
void selectPanelEdge(Panel &panel,bool first)
{
SetFocus(panel.list);
activatePanel(static_cast<int>(&panel-panels));
const auto count=SendMessageW(panel.list,LB_GETCOUNT,0,0);
if(count<=0) return;
SendMessageW(panel.list,LB_SETCURSEL,first?0:count-1,0);
@@ -3256,19 +3342,7 @@ LRESULT CALLBACK panelKeys(HWND window, UINT message, WPARAM wp, LPARAM lp, UINT
}
if (message == WM_KEYDOWN && (wp == VK_LEFT || wp == VK_RIGHT))
return scrollList(window, message, wp == VK_LEFT ? VK_HOME : VK_END, lp);
if (message == WM_SETFOCUS)
{
const auto previous=active;
active = static_cast<int>(index);
if(previous!=active)
for(auto &panel:panels)
{
InvalidateRect(panel.path,nullptr,FALSE);
InvalidateRect(panel.list,nullptr,FALSE);
}
showFileDetails(panels[index]);
if(previous!=active) layout(GetParent(window));
}
if(message==WM_SETFOCUS) activatePanel(static_cast<int>(index));
if(message==WM_CHAR && wp>=32 && wp!=127)
{
SetFocus(terminalInput);
@@ -3276,15 +3350,15 @@ LRESULT CALLBACK panelKeys(HWND window, UINT message, WPARAM wp, LPARAM lp, UINT
}
if (message == WM_KEYDOWN)
{
if (wp == VK_TAB) { SetFocus(panels[1 - index].list); return 0; }
if(wp==VK_TAB) { activatePanel(static_cast<int>(1-index)); SetFocus(terminalInput); return 0; }
if (wp == VK_RETURN) { enter(panels[index]); return 0; }
if (wp == VK_F2) { open(panels[index], panels[index].directory); return 0; }
if (wp == VK_F10) { SendMessageW(GetParent(window), WM_CLOSE, 0, 0); return 0; }
if (wp == VK_F3 || wp == VK_F4)
{
const auto &panel = panels[index];
auto row = SendMessageW(window, LB_GETCURSEL, 0, 0);
if (row < 0 || static_cast<size_t>(row) >= panel.rows.size()) return 0;
const auto row=currentPanelRow(panel);
if(row==LB_ERR) return 0;
const bool edit = wp == VK_F4;
openPanelFile(panel, panel.rows[row], edit ? L"edit" : L"open",
edit ? L"No editor is registered for this file." : L"Cannot open the selected file.");
@@ -3302,6 +3376,7 @@ LRESULT CALLBACK panelKeys(HWND window, UINT message, WPARAM wp, LPARAM lp, UINT
}
}
auto result=scrollList(window, message, wp, lp);
if(message==WM_LBUTTONUP && GetFocus()==window) restoreCommandFocus();
if(message==WM_PAINT && panels[index].wide)
{
auto dc=GetDC(window);
@@ -3433,27 +3508,19 @@ bool handleFilesMenuKey(HWND window,UINT message,WPARAM key,LPARAM flags,bool mo
const bool repeated=(flags & (1LL << 30))!=0;
if(message==WM_SYSKEYDOWN && key==VK_MENU)
{
if(!repeated) altAlone=!modified;
return !modified;
}
if((message==WM_SYSKEYUP || message==WM_KEYUP) && key==VK_MENU)
{
if(std::exchange(altAlone,false))
{
toggleFilesMenu();
return true;
}
return false;
}
return !modified;
if(message!=WM_KEYDOWN && message!=WM_SYSKEYDOWN) return false;
altAlone=false;
if(modified) return false;
if(message==WM_KEYDOWN && key==VK_F9)
{
if(!repeated) toggleFilesMenu();
return true;
}
if(message==WM_SYSKEYDOWN && (flags & (1LL << 29)) && key>=L'A' && key<=L'Z')
if(IsWindowVisible(menuBar) && message==WM_SYSKEYDOWN &&
(flags & (1LL << 29)) && key>=L'A' && key<=L'Z')
{
const auto category=menuAccessKeys.find(static_cast<wchar_t>(key));
if(category!=menuAccessKeys.npos)
@@ -3465,6 +3532,93 @@ bool handleFilesMenuKey(HWND window,UINT message,WPARAM key,LPARAM flags,bool mo
return false;
}
void searchPanelByPrefix(HWND window,wchar_t character)
{
if(character==L'\b')
{
if(!panelSearchPrefix.empty()) panelSearchPrefix.pop_back();
}
else panelSearchPrefix+=character;
auto &panel=panels[active];
const auto length=static_cast<int>(panelSearchPrefix.size());
for(size_t row=0;length && row<panel.rows.size();++row)
{
if(panel.sizes[row]==L"Up") continue;
const auto name=panel.rows[row].filename().native();
if(name.size()<panelSearchPrefix.size() ||
CompareStringOrdinal(name.data(),length,panelSearchPrefix.data(),length,TRUE)!=CSTR_EQUAL)
continue;
SendMessageW(panel.list,LB_SETCURSEL,row,0);
InvalidateRect(panel.list,nullptr,FALSE);
showFileDetails(panel);
if(!IsWindowVisible(panelSearchPopup))
{
placePanelSearch(window);
ShowWindow(panelSearchPopup,SW_SHOWNA);
}
break;
}
if(IsWindowVisible(panelSearchPopup)) InvalidateRect(panelSearchPopup,nullptr,FALSE);
else panelSearchPrefix.clear();
}
bool handlePanelSearchKey(HWND window,MSG &message)
{
const bool searching=IsWindowVisible(panelSearchPopup);
if(message.message==WM_LBUTTONDOWN || message.message==WM_RBUTTONDOWN ||
message.message==WM_NCLBUTTONDOWN || message.message==WM_MOUSEWHEEL)
{
if(searching && message.hwnd!=panelSearchPopup) closePanelSearch();
return false;
}
const bool input=message.hwnd==terminalInput || message.hwnd==panels[0].list ||
message.hwnd==panels[1].list;
if(!input || terminalVisible || IsWindowVisible(menuBar) || IsWindowVisible(picker)) return false;
const bool modified=(GetKeyState(VK_CONTROL)&0x8000) || (GetKeyState(VK_LWIN)&0x8000) ||
(GetKeyState(VK_RWIN)&0x8000);
const bool alt=(message.lParam & (1LL<<29))!=0;
if((message.message==WM_SYSCHAR && alt) || (searching && message.message==WM_CHAR))
{
if(!modified && (message.wParam==L'\b' ||
(message.wParam>=32 && message.wParam!=127 && (!alt || message.wParam!=L' '))))
{
searchPanelByPrefix(window,static_cast<wchar_t>(message.wParam));
return true;
}
return false;
}
if(message.message!=WM_KEYDOWN && message.message!=WM_SYSKEYDOWN) return false;
if(!modified && message.wParam!=VK_SPACE && message.message==WM_SYSKEYDOWN && alt &&
(message.wParam==VK_BACK || MapVirtualKeyW(static_cast<UINT>(message.wParam),MAPVK_VK_TO_CHAR)))
{
TranslateMessage(&message);
return true;
}
if(!searching || message.wParam==VK_MENU || message.wParam==VK_SHIFT) return false;
if(!modified && !alt && (message.wParam==VK_BACK ||
MapVirtualKeyW(static_cast<UINT>(message.wParam),MAPVK_VK_TO_CHAR)) &&
message.wParam!=VK_RETURN && message.wParam!=VK_ESCAPE && message.wParam!=VK_TAB)
{
TranslateMessage(&message);
return true;
}
closePanelSearch();
if(message.wParam==VK_RETURN && !modified && !alt)
{
SendMessageW(panels[active].list,message.message,message.wParam,message.lParam);
return true;
}
return message.wParam==VK_ESCAPE && !modified && !alt;
}
void restoreCommandFocus()
{
if(GetFocus()!=terminalInput && !GetCapture() && !copyInProgress && !activeFilesDialog &&
!commandHistoryWindow && !IsWindowVisible(menuBar) && !IsWindowVisible(picker))
SetFocus(terminalInput);
}
LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
{
if(message==filesPreferencesChanged)
@@ -3481,8 +3635,11 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
}
switch (message)
{
case WM_SETFOCUS:
restoreCommandFocus();
return 0;
case WM_ACTIVATEAPP:
if(!wp) altAlone=false;
if(!wp) closePanelSearch();
break;
case WM_COPYDATA:
{
@@ -3538,6 +3695,8 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
SendMessageW(picker,LB_SETITEMHEIGHT,0,textHeight);
status = CreateWindowW(L"STATIC", L"", WS_CHILD | WS_VISIBLE | SS_PATHELLIPSIS,
0, 0, 0, 0, window, reinterpret_cast<HMENU>(144), instance, nullptr);
panelSearchPopup=CreateWindowW(L"STATIC",L"Search",WS_CHILD|WS_CLIPSIBLINGS|SS_OWNERDRAW|SS_NOTIFY,
0,0,0,0,window,reinterpret_cast<HMENU>(148),instance,nullptr);
terminalInput=CreateWindowW(L"EDIT",L"",WS_CHILD|WS_VISIBLE|ES_AUTOHSCROLL,
0,0,0,0,window,nullptr,instance,nullptr);
terminalPrompt=CreateWindowW(L"STATIC",L"",WS_CHILD|WS_VISIBLE|SS_PATHELLIPSIS,
@@ -3553,7 +3712,7 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
SetWindowSubclass(terminalInput,terminalInputKeys,1,0);
SetTimer(window,terminalTimer,50,nullptr);
SetTimer(window,directoryWatchTimer,200,nullptr);
if (!mono || !blue || !teal || !menuBar || !picker || !pickerFrame || !status) return -1;
if (!mono || !blue || !teal || !menuBar || !picker || !pickerFrame || !status || !panelSearchPopup) return -1;
SendMessageW(status, WM_SETFONT, reinterpret_cast<WPARAM>(mono), FALSE);
const wchar_t *labels[]{L"F2 Refresh", L"F3 View", L"F4 Edit", L"F5 Copy", L"F6 Move", L"F10 Exit"};
for (int i = 0; i < static_cast<int>(std::size(commands)); ++i)
@@ -3727,6 +3886,21 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
case WM_DRAWITEM:
{
const auto &item=*reinterpret_cast<const DRAWITEMSTRUCT *>(lp);
if(item.hwndItem==panelSearchPopup)
{
const auto frame=dialogFrameMetrics(GetDpiForWindow(window));
const auto surface=GetSysColorBrush(COLOR_BTNFACE);
const auto border=GetSysColorBrush(COLOR_BTNTEXT);
const auto ink=GetSysColor(COLOR_BTNTEXT);
const auto box=paintDialogFrame(item.hDC,item.rcItem,frame,surface,border);
paintDialogFrameCaptions(item.hDC,item.rcItem,box,L"Search",surface,ink,mono);
auto field=item.rcItem;
InflateRect(&field,-frame.content(),-frame.content());
const auto cells=static_cast<size_t>(std::max(0L,field.right-field.left-10)/std::max(1,cellWidth));
const auto start=panelSearchPrefix.size()>cells?panelSearchPrefix.size()-cells:0;
paintText(item.hDC,field,panelSearchPrefix.c_str()+start,RGB(0,0,0),RGB(0,128,128));
return TRUE;
}
if(item.CtlID==142 || item.CtlID==143)
{
auto bounds=item.rcItem;
@@ -4001,7 +4175,6 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
if (LOWORD(wp) >= 110 && LOWORD(wp) <= 115 && HIWORD(wp) == BN_CLICKED)
{
const WPARAM keys[]{VK_F2, VK_F3, VK_F4, VK_F5, VK_F6, VK_F10};
SetFocus(panels[active].list);
SendMessageW(panels[active].list, WM_KEYDOWN, keys[LOWORD(wp) - 110], 0);
}
if (LOWORD(wp) >= 102 && LOWORD(wp) <= 103 && HIWORD(wp) == LBN_DBLCLK)
@@ -4111,8 +4284,13 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
SetFocus(terminalInput);
const auto com=CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
MSG message{};
while (GetMessageW(&message, nullptr, 0, 0) > 0)
for(;;)
{
if(GetForegroundWindow()==window || IsChild(window,GetFocus())) restoreCommandFocus();
if(GetMessageW(&message,nullptr,0,0)<=0) break;
if(message.hwnd==window && message.message>=WM_KEYFIRST && message.message<=WM_KEYLAST &&
GetFocus()==terminalInput)
message.hwnd=terminalInput;
if(commandHistoryWindow)
{
if(message.message==WM_KEYDOWN && (message.wParam==VK_RETURN || message.wParam==VK_ESCAPE) &&
@@ -4128,6 +4306,7 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
message.message==WM_CONTEXTMENU;
if(ownerClient && clientInput) continue;
}
if(!copyInProgress && handlePanelSearchKey(window,message)) continue;
if(message.message==WM_KEYDOWN && (message.wParam==VK_UP || message.wParam==VK_DOWN) &&
(GetKeyState(VK_CONTROL)&0x8000) && !(GetKeyState(VK_MENU)&0x8000) &&
(message.hwnd==window || IsChild(window,message.hwnd)) && !terminalVisible && !copyInProgress)
@@ -4150,8 +4329,9 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
if(terminalVisible || startTerminal(window)) showTerminal(window,!terminalVisible);
continue;
}
if(message.message==WM_KEYDOWN &&
(message.hwnd==panels[0].list || message.hwnd==panels[1].list) &&
const bool panelFocused=message.hwnd==panels[0].list || message.hwnd==panels[1].list;
const bool mainInput=panelFocused || message.hwnd==terminalInput;
if(message.message==WM_KEYDOWN && mainInput && !terminalVisible &&
(GetKeyState(VK_CONTROL)&0x8000) && !(GetKeyState(VK_MENU)&0x8000) &&
!(GetKeyState(VK_LWIN)&0x8000) && !(GetKeyState(VK_RWIN)&0x8000) &&
message.wParam>='0' && message.wParam<='9' && !copyInProgress &&
@@ -4161,7 +4341,7 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
{
hidePicker();
showMenu(false);
auto &panel=panels[message.hwnd==panels[1].list];
auto &panel=panels[active];
const auto digit=static_cast<int>(message.wParam-'0');
if(GetKeyState(VK_SHIFT)&0x8000) addFolderShortcut(panel,digit);
else showFolderShortcuts(panel,digit);
@@ -4183,15 +4363,24 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
}
continue;
}
const bool panelFocused=message.hwnd==panels[0].list || message.hwnd==panels[1].list;
if(panelFocused && !copyInProgress && message.message==WM_KEYDOWN &&
if(mainInput &&
!copyInProgress && message.message==WM_KEYDOWN &&
!(GetKeyState(VK_MENU)&0x8000) &&
!(GetKeyState(VK_LWIN)&0x8000) && !(GetKeyState(VK_RWIN)&0x8000))
{
const bool ctrl=(GetKeyState(VK_CONTROL)&0x8000)!=0;
const bool shift=(GetKeyState(VK_SHIFT)&0x8000)!=0;
if((message.wParam=='V' && ctrl && !shift) ||
(message.wParam==VK_INSERT && shift && !ctrl))
if(ctrl && !shift && (message.wParam=='F' || message.wParam=='C'))
{
if(!(message.lParam & (1LL<<30)))
{
if(message.wParam=='F') insertCurrentPanelPath();
else SendMessageW(terminalInput,WM_COPY,0,0);
}
continue;
}
if(panelFocused && ((message.wParam=='V' && ctrl && !shift) ||
(message.wParam==VK_INSERT && shift && !ctrl)))
{
SetFocus(terminalInput);
SendMessageW(terminalInput,WM_PASTE,0,0);
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.82";
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;
+19 -5
View File
@@ -82,7 +82,7 @@ function Select-Row($List, [string]$Name, [switch]$DoubleClick) {
[FilesActivationUi]::ClickRow($List, $row, $DoubleClick.IsPresent)
if (!$DoubleClick) {
Wait-Until "selection of $Name" {
[FilesActivationUi]::Focus($window) -eq $List -and
[FilesActivationUi]::Focus($window) -eq $inputBox -and
[FilesActivationUi]::Send($List, 0x188, [IntPtr]::Zero, [IntPtr]::Zero).ToInt32() -eq (Find-Row $List $Name)
}
}
@@ -94,7 +94,7 @@ function Press-Enter($Control) {
function Invoke-Row($List, [string]$Name, [bool]$DoubleClick) {
Select-Row $List $Name -DoubleClick:$DoubleClick
if (!$DoubleClick) { Press-Enter $List }
if (!$DoubleClick) { Press-Enter $inputBox }
}
function Send-Terminal([string]$Command) {
@@ -168,6 +168,20 @@ try {
$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
@@ -212,7 +226,7 @@ try {
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 $left -or
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.' }
@@ -221,14 +235,14 @@ try {
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 $panel.List) { throw 'Modification moved focus.' }
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 $right) { throw 'Deletion moved focus.' }
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) {