Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68edcc0bfb | ||
|
|
b705c4e4a0 | ||
|
|
31d582d29e | ||
|
|
ad2fa2c92e | ||
|
|
796d03f8a9 | ||
|
|
be68b706f0 | ||
|
|
23a7c73331 | ||
|
|
82fe8ce3a1 | ||
|
|
00cd1262b4 | ||
|
|
7bc734a9a8 | ||
|
|
a19ccaf035 |
No files matched your search
+10
-1
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(KSwitcher VERSION 0.6.79 LANGUAGES CXX RC)
|
||||
project(KSwitcher VERSION 0.6.82 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)
|
||||
|
||||
+19
-1
@@ -4,6 +4,12 @@ Far Manager є зразком для оформлення файлового м
|
||||
|
||||
## Скорочення
|
||||
|
||||
- Enter і подвійний клік відкривають папку в поточній панелі, а локальний
|
||||
документ — у програмі, призначеній для його типу у Windows. EXE запускається
|
||||
окремим процесом. BAT і CMD виконуються в поточній сесії вбудованого
|
||||
термінала, а їхній вивід з’являється під панелями. Щоб відкрити файл із
|
||||
панелі LAN, спочатку скопіюй його до локальної папки.
|
||||
|
||||
- У командному рядку Ctrl+V і Shift+Insert вставляють текст із буфера обміну
|
||||
в позицію курсора або замінюють виділення. Якщо фокус на файловій панелі,
|
||||
ці клавіші переводять його до командного рядка й вставляють текст там.
|
||||
@@ -44,7 +50,7 @@ Far Manager є зразком для оформлення файлового м
|
||||
- Короткий Alt перемикає головне меню Files при відпусканні. F9 також перемикає меню. Після закриття фокус повертається до активної панелі. Alt з літерою верхнього рівня спрацьовує одразу при натисканні літери. Alt+O відкриває Options, а відпускання Alt після комбінації не перемикає меню.
|
||||
- Коли головне меню відкрите, L, F, C, O та R відкривають Left, Files, Commands, Options та Right. У підменю підсвічена літера викликає відповідну команду. Літери унікальні в межах поточного рівня. Усталені мнемоніки мають пріоритет, зокрема X для Exit.
|
||||
- Глобальне скорочення **Open KSwitcher Files** можна змінити або очистити в налаштуваннях. Для профілю без цього поля пропонується Ctrl+Alt+F, якщо Windows реєструє комбінацію і вона вільна серед прив’язок KSwitcher.
|
||||
- Глобальне скорочення повертає до відкритого вікна Files із тієї ж збірки або запускає Files, якщо такого вікна немає. Пункт **Files** у треї відкриває нове вікно.
|
||||
- Глобальне скорочення повертає до відкритого вікна 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.
|
||||
@@ -66,6 +72,18 @@ Far Manager є зразком для оформлення файлового м
|
||||
|
||||
## Панелі
|
||||
|
||||
- Обидві панелі автоматично підхоплюють створення, зміну й видалення файлів
|
||||
у відкритих локальних папках. Фокус залишається там, де був, а курсор
|
||||
лишається на поточному елементі, якщо той ще існує.
|
||||
|
||||
- Ctrl+8 відкриває список усіх ярликів папок. Кожен рядок показує групу
|
||||
й назву або шлях. Enter відкриває папку в активній панелі, F4 редагує
|
||||
ярлик, а Delete прибирає його зі списку.
|
||||
|
||||
- У Change drive повідомлення No paired computers з’являється після
|
||||
успішної відповіді з порожнім списком. Збої локального запиту до
|
||||
KSwitcher відображаються окремо.
|
||||
|
||||
- Якщо команда у вбудованому PowerShell змінює поточну папку,
|
||||
панель, з якої надіслано команду, переходить до цієї папки.
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,5 +1,23 @@
|
||||
# Latest Windows release
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1 +1 @@
|
||||
328f04dbf1a2088f89616faaf07f6dd89da34fecda20c915a7e8a21f13cb80db KSwitcher-win-x64.zip
|
||||
84ba82bdf179419c6cc7a612b12c6adb962a47c55aacee311861e9843be1e683 KSwitcher-win-x64.zip
|
||||
+54
-54
@@ -1,131 +1,131 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"version": "0.6.82",
|
||||
"files": [
|
||||
{
|
||||
"size": 2341376,
|
||||
"path": "KSwitcher.exe",
|
||||
"sha256": "0ab0b2a3468d0a5849aba37e14f2e86bf863c9ed85f1c3862367ac9b1e1f8290"
|
||||
"sha256": "8017fa3d840d4e89871b926ceea6c53d99238ba8145a8158930098c3fd7b8485",
|
||||
"path": "KSwitcher.exe"
|
||||
},
|
||||
{
|
||||
"size": 2341888,
|
||||
"path": "KSwitcher.Config.exe",
|
||||
"sha256": "7a6d642e7bba3b6b8c25085c569b19db2ef9abded07beb0d522cef939e2ab853"
|
||||
"sha256": "cbb6624c0382d3a9bd1aa65542f9c2475a171cd11d55372b1c3a3c21fe65d86c",
|
||||
"path": "KSwitcher.Config.exe"
|
||||
},
|
||||
{
|
||||
"size": 3319808,
|
||||
"path": "KSwitcher.Screenshot.exe",
|
||||
"sha256": "8071665518302aeb40c9f668c5bb944f2ac0012c3f57a16d55303619fe20a49f"
|
||||
"sha256": "bc87038c9b090cea36475d3921e6d8a7567d407a447903291f45301fd0e4394d",
|
||||
"path": "KSwitcher.Screenshot.exe"
|
||||
},
|
||||
{
|
||||
"size": 1825280,
|
||||
"path": "KSwitcher.Files.exe",
|
||||
"sha256": "f96261fde331f028d27f3fe7b268f78971f96a05a65d9dd515f76c3d5047f2ef"
|
||||
"size": 1829888,
|
||||
"sha256": "487e51da766bab3c1e2fbcb88d374ea3a03cda6a5736f1972b861cfff68af82f",
|
||||
"path": "KSwitcher.Files.exe"
|
||||
},
|
||||
{
|
||||
"size": 194912,
|
||||
"path": "WebView2Loader.dll",
|
||||
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6"
|
||||
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6",
|
||||
"path": "WebView2Loader.dll"
|
||||
},
|
||||
{
|
||||
"size": 1103,
|
||||
"path": "terminal/addon-fit-LICENSE.txt",
|
||||
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f"
|
||||
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f",
|
||||
"path": "terminal/addon-fit-LICENSE.txt"
|
||||
},
|
||||
{
|
||||
"size": 1521,
|
||||
"path": "terminal/addon-fit.js",
|
||||
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527"
|
||||
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527",
|
||||
"path": "terminal/addon-fit.js"
|
||||
},
|
||||
{
|
||||
"size": 527,
|
||||
"path": "terminal/index.html",
|
||||
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef"
|
||||
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef",
|
||||
"path": "terminal/index.html"
|
||||
},
|
||||
{
|
||||
"size": 3481,
|
||||
"path": "terminal/terminal.js",
|
||||
"sha256": "f677e61d7d90d3023839ee131ddabed7a5c3510b18d47cc9a3c19353913ca5cc"
|
||||
"size": 3978,
|
||||
"sha256": "cc6c9cbb3684dbcda1ff587492b5a2047dd62d79b16a037bbc1d9b261afa3f78",
|
||||
"path": "terminal/terminal.js"
|
||||
},
|
||||
{
|
||||
"size": 1261,
|
||||
"path": "terminal/xterm-LICENSE.txt",
|
||||
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd"
|
||||
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd",
|
||||
"path": "terminal/xterm-LICENSE.txt"
|
||||
},
|
||||
{
|
||||
"size": 7112,
|
||||
"path": "terminal/xterm.css",
|
||||
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6"
|
||||
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6",
|
||||
"path": "terminal/xterm.css"
|
||||
},
|
||||
{
|
||||
"size": 488663,
|
||||
"path": "terminal/xterm.js",
|
||||
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972"
|
||||
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972",
|
||||
"path": "terminal/xterm.js"
|
||||
},
|
||||
{
|
||||
"size": 4401,
|
||||
"path": "LICENSE",
|
||||
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c"
|
||||
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
|
||||
"path": "LICENSE"
|
||||
},
|
||||
{
|
||||
"size": 1075,
|
||||
"path": "json-LICENSE.txt",
|
||||
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86"
|
||||
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86",
|
||||
"path": "json-LICENSE.txt"
|
||||
},
|
||||
{
|
||||
"size": 93885,
|
||||
"path": "README.md",
|
||||
"sha256": "df6d40d0eb66ad7da8db1cffe6bd211de70dfee474cc228e041c821b3940718b"
|
||||
"sha256": "df6d40d0eb66ad7da8db1cffe6bd211de70dfee474cc228e041c821b3940718b",
|
||||
"path": "README.md"
|
||||
},
|
||||
{
|
||||
"size": 11418,
|
||||
"path": "LAN-PREVIEW.md",
|
||||
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada"
|
||||
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada",
|
||||
"path": "LAN-PREVIEW.md"
|
||||
},
|
||||
{
|
||||
"size": 682867,
|
||||
"path": "dictionaries/en.txt",
|
||||
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21"
|
||||
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21",
|
||||
"path": "dictionaries/en.txt"
|
||||
},
|
||||
{
|
||||
"size": 20138,
|
||||
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
|
||||
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5"
|
||||
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5",
|
||||
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt"
|
||||
},
|
||||
{
|
||||
"size": 2265,
|
||||
"path": "dictionaries/README.md",
|
||||
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311"
|
||||
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
|
||||
"path": "dictionaries/README.md"
|
||||
},
|
||||
{
|
||||
"size": 1060364,
|
||||
"path": "dictionaries/ru.txt",
|
||||
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b"
|
||||
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b",
|
||||
"path": "dictionaries/ru.txt"
|
||||
},
|
||||
{
|
||||
"size": 1280,
|
||||
"path": "dictionaries/sources.json",
|
||||
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408"
|
||||
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
|
||||
"path": "dictionaries/sources.json"
|
||||
},
|
||||
{
|
||||
"size": 940799,
|
||||
"path": "dictionaries/uk.txt",
|
||||
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf"
|
||||
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
|
||||
"path": "dictionaries/uk.txt"
|
||||
},
|
||||
{
|
||||
"size": 993,
|
||||
"path": "tools/README.txt",
|
||||
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a"
|
||||
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
|
||||
"path": "tools/README.txt"
|
||||
},
|
||||
{
|
||||
"size": 134656,
|
||||
"path": "tools/StayHere.exe",
|
||||
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376"
|
||||
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376",
|
||||
"path": "tools/StayHere.exe"
|
||||
},
|
||||
{
|
||||
"size": 2796,
|
||||
"path": "tools/StayHere-README.md",
|
||||
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413"
|
||||
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
|
||||
"path": "tools/StayHere-README.md"
|
||||
}
|
||||
],
|
||||
"version": "0.6.79"
|
||||
]
|
||||
}
|
||||
+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,79,0
|
||||
PRODUCTVERSION 0,6,79,0
|
||||
FILEVERSION 0,6,82,0
|
||||
PRODUCTVERSION 0,6,82,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.79"
|
||||
VALUE "FileVersion", "0.6.82"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -24,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;
|
||||
@@ -64,8 +66,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;
|
||||
}
|
||||
@@ -83,7 +86,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;
|
||||
|
||||
+1
-1
@@ -150,7 +150,7 @@ bool activateFilesWindow()
|
||||
{
|
||||
wchar_t name[32]{};
|
||||
if(!IsWindowVisible(candidate) || !GetClassNameW(candidate,name,std::size(name)) ||
|
||||
wcscmp(name,L"KSwitcher.Files") || !configurationPeer(candidate,L"KSwitcher.Files.exe"))
|
||||
wcscmp(name,L"KSwitcher.Files"))
|
||||
return TRUE;
|
||||
*reinterpret_cast<HWND *>(context)=candidate;
|
||||
return FALSE;
|
||||
|
||||
@@ -187,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);
|
||||
@@ -211,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())
|
||||
@@ -228,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)};
|
||||
@@ -304,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;
|
||||
}
|
||||
@@ -331,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())
|
||||
{
|
||||
@@ -340,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;
|
||||
@@ -351,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);
|
||||
@@ -370,7 +397,7 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
}
|
||||
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;
|
||||
@@ -378,7 +405,7 @@ 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);
|
||||
@@ -386,17 +413,17 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
}
|
||||
folderShortcuts=std::move(updated);
|
||||
notifyFilesPreferences(GetAncestor(dialog,GA_ROOTOWNER));
|
||||
if(folderShortcuts[view->digit].empty()) finishFilesDialog(dialog,IDCANCEL);
|
||||
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)
|
||||
{
|
||||
@@ -419,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);
|
||||
|
||||
+176
-31
@@ -245,6 +245,23 @@ struct Panel
|
||||
HWND path{}, list{}, heading{};
|
||||
fs::path directory;
|
||||
std::optional<FilesPeer> peer;
|
||||
struct DirectoryWatch
|
||||
{
|
||||
HANDLE handle=INVALID_HANDLE_VALUE;
|
||||
~DirectoryWatch() { reset(); }
|
||||
void reset()
|
||||
{
|
||||
if(handle!=INVALID_HANDLE_VALUE) FindCloseChangeNotification(handle);
|
||||
handle=INVALID_HANDLE_VALUE;
|
||||
}
|
||||
void start(const fs::path &path)
|
||||
{
|
||||
reset();
|
||||
handle=FindFirstChangeNotificationW(path.c_str(),FALSE,
|
||||
FILE_NOTIFY_CHANGE_FILE_NAME|FILE_NOTIFY_CHANGE_DIR_NAME|
|
||||
FILE_NOTIFY_CHANGE_ATTRIBUTES|FILE_NOTIFY_CHANGE_SIZE|FILE_NOTIFY_CHANGE_LAST_WRITE);
|
||||
}
|
||||
} watch;
|
||||
std::map<fs::path,uint64_t> timestamps;
|
||||
std::vector<fs::path> rows;
|
||||
std::vector<std::wstring> sizes;
|
||||
@@ -258,6 +275,7 @@ Panel panels[2];
|
||||
FilesTerminal terminal;
|
||||
std::ofstream terminalTrace;
|
||||
unsigned terminalTraceLines{};
|
||||
bool traceTerminalOutput{};
|
||||
void traceTerminal(const char *event,int a=0,int b=0,int c=0,int d=0,int e=0)
|
||||
{
|
||||
if(!terminalTrace.is_open() || terminalTraceLines>=4096) return;
|
||||
@@ -269,6 +287,7 @@ COORD terminalSize{80,25};
|
||||
bool terminalViewRequested{};
|
||||
HWND terminalInput{},terminalOutput{},terminalPrompt{};
|
||||
constexpr UINT_PTR terminalTimer=2;
|
||||
constexpr UINT_PTR directoryWatchTimer=4;
|
||||
bool terminalVisible{};
|
||||
std::deque<int> terminalCommandPanels;
|
||||
std::wstring terminalDirectory;
|
||||
@@ -299,6 +318,8 @@ std::vector<fs::path> drivePaths;
|
||||
int driveHover=-1;
|
||||
std::array<LONG,3> driveColumnStarts{};
|
||||
HWND drivePeerService{};
|
||||
bool drivePeerReplyReceived{};
|
||||
std::wstring drivePeerLookupError;
|
||||
std::vector<FilesPeer> drivePeers;
|
||||
struct DriveQuery
|
||||
{
|
||||
@@ -357,7 +378,7 @@ void chooseDrive(int index, bool fromMenu = false);
|
||||
void openMenu(HWND window, int index);
|
||||
void showFileDetails(const Panel &panel);
|
||||
void editOptions(HWND owner);
|
||||
void showFolderShortcuts(Panel &panel,int digit);
|
||||
void showFolderShortcuts(Panel &panel,int digit,bool all=false);
|
||||
void layout(HWND window);
|
||||
|
||||
DWORD preference(const wchar_t *name, DWORD fallback)
|
||||
@@ -812,8 +833,10 @@ bool startTerminal(HWND owner)
|
||||
if(message==L"ready")
|
||||
{
|
||||
terminalView.setFont(displayFont,fontPoints);
|
||||
if(terminalTrace.is_open()) terminalView.enableTrace();
|
||||
if(terminalTrace.is_open()) terminalView.enableTrace(traceTerminalOutput);
|
||||
}
|
||||
else if(traceTerminalOutput && terminalTrace.is_open() && message.starts_with(L"output:"))
|
||||
terminalTrace<<"output "<<ks::utf8(message.substr(7))<<std::endl;
|
||||
else if(terminalTrace.is_open() && message.starts_with(L"trace:"))
|
||||
{
|
||||
int phase{},height{},rows{},viewport{},base{},offset{};
|
||||
@@ -890,6 +913,19 @@ void updateTerminalOutput()
|
||||
const auto bytes=terminal.takeOutput();
|
||||
if(!bytes.empty()) terminalView.send(bytes);
|
||||
}
|
||||
|
||||
bool submitTerminalCommand(const Panel &panel,const std::wstring &value)
|
||||
{
|
||||
if(!startTerminal(GetParent(panel.list))) return false;
|
||||
if(!terminal.write(value+L"\r"))
|
||||
{
|
||||
setStatus(L"Could not send PowerShell input.");
|
||||
return false;
|
||||
}
|
||||
terminalCommandPanels.push_back(static_cast<int>(&panel-panels));
|
||||
return true;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK terminalInputKeys(HWND input,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR)
|
||||
{
|
||||
if(message==WM_CHAR && wp==VK_ESCAPE)
|
||||
@@ -926,12 +962,9 @@ LRESULT CALLBACK terminalInputKeys(HWND input,UINT message,WPARAM wp,LPARAM lp,U
|
||||
SendMessageW(panels[active].list,WM_KEYDOWN,VK_RETURN,lp);
|
||||
return 0;
|
||||
}
|
||||
const auto owner=GetParent(input);
|
||||
if(!startTerminal(owner)) return 0;
|
||||
std::wstring value(GetWindowTextLengthW(input)+1,L'\0');
|
||||
value.resize(GetWindowTextW(input,value.data(),static_cast<int>(value.size())));
|
||||
if(!terminal.write(value+L"\r")) { setStatus(L"Could not send PowerShell input."); return 0; }
|
||||
terminalCommandPanels.push_back(active);
|
||||
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"");
|
||||
@@ -1011,6 +1044,11 @@ void openOn(Panel &panel,const fs::path &directory,bool offerElevation,std::opti
|
||||
{
|
||||
const auto previousDirectory=(panel.peer?panel.peer->id:std::string{})==
|
||||
(peer?peer->id:std::string{})?panel.directory:fs::path{};
|
||||
const bool sameDirectory=previousDirectory==directory;
|
||||
// Start watching before enumeration so changes during a scan trigger another refresh.
|
||||
Panel::DirectoryWatch nextWatch;
|
||||
if(!peer && (!sameDirectory || panel.watch.handle==INVALID_HANDLE_VALUE))
|
||||
nextWatch.start(directory);
|
||||
WIN32_FIND_DATAW data{};
|
||||
const auto search=peer?INVALID_HANDLE_VALUE:FindFirstFileW((directory/L"*").c_str(),&data);
|
||||
auto error=search==INVALID_HANDLE_VALUE ? GetLastError() : ERROR_SUCCESS;
|
||||
@@ -1094,11 +1132,24 @@ void openOn(Panel &panel,const fs::path &directory,bool offerElevation,std::opti
|
||||
std::sort(folders.begin(), folders.end());
|
||||
std::sort(files.begin(), files.end());
|
||||
std::set<fs::path> markedPaths;
|
||||
if(previousDirectory==directory)
|
||||
fs::path previousSelection;
|
||||
LRESULT previousRow=0, previousTop=0;
|
||||
if(sameDirectory)
|
||||
{
|
||||
previousRow=SendMessageW(panel.list,LB_GETCURSEL,0,0);
|
||||
previousTop=SendMessageW(panel.list,LB_GETTOPINDEX,0,0);
|
||||
if(previousRow>=0 && static_cast<size_t>(previousRow)<panel.rows.size())
|
||||
previousSelection=panel.rows[previousRow];
|
||||
for(size_t i=0;i<panel.rows.size();++i)
|
||||
if(panel.marked[i]) markedPaths.insert(panel.rows[i]);
|
||||
}
|
||||
panel.directory = directory;
|
||||
panel.peer=std::move(peer);
|
||||
if(!sameDirectory || nextWatch.handle!=INVALID_HANDLE_VALUE)
|
||||
{
|
||||
panel.watch.reset();
|
||||
std::swap(panel.watch.handle,nextWatch.handle);
|
||||
}
|
||||
panel.timestamps=std::move(timestamps);
|
||||
panel.rows.clear();
|
||||
panel.sizes.clear();
|
||||
@@ -1129,18 +1180,25 @@ void openOn(Panel &panel,const fs::path &directory,bool offerElevation,std::opti
|
||||
for (const auto &file : files) add(file, file.filename().wstring(), false);
|
||||
const auto label=(panel.peer?panel.peer->name+L" · ":std::wstring{})+directory.native();
|
||||
SetWindowTextW(panel.path,label.c_str());
|
||||
if(&panel==&panels[active])
|
||||
if(&panel==&panels[active] && !sameDirectory)
|
||||
layout(GetParent(panel.list));
|
||||
auto selected=panel.rows.begin();
|
||||
auto selected=std::find(panel.rows.begin(),panel.rows.end(),previousSelection);
|
||||
if(selected==panel.rows.end() && !panel.rows.empty())
|
||||
selected=panel.rows.begin()+std::clamp<LRESULT>(previousRow,0,panel.rows.size()-1);
|
||||
if(!previousDirectory.empty() && previousDirectory.parent_path()==directory)
|
||||
{
|
||||
const auto child=std::find(panel.rows.begin(),panel.rows.end(),previousDirectory);
|
||||
if(child!=panel.rows.end()) selected=child;
|
||||
}
|
||||
SendMessageW(panel.list,LB_SETCURSEL,selected-panel.rows.begin(),0);
|
||||
if(selected!=panel.rows.end())
|
||||
{
|
||||
if(sameDirectory) SendMessageW(panel.list,LB_SETTOPINDEX,previousTop,0);
|
||||
SendMessageW(panel.list,LB_SETCURSEL,selected-panel.rows.begin(),0);
|
||||
}
|
||||
InvalidateRect(panel.list,nullptr,FALSE);
|
||||
if(error!=ERROR_SUCCESS && error!=ERROR_NO_MORE_FILES && error!=ERROR_FILE_NOT_FOUND)
|
||||
setStatus(L"Some entries could not be read.");
|
||||
else showFileDetails(panel);
|
||||
else if(&panel==&panels[active]) showFileDetails(panel);
|
||||
}
|
||||
|
||||
void open(Panel &panel,const fs::path &directory,bool offerElevation=false)
|
||||
@@ -1148,6 +1206,19 @@ void open(Panel &panel,const fs::path &directory,bool offerElevation=false)
|
||||
openOn(panel,directory,offerElevation,panel.peer);
|
||||
}
|
||||
|
||||
void refreshChangedPanels()
|
||||
{
|
||||
for(auto &panel:panels)
|
||||
{
|
||||
const auto watch=panel.watch.handle;
|
||||
if(watch==INVALID_HANDLE_VALUE || WaitForSingleObject(watch,0)!=WAIT_OBJECT_0) continue;
|
||||
if(!FindNextChangeNotification(watch)) panel.watch.reset();
|
||||
// Pass a copy because opening the folder rebuilds the panel.
|
||||
const auto directory=panel.directory;
|
||||
open(panel,directory);
|
||||
}
|
||||
}
|
||||
|
||||
void selectDrive()
|
||||
{
|
||||
auto row=SendMessageW(picker,LB_GETCURSEL,0,0);
|
||||
@@ -1235,6 +1306,8 @@ LRESULT CALLBACK pickerKeys(HWND window, UINT msg, WPARAM wp, LPARAM lp, UINT_PT
|
||||
void queryDrivePeers()
|
||||
{
|
||||
drivePeers.clear();
|
||||
drivePeerLookupError.clear();
|
||||
drivePeerReplyReceived=false;
|
||||
drivePeerService=FindWindowW(L"KSwitcher.Main",L"KSwitcher");
|
||||
if(drivePeerService)
|
||||
{
|
||||
@@ -1245,14 +1318,28 @@ void queryDrivePeers()
|
||||
reinterpret_cast<WPARAM>(GetParent(picker)),reinterpret_cast<LPARAM>(&request),
|
||||
SMTO_ABORTIFHUNG|SMTO_ERRORONEXIT,350,&result);
|
||||
const auto error=GetLastError();
|
||||
if(!delivered || result!=1)
|
||||
if(!delivered || result!=1 || !drivePeerReplyReceived)
|
||||
{
|
||||
drivePeers.clear();
|
||||
if(delivered && result!=1)
|
||||
drivePeerLookupError=L"KSwitcher rejected the peer request";
|
||||
else if(delivered)
|
||||
drivePeerLookupError=L"Invalid peer list from KSwitcher";
|
||||
else if(error==ERROR_ACCESS_DENIED)
|
||||
drivePeerLookupError=L"Access denied by Windows";
|
||||
else if(error==ERROR_TIMEOUT)
|
||||
drivePeerLookupError=L"KSwitcher did not respond in time";
|
||||
else
|
||||
drivePeerLookupError=L"Peer request failed (Windows code "+std::to_wstring(error)+L")";
|
||||
ks::log(L"LAN Files peer lookup failed: delivered="+std::to_wstring(delivered!=0)+
|
||||
L", reply="+std::to_wstring(result)+L", Windows error="+std::to_wstring(error));
|
||||
}
|
||||
}
|
||||
else ks::log(L"LAN Files peer lookup failed: local KSwitcher window not found");
|
||||
else
|
||||
{
|
||||
drivePeerLookupError=L"KSwitcher is not running";
|
||||
ks::log(L"LAN Files peer lookup failed: local KSwitcher window not found");
|
||||
}
|
||||
drivePeerService=nullptr;
|
||||
}
|
||||
|
||||
@@ -1324,7 +1411,8 @@ void showRemoteDrives(bool focus=false,int initialRow=0)
|
||||
return SendMessageW(picker,LB_ADDSTRING,0,reinterpret_cast<LPARAM>(label.c_str()));
|
||||
};
|
||||
add(L"KSwitcher LAN");
|
||||
if(driveQueries.empty()) add(L" No paired computers");
|
||||
if(!drivePeerLookupError.empty()) add(L" "+drivePeerLookupError);
|
||||
else if(driveQueries.empty()) add(L" No paired computers");
|
||||
for(const auto &query:driveQueries)
|
||||
{
|
||||
add(query.peer.name);
|
||||
@@ -1478,6 +1566,47 @@ void chooseDrive(int index,bool fromMenu)
|
||||
RedrawWindow(owner,nullptr,nullptr,RDW_INVALIDATE|RDW_ALLCHILDREN|RDW_UPDATENOW);
|
||||
}
|
||||
|
||||
void openPanelFile(const Panel &panel, const fs::path &file, const wchar_t *verb, const wchar_t *failureMessage)
|
||||
{
|
||||
if(panel.peer)
|
||||
{
|
||||
setStatus(L"Copy the remote file to a local panel before opening it.");
|
||||
return;
|
||||
}
|
||||
std::error_code error;
|
||||
if (!fs::is_regular_file(file, error))
|
||||
{
|
||||
if(!verb) setStatus(failureMessage);
|
||||
return;
|
||||
}
|
||||
const auto extension=file.extension().wstring();
|
||||
const auto isExtension=[&](const wchar_t *expected)
|
||||
{
|
||||
return CompareStringOrdinal(extension.c_str(),-1,expected,-1,TRUE)==CSTR_EQUAL;
|
||||
};
|
||||
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"'";
|
||||
if(submitTerminalCommand(panel,command) && terminalRows<5)
|
||||
{
|
||||
terminalRows=5;
|
||||
layout(GetParent(panel.list));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!verb && isExtension(L".exe")) verb=L"open";
|
||||
if (reinterpret_cast<INT_PTR>(ShellExecuteW(GetParent(panel.list), verb, file.c_str(), nullptr,
|
||||
panel.directory.c_str(), SW_SHOWNORMAL)) <= 32)
|
||||
setStatus(failureMessage);
|
||||
}
|
||||
|
||||
void enter(Panel &panel)
|
||||
{
|
||||
auto row = SendMessageW(panel.list, LB_GETCURSEL, 0, 0);
|
||||
@@ -1485,6 +1614,7 @@ void enter(Panel &panel)
|
||||
{
|
||||
auto target = panel.rows[row];
|
||||
if(panel.sizes[row]==L"Folder" || panel.sizes[row]==L"Up") open(panel,target,true);
|
||||
else openPanelFile(panel, target, nullptr, L"Cannot open the selected file.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3153,20 +3283,11 @@ LRESULT CALLBACK panelKeys(HWND window, UINT message, WPARAM wp, LPARAM lp, UINT
|
||||
if (wp == VK_F3 || wp == VK_F4)
|
||||
{
|
||||
const auto &panel = panels[index];
|
||||
if(panel.peer)
|
||||
{
|
||||
setStatus(L"Copy the remote file to a local panel before opening it.");
|
||||
return 0;
|
||||
}
|
||||
auto row = SendMessageW(window, LB_GETCURSEL, 0, 0);
|
||||
if (row < 0 || static_cast<size_t>(row) >= panel.rows.size()) return 0;
|
||||
const auto &file = panel.rows[row];
|
||||
std::error_code error;
|
||||
if (!fs::is_regular_file(file, error)) return 0;
|
||||
auto verb = wp == VK_F4 ? L"edit" : L"open";
|
||||
if (reinterpret_cast<INT_PTR>(ShellExecuteW(GetParent(window), verb, file.c_str(), nullptr,
|
||||
panel.directory.c_str(), SW_SHOWNORMAL)) <= 32)
|
||||
setStatus(wp == VK_F4 ? L"No editor is registered for this file." : L"Cannot open the selected file.");
|
||||
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.");
|
||||
return 0;
|
||||
}
|
||||
if (wp == VK_F5 || wp == VK_F6)
|
||||
@@ -3372,15 +3493,19 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
||||
try
|
||||
{
|
||||
const auto *begin=static_cast<const char*>(reply->lpData);
|
||||
for(const auto &row:Json::parse(begin,begin+reply->cbData))
|
||||
const auto rows=Json::parse(begin,begin+reply->cbData);
|
||||
if(!rows.is_array()) return FALSE;
|
||||
for(const auto &row:rows)
|
||||
drivePeers.push_back({row.at("id"),row.at("address"),ks::wide(row.at("name")),ks::lan::FilesPort,row.value("filesBrowse",false)});
|
||||
}
|
||||
catch(const std::exception &){drivePeers.clear();return FALSE;}
|
||||
drivePeerReplyReceived=true;
|
||||
return TRUE;
|
||||
}
|
||||
case WM_TIMER:
|
||||
if(wp==driveQueryTimer) {pollDriveQueries(window);return 0;}
|
||||
if(wp==terminalTimer) { updateTerminalOutput(); return 0; }
|
||||
if(wp==directoryWatchTimer) { refreshChangedPanels(); return 0; }
|
||||
if(wp!=transferHighlightTimer) break;
|
||||
++transferHighlightPhase;
|
||||
for(auto &panel:panels)
|
||||
@@ -3412,13 +3537,13 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
||||
SetWindowSubclass(picker,pickerKeys,1,0);
|
||||
SendMessageW(picker,LB_SETITEMHEIGHT,0,textHeight);
|
||||
status = CreateWindowW(L"STATIC", L"", WS_CHILD | WS_VISIBLE | SS_PATHELLIPSIS,
|
||||
0, 0, 0, 0, window, nullptr, instance, nullptr);
|
||||
0, 0, 0, 0, window, reinterpret_cast<HMENU>(144), 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,
|
||||
0,0,0,0,window,nullptr,instance,nullptr);
|
||||
terminalOutput=CreateWindowW(L"STATIC",L"",WS_CHILD|WS_CLIPCHILDREN,
|
||||
0,0,0,0,window,nullptr,instance,nullptr);
|
||||
0,0,0,0,window,reinterpret_cast<HMENU>(145),instance,nullptr);
|
||||
terminalBackground=CreateSolidBrush(RGB(24,24,24));
|
||||
if(!terminalInput || !terminalOutput || !terminalPrompt || !terminalBackground) return -1;
|
||||
for(const auto control:{terminalInput,terminalOutput,terminalPrompt})
|
||||
@@ -3427,6 +3552,7 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
||||
SendMessageW(terminalInput,EM_SETMARGINS,EC_LEFTMARGIN|EC_RIGHTMARGIN,0);
|
||||
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;
|
||||
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"};
|
||||
@@ -3921,6 +4047,8 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
KillTimer(window,terminalTimer);
|
||||
KillTimer(window,directoryWatchTimer);
|
||||
for(auto &panel:panels) panel.watch.reset();
|
||||
terminalView.close();
|
||||
terminal.close();
|
||||
PostQuitMessage(0);
|
||||
@@ -3952,8 +4080,10 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
|
||||
return static_cast<int>(result);
|
||||
}
|
||||
for(int i=1;i<count;++i)
|
||||
if(std::wstring_view(args[i])==L"--terminal-trace")
|
||||
if(std::wstring_view(args[i])==L"--terminal-trace" ||
|
||||
std::wstring_view(args[i])==L"--terminal-output-trace")
|
||||
{
|
||||
traceTerminalOutput=std::wstring_view(args[i])==L"--terminal-output-trace";
|
||||
wchar_t temp[MAX_PATH]{};
|
||||
const auto length=GetTempPathW(MAX_PATH,temp);
|
||||
if(length && length<MAX_PATH)
|
||||
@@ -4024,7 +4154,8 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
|
||||
(message.hwnd==panels[0].list || message.hwnd==panels[1].list) &&
|
||||
(GetKeyState(VK_CONTROL)&0x8000) && !(GetKeyState(VK_MENU)&0x8000) &&
|
||||
!(GetKeyState(VK_LWIN)&0x8000) && !(GetKeyState(VK_RWIN)&0x8000) &&
|
||||
message.wParam>='0' && message.wParam<='9' && !copyInProgress)
|
||||
message.wParam>='0' && message.wParam<='9' && !copyInProgress &&
|
||||
(message.wParam!='8' || (GetKeyState(VK_SHIFT)&0x8000)))
|
||||
{
|
||||
if(!(message.lParam & (1LL<<30)))
|
||||
{
|
||||
@@ -4038,6 +4169,20 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
|
||||
continue;
|
||||
}
|
||||
const bool filesMessage=message.hwnd==window || IsChild(window,message.hwnd);
|
||||
if(filesMessage && !terminalVisible && !copyInProgress && !commandHistoryWindow &&
|
||||
message.message==WM_KEYDOWN && message.wParam=='8' &&
|
||||
(GetKeyState(VK_CONTROL)&0x8000) && !(GetKeyState(VK_MENU)&0x8000) &&
|
||||
!(GetKeyState(VK_SHIFT)&0x8000) && !(GetKeyState(VK_LWIN)&0x8000) &&
|
||||
!(GetKeyState(VK_RWIN)&0x8000))
|
||||
{
|
||||
if(!(message.lParam & (1LL<<30)))
|
||||
{
|
||||
hidePicker();
|
||||
showMenu(false);
|
||||
showFolderShortcuts(panels[active],-1,true);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const bool panelFocused=message.hwnd==panels[0].list || message.hwnd==panels[1].list;
|
||||
if(panelFocused && !copyInProgress && message.message==WM_KEYDOWN &&
|
||||
!(GetKeyState(VK_MENU)&0x8000) &&
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.79";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.82";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
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 $List -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 $List }
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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 $left -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 $panel.List) { 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.' }
|
||||
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