Compare commits

...
15 changed files with 297 additions and 93 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.78 LANGUAGES CXX RC)
project(KSwitcher VERSION 0.6.81 LANGUAGES CXX RC)
if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif()
+19
View File
@@ -44,6 +44,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 у поточній сесії 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.
@@ -65,6 +66,24 @@ Far Manager є зразком для оформлення файлового м
## Панелі
- Ctrl+8 відкриває список усіх ярликів папок. Кожен рядок показує групу
й назву або шлях. Enter відкриває папку в активній панелі, F4 редагує
ярлик, а Delete прибирає його зі списку.
- У Change drive повідомлення No paired computers з’являється після
успішної відповіді з порожнім списком. Збої локального запиту до
KSwitcher відображаються окремо.
- Якщо команда у вбудованому PowerShell змінює поточну папку,
панель, з якої надіслано команду, переходить до цієї папки.
- Коли список LAN-дисків змінює розмір меню Change drive, рамка й список
змінюють позицію разом. Рамка повністю перемальовується після зміни
розміру, а список має однаковий внутрішній відступ зліва і справа.
- Тайм-аут запиту дисків відображається як Connection timed out. Якщо
ввімкнено Write log, Files записує помилки запитів пар і дисків в окремий
журнал `data/kswitcher-files.log`. Системні помилки містять код Windows.
- Застосовані налаштування Files одразу підхоплюють інші відкриті вікна.
Apply зберігає поля, змінені в поточній чернетці. Якщо два вікна одночасно
редагували набір стилів файлів, відкрий Options заново, щоб побачити свіжі правила.
Binary file not shown.
+24
View File
@@ -1,5 +1,29 @@
# Latest Windows release
## v0.6.81 — 2026-11-14
Ctrl+8 lists all folder shortcuts. Press Enter to open a folder, F4 to edit the shortcut, or Delete to remove it. Ctrl+Alt+F brings an open Files window forward.
**Українською.** Ctrl+8 показує всі ярлики папок. Enter відкриває папку, F4 редагує ярлик, Delete видаляє його. Ctrl+Alt+F повертає до відкритого вікна Files.
## v0.6.80 — 2026-11-14
Includes the Files updates from v0.6.79. Change drive now explains why the local peer lookup failed and shows No paired computers only when the response is empty.
**Українською.** Випуск включає зміни Files із v0.6.79. Change drive показує конкретну причину збою запиту пар. No paired computers з’являється лише після успішної порожньої відповіді.
## v0.6.79 — 2026-11-14
In KSwitcher Files, changing directory in PowerShell updates the panel
that sent the command. The global Files shortcut brings an open window to
the foreground. Change drive redraws its frame when LAN drives arrive and
shows a short timeout message. Files writes LAN lookup errors to its own log.
**Українською.** Зміна каталогу в PowerShell оновлює панель, з якої
надіслано команду. Глобальна клавіша Files повертає до відкритого вікна.
Change drive перемальовує рамку після появи LAN-дисків і показує коротке
повідомлення про тайм-аут. Files записує помилки LAN у власний журнал.
## v0.6.78 — 2026-11-14
The Windows package includes KSwitcher Files, a two-panel file manager with
+1 -1
View File
@@ -1 +1 @@
d7362104941853bb30646fa44175686ed3d2fd3abcf1fd83daee04dd1d5c6912 KSwitcher-win-x64.zip
d471919e04961b9fb323ef83e8b5e6ed41ce4667e881b5133574d6f4915c53e7 KSwitcher-win-x64.zip
+57 -57
View File
@@ -1,131 +1,131 @@
{
"schema": 1,
"files": [
{
"path": "KSwitcher.exe",
"sha256": "1e67e8e15eada80c902764f14a12a0bd370080709a94d454cc3c565dd3de1136",
"size": 2340864
"size": 2341376,
"sha256": "ac5858551e4c487fe75954ea0e01d12ca021ada8e785b901f425dea9c5dedea1",
"path": "KSwitcher.exe"
},
{
"path": "KSwitcher.Config.exe",
"sha256": "26e9f5db4b6f79408e30fb2650f1a8318b5c17e39ae2bccf614901b815ca6606",
"size": 2341376
"size": 2341888,
"sha256": "bb20f7dc5feb3e5657e94f18088aac26d818cb8e35f4e1248f67dedabf7139dd",
"path": "KSwitcher.Config.exe"
},
{
"path": "KSwitcher.Screenshot.exe",
"sha256": "d9593c6ff103070f1f72b93c0f350ba4782d9df814090565f48bd0aa34125059",
"size": 3319296
"size": 3319808,
"sha256": "3b71f51aee26e0a146678b7278a81f6a9dd827f9b8e650c33ede997eb813b97f",
"path": "KSwitcher.Screenshot.exe"
},
{
"path": "KSwitcher.Files.exe",
"sha256": "22f897cfdc3207350e3edb63f33747d71a0f9d60c7a8d3771f642ee589ed6ce1",
"size": 1815552
"size": 1827328,
"sha256": "f4c5d3359b601b753906d6c4ac3b1fef66ae95f341a30bb66ba5229cc3ea5645",
"path": "KSwitcher.Files.exe"
},
{
"path": "WebView2Loader.dll",
"size": 194912,
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6",
"size": 194912
"path": "WebView2Loader.dll"
},
{
"path": "terminal/addon-fit-LICENSE.txt",
"size": 1103,
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f",
"size": 1103
"path": "terminal/addon-fit-LICENSE.txt"
},
{
"path": "terminal/addon-fit.js",
"size": 1521,
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527",
"size": 1521
"path": "terminal/addon-fit.js"
},
{
"path": "terminal/index.html",
"size": 527,
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef",
"size": 527
"path": "terminal/index.html"
},
{
"path": "terminal/terminal.js",
"sha256": "142624d166314c4539dd14f922c17dca3d2bc7203f84aff2df03a196602c4b09",
"size": 3174
"size": 3481,
"sha256": "f677e61d7d90d3023839ee131ddabed7a5c3510b18d47cc9a3c19353913ca5cc",
"path": "terminal/terminal.js"
},
{
"path": "terminal/xterm-LICENSE.txt",
"size": 1261,
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd",
"size": 1261
"path": "terminal/xterm-LICENSE.txt"
},
{
"path": "terminal/xterm.css",
"size": 7112,
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6",
"size": 7112
"path": "terminal/xterm.css"
},
{
"path": "terminal/xterm.js",
"size": 488663,
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972",
"size": 488663
"path": "terminal/xterm.js"
},
{
"path": "LICENSE",
"size": 4401,
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
"size": 4401
"path": "LICENSE"
},
{
"path": "json-LICENSE.txt",
"size": 1075,
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86",
"size": 1075
"path": "json-LICENSE.txt"
},
{
"path": "README.md",
"size": 93885,
"sha256": "df6d40d0eb66ad7da8db1cffe6bd211de70dfee474cc228e041c821b3940718b",
"size": 93885
"path": "README.md"
},
{
"path": "LAN-PREVIEW.md",
"size": 11418,
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada",
"size": 11418
"path": "LAN-PREVIEW.md"
},
{
"path": "dictionaries/en.txt",
"size": 682867,
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21",
"size": 682867
"path": "dictionaries/en.txt"
},
{
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
"size": 20138,
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5",
"size": 20138
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt"
},
{
"path": "dictionaries/README.md",
"size": 2265,
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
"size": 2265
"path": "dictionaries/README.md"
},
{
"path": "dictionaries/ru.txt",
"size": 1060364,
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b",
"size": 1060364
"path": "dictionaries/ru.txt"
},
{
"path": "dictionaries/sources.json",
"size": 1280,
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
"size": 1280
"path": "dictionaries/sources.json"
},
{
"path": "dictionaries/uk.txt",
"size": 940799,
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
"size": 940799
"path": "dictionaries/uk.txt"
},
{
"path": "tools/README.txt",
"size": 993,
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
"size": 993
"path": "tools/README.txt"
},
{
"path": "tools/StayHere.exe",
"size": 134656,
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376",
"size": 134656
"path": "tools/StayHere.exe"
},
{
"path": "tools/StayHere-README.md",
"size": 2796,
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
"size": 2796
"path": "tools/StayHere-README.md"
}
],
"version": "0.6.78"
"schema": 1,
"version": "0.6.81"
}
+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,78,0
PRODUCTVERSION 0,6,78,0
FILEVERSION 0,6,81,0
PRODUCTVERSION 0,6,81,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.78"
VALUE "FileVersion", "0.6.81"
END
END
BLOCK "VarFileInfo"
+10
View File
@@ -7,6 +7,16 @@ const terminal = new Terminal({
theme: { background: '#181818', foreground: '#e8e8e8' },
});
const fit = new FitAddon.FitAddon();
terminal.parser.registerOscHandler(633, data => {
const prefix = 'ks-directory:';
if (!data.startsWith(prefix)) return false;
try {
bridge.postMessage('directory:' + decodeURIComponent(data.slice(prefix.length)));
} catch {
bridge.postMessage('directory:');
}
return true;
});
terminal.loadAddon(fit);
const host = document.getElementById('terminal');
terminal.open(host);
+23 -1
View File
@@ -143,6 +143,28 @@ ULONG_PTR snippetReplayMarker()
}
constexpr int OpenFiles = 25, FilesHotkeyFirst = 0x4B10, FilesHotkeySecond = 0x4B11;
bool activateFilesWindow()
{
HWND files{};
EnumWindows([](HWND candidate, LPARAM context) -> BOOL
{
wchar_t name[32]{};
if(!IsWindowVisible(candidate) || !GetClassNameW(candidate,name,std::size(name)) ||
wcscmp(name,L"KSwitcher.Files"))
return TRUE;
*reinterpret_cast<HWND *>(context)=candidate;
return FALSE;
},reinterpret_cast<LPARAM>(&files));
if(!files) return false;
if(IsIconic(files)) ShowWindowAsync(files,SW_RESTORE);
if(!SetForegroundWindow(files))
{
FLASHWINFO flash{sizeof(flash),files,FLASHW_TRAY,3,0};
FlashWindowEx(&flash);
}
return true;
}
std::vector<std::pair<int, std::wstring>> shortcuts(const Settings &s)
{
std::vector<std::pair<int, std::wstring>> out{{Toggle, s.toggle},
@@ -3037,7 +3059,7 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
case WM_HOTKEY:
if (app->filesHotkeyId && wp == static_cast<WPARAM>(app->filesHotkeyId))
{
app->dispatch(OpenFiles);
if(!activateFilesWindow()) app->dispatch(OpenFiles);
}
else if (wp != FilesHotkeyFirst && wp != FilesHotkeySecond) app->dispatch((int)wp);
return 0;
+40 -12
View File
@@ -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);
+109 -15
View File
@@ -6,6 +6,7 @@
#include <commdlg.h>
#include <algorithm>
#include <array>
#include <deque>
#include <filesystem>
#include <fstream>
#include <future>
@@ -269,6 +270,8 @@ bool terminalViewRequested{};
HWND terminalInput{},terminalOutput{},terminalPrompt{};
constexpr UINT_PTR terminalTimer=2;
bool terminalVisible{};
std::deque<int> terminalCommandPanels;
std::wstring terminalDirectory;
int terminalRows{};
struct CommandEntry { std::wstring date, command; };
std::vector<CommandEntry> terminalHistory;
@@ -296,6 +299,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
{
@@ -354,7 +359,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)
@@ -797,6 +802,8 @@ void showTerminal(HWND owner,bool show)
SetFocus(terminalInput);
}
void open(Panel &panel,const fs::path &directory,bool offerElevation);
bool startTerminal(HWND owner)
{
if(terminal.alive()) return true;
@@ -819,6 +826,20 @@ bool startTerminal(HWND owner)
traceTerminal(events[phase],height,rows,viewport,base,offset);
}
}
else if(message.starts_with(L"directory:"))
{
const auto directory=message.substr(10);
if(!terminalCommandPanels.empty())
{
const int panel=terminalCommandPanels.front();
terminalCommandPanels.pop_front();
std::error_code error;
if(panel>=0 && !panels[panel].peer && directory!=terminalDirectory &&
!directory.empty() && fs::is_directory(directory,error))
open(panels[panel],directory,false);
}
terminalDirectory=directory;
}
else if(message==L"focus-input")
{
if(GetForegroundWindow()==owner) SetFocus(terminalInput);
@@ -855,7 +876,12 @@ bool startTerminal(HWND owner)
setStatus(L"Select a local panel to start the terminal.");
return false;
}
if(terminal.start(panels[active].directory.c_str(),terminalSize)) return true;
if(terminal.start(panels[active].directory.c_str(),terminalSize))
{
terminalCommandPanels={-1};
terminalDirectory.clear();
return true;
}
setStatus(L"Could not start PowerShell.");
return false;
}
@@ -907,6 +933,7 @@ LRESULT CALLBACK terminalInputKeys(HWND input,UINT message,WPARAM wp,LPARAM lp,U
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(value.find_first_not_of(L" \t\r\n")!=std::wstring::npos) rememberCommand(value);
terminalHistoryIndex=terminalHistory.size();
SetWindowTextW(input,L"");
@@ -1210,14 +1237,39 @@ 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)
{
COPYDATASTRUCT request{FilesLanPeers,0,nullptr};
DWORD_PTR result{};
SendMessageTimeoutW(drivePeerService,WM_COPYDATA,reinterpret_cast<WPARAM>(GetParent(picker)),
reinterpret_cast<LPARAM>(&request),SMTO_ABORTIFHUNG|SMTO_ERRORONEXIT,350,&result);
if(result!=1) drivePeers.clear();
SetLastError(ERROR_SUCCESS);
const auto delivered=SendMessageTimeoutW(drivePeerService,WM_COPYDATA,
reinterpret_cast<WPARAM>(GetParent(picker)),reinterpret_cast<LPARAM>(&request),
SMTO_ABORTIFHUNG|SMTO_ERRORONEXIT,350,&result);
const auto error=GetLastError();
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
{
drivePeerLookupError=L"KSwitcher is not running";
ks::log(L"LAN Files peer lookup failed: local KSwitcher window not found");
}
drivePeerService=nullptr;
}
@@ -1258,8 +1310,17 @@ void layoutDrivePicker(int selected,bool focus)
static_cast<int>(SendMessageW(picker,LB_GETCOUNT,0,0)))*textHeight;
const int x=panel.left+(panel.right-panel.left-width)/2;
const int y=panel.top+(panel.bottom-panel.top-height)/2;
SetWindowPos(pickerFrame,HWND_TOP,x-inset,y-vertical,width+2*inset,height+2*vertical,SWP_SHOWWINDOW|SWP_NOACTIVATE);
SetWindowPos(picker,HWND_TOP,x,y,width,height,SWP_SHOWWINDOW|SWP_NOACTIVATE);
constexpr UINT flags=SWP_SHOWWINDOW|SWP_NOACTIVATE|SWP_NOCOPYBITS;
auto layout=BeginDeferWindowPos(2);
if(layout) layout=DeferWindowPos(layout,pickerFrame,HWND_TOP,
x-inset,y-vertical,width+2*inset,height+2*vertical,flags);
if(layout) layout=DeferWindowPos(layout,picker,HWND_TOP,x,y,width,height,flags);
if(!layout || !EndDeferWindowPos(layout))
{
SetWindowPos(pickerFrame,HWND_TOP,x-inset,y-vertical,width+2*inset,height+2*vertical,flags);
SetWindowPos(picker,HWND_TOP,x,y,width,height,flags);
}
RedrawWindow(pickerFrame,nullptr,nullptr,RDW_INVALIDATE|RDW_UPDATENOW);
SendMessageW(picker,LB_SETCURSEL,selected,0);
if(focus) SetFocus(picker);
}
@@ -1281,7 +1342,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);
@@ -1347,10 +1409,15 @@ void pollDriveQueries(HWND owner)
try {query.drives=query.pending.get();}
catch(const std::system_error &e)
{
query.error=query.peer.filesBrowse?fileError(e):L"Check build / TCP 45885";
ks::log(L"LAN Files drive query failed: Windows error "+std::to_wstring(e.code().value()));
if(e.code()==std::error_code(WSAETIMEDOUT,std::system_category()))
query.error=L"Connection timed out";
else
query.error=query.peer.filesBrowse?fileError(e):L"Check build / TCP 45885";
}
catch(const std::exception &)
{
ks::log(L"LAN Files drive query failed: unexpected error");
query.error=query.peer.filesBrowse?L"Cannot reach TCP 45885":L"Check build / TCP 45885";
}
changed=true;
@@ -3324,10 +3391,13 @@ 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:
@@ -3805,15 +3875,14 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
FillRect(item.hDC,&line,static_cast<HBRUSH>(GetStockObject(DC_BRUSH)));
}
}
if (item.CtlType==ODT_LISTBOX)
if (item.CtlType==ODT_LISTBOX && item.hwndItem!=picker)
{
RECT track{};
GetWindowRect(item.hwndItem,&track);
auto owner=item.hwndItem==picker?pickerFrame:window;
MapWindowPoints(nullptr,owner,reinterpret_cast<POINT*>(&track),2);
MapWindowPoints(nullptr,window,reinterpret_cast<POINT*>(&track),2);
track.left=track.right;
track.right+=5;
InvalidateRect(owner,&track,FALSE);
InvalidateRect(window,&track,FALSE);
}
return TRUE;
}
@@ -3885,6 +3954,16 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
{
bool writeLog=true;
try
{
const auto settings=ks::dataDirectory()/L"settings.json";
if(std::filesystem::exists(settings))
writeLog=Json::parse(ks::readFile(settings,1024*1024)).value("writeLog",true);
}
catch(const std::exception &) {}
ks::setLogFilename(L"kswitcher-files.log");
ks::setLoggingEnabled(writeLog);
int count{};
if(auto args=CommandLineToArgvW(GetCommandLineW(),&count))
{
@@ -3967,7 +4046,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)))
{
@@ -3981,6 +4061,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) &&
+1 -1
View File
@@ -49,7 +49,7 @@ public:
startup.StartupInfo.cb=sizeof(startup);
startup.lpAttributeList=attributes;
PROCESS_INFORMATION info{};
wchar_t command[]=L"powershell.exe -NoLogo -NoProfile -NoExit -Command \"Remove-Module PSReadLine -ErrorAction SilentlyContinue; function global:prompt { ([string][char]27) + '[?25l' }; Clear-Host\"";
wchar_t command[]=L"powershell.exe -NoLogo -NoProfile -NoExit -Command \"Remove-Module PSReadLine -ErrorAction SilentlyContinue; function global:prompt { $location = Get-Location; $path = if ($location.Provider.Name -eq 'FileSystem') { [Uri]::EscapeDataString($location.ProviderPath) }; ([string][char]27) + ']633;ks-directory:' + $path + [char]7 + [char]27 + '[?25l' }; Clear-Host\"";
wchar_t system[MAX_PATH]{};
const auto length=GetSystemDirectoryW(system,MAX_PATH);
const auto shell=std::wstring(system)+L"\\WindowsPowerShell\\v1.0\\powershell.exe";
+7 -1
View File
@@ -737,6 +737,7 @@ namespace
{
std::recursive_mutex logGate;
bool loggingEnabled = false;
std::wstring logFilename = L"kswitcher.log";
DiagnosticBuffer diagnosticBuffer;
}
size_t DiagnosticBuffer::expire(ULONGLONG now)
@@ -832,6 +833,11 @@ void diagnosticOperation(const std::string &json)
record["context"] = utf8(diagnosticBuffer.record(L"Operation complete", true, GetTickCount64()));
log(L"Operation " + wide(record.dump(-1, ' ', true, nlohmann::json::error_handler_t::replace)));
}
void setLogFilename(std::wstring filename)
{
std::lock_guard lock(logGate);
logFilename = std::move(filename);
}
void log(const std::wstring &message)
{
std::lock_guard lock(logGate);
@@ -839,7 +845,7 @@ void log(const std::wstring &message)
return;
try
{
auto path = dataDirectory() / L"kswitcher.log";
auto path = dataDirectory() / logFilename;
std::error_code ec;
auto size = std::filesystem::file_size(path, ec);
if (!ec && size > 256 * 1024)
+1
View File
@@ -94,6 +94,7 @@ class DiagnosticBuffer
void clear() { entries.clear(); }
};
void setLoggingEnabled(bool enabled);
void setLogFilename(std::wstring filename);
void expireDiagnostics(ULONGLONG now);
void diagnosticEvent(const std::wstring &message, bool problem = false);
void diagnosticOperation(const std::string &json);
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.78";
inline constexpr wchar_t AppVersion[] = L"0.6.81";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;