Compare commits

..
Author SHA1 Message Date
agrynco be68b706f0 fix(files): use ctrl+8 and activate the open files window 2026-11-14 16:24:47 +02:00
agrynco 23a7c73331 feat(files): list all folder shortcuts with ctrl+f8
Reuse the folder shortcuts dialog and open the selected destination in the active panel.
2026-11-14 16:19:23 +02:00
agrynco 82fe8ce3a1 Merge pull request 'Show specific LAN peer lookup errors in Files' (#22) from codex/files-peer-errors into main 2026-11-14 16:14:28 +02:00
agrynco 00cd1262b4 chore(release): package kswitcher v0.6.80 2026-11-14 16:10:08 +02:00
agrynco 7bc734a9a8 fix(files): distinguish lan peer lookup failures 2026-11-14 16:07:54 +02:00
agrynco a19ccaf035 Merge pull request 'Synchronize Files with PowerShell and fix the LAN drive picker' (#21) from codex/files-polish-release into main 2026-11-14 16:01:54 +02:00
agrynco 60625103f8 chore(release): package kswitcher v0.6.79 2026-11-14 15:53:27 +02:00
agrynco aa02f2cacb fix(files): synchronize terminal navigation and polish lan picker 2026-11-14 15:52:14 +02:00
agrynco 6a1e39efa9 Merge pull request 'Synchronize KSwitcher Files settings across open windows' (#20) from codex/files-live-settings into main 2026-11-14 15:18:15 +02:00
agrynco 7242c3f775 test(settings): keep picker fixture within the monitor 2026-11-14 15:17:54 +02:00
agrynco a43f6441b4 chore(release): refresh verified windows package 2026-11-14 15:15:20 +02:00
agrynco 2f5ae373a1 fix(settings): preserve unset files shortcut in options 2026-11-14 15:14:16 +02:00
agrynco b978361180 chore(release): package kswitcher v0.6.78 with files 2026-11-14 15:11:26 +02:00
agrynco 93b8055085 fix(files): synchronize settings across windows
Reload shared preferences in open windows and apply only fields changed in Options.
2026-11-14 15:09:17 +02:00
agrynco 70ee338fc3 Merge pull request 'Add searchable command history to KSwitcher Files' (#19) from codex/files-command-history into main 2026-11-14 14:43:10 +02:00
19 changed files with 499 additions and 144 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.77 LANGUAGES CXX RC)
project(KSwitcher VERSION 0.6.80 LANGUAGES CXX RC)
if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif()
+23
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,28 @@ Far Manager є зразком для оформлення файлового м
## Панелі
- Ctrl+8 відкриває список усіх ярликів папок. Кожен рядок показує групу
й назву або шлях. Enter відкриває папку в активній панелі, F4 редагує
ярлик, а Delete прибирає його зі списку.
- У Change drive повідомлення No paired computers з’являється після
успішної відповіді з порожнім списком. Збої локального запиту до
KSwitcher відображаються окремо.
- Якщо команда у вбудованому PowerShell змінює поточну папку,
панель, з якої надіслано команду, переходить до цієї папки.
- Коли список LAN-дисків змінює розмір меню Change drive, рамка й список
змінюють позицію разом. Рамка повністю перемальовується після зміни
розміру, а список має однаковий внутрішній відступ зліва і справа.
- Тайм-аут запиту дисків відображається як Connection timed out. Якщо
ввімкнено Write log, Files записує помилки запитів пар і дисків в окремий
журнал `data/kswitcher-files.log`. Системні помилки містять код Windows.
- Застосовані налаштування Files одразу підхоплюють інші відкриті вікна.
Apply зберігає поля, змінені в поточній чернетці. Якщо два вікна одночасно
редагували набір стилів файлів, відкрий Options заново, щоб побачити свіжі правила.
- Типовий колір тексту для ZIP-архівів — світлий коралово-рожевий.
- Меню Change drive показує літеру, тип, загальний обсяг і вільне місце. Відкриття меню запускає нове LAN-виявлення. Секція KSwitcher LAN показує збережені пари, зокрема офлайн. Після кожного відкриття меню асинхронно запитує диски й показує їх під назвою відповідного ПК. Поки триває запит, видно стан завантаження. Якщо запит не вдався, видно помилку. Повторне відкриття запускає новий запит. Кожна панель має власний диск і шлях, а в заголовку віддаленої панелі перед шляхом показано назву ПК.
Binary file not shown.
+30
View File
@@ -1,5 +1,35 @@
# Latest Windows release
## v0.6.80 — 2026-11-14
Includes the Files updates from v0.6.79. Change drive now explains why the local peer lookup failed and shows No paired computers only when the response is empty.
**Українською.** Випуск включає зміни Files із v0.6.79. Change drive показує конкретну причину збою запиту пар. No paired computers з’являється лише після успішної порожньої відповіді.
## v0.6.79 — 2026-11-14
In KSwitcher Files, changing directory in PowerShell updates the panel
that sent the command. The global Files shortcut brings an open window to
the foreground. Change drive redraws its frame when LAN drives arrive and
shows a short timeout message. Files writes LAN lookup errors to its own log.
**Українською.** Зміна каталогу в PowerShell оновлює панель, з якої
надіслано команду. Глобальна клавіша Files повертає до відкритого вікна.
Change drive перемальовує рамку після появи LAN-дисків і показує коротке
повідомлення про тайм-аут. Files записує помилки LAN у власний журнал.
## v0.6.78 — 2026-11-14
The Windows package includes KSwitcher Files, a two-panel file manager with
local and paired LAN drives, folder shortcuts, an embedded PowerShell terminal,
and searchable command history. Applied Files settings update other open
instances immediately.
**Українською.** До пакета для Windows входить KSwitcher Files, двопанельний
файловий менеджер із локальними дисками й дисками спарених ПК, ярликами папок,
вбудованим PowerShell та пошуком в історії команд. Застосовані налаштування
Files одразу підхоплюють інші відкриті вікна.
## v0.6.77 — 2026-11-13
The screenshot editor remembers the local pencil smoothing default across
+1 -1
View File
@@ -1 +1 @@
51d95639527590b36453a5ef485532a7d2135950222fa197eb431a5309f1a45c KSwitcher-win-x64.zip
f6067958677c4045fc5243459fc37522489a7f081969dac42befd07101814e61 KSwitcher-win-x64.zip
+80 -35
View File
@@ -1,86 +1,131 @@
{
"version": "0.6.77",
"schema": 1,
"version": "0.6.80",
"files": [
{
"sha256": "654544aa5a7c944f607edf5324cff68d1f447b9219fb9b3933e90b0eb8a3ba13",
"path": "KSwitcher.exe",
"size": 2249216,
"sha256": "f1c79d337d746efcda03946cc833b7b5588ef4e739f1dff7d3afa15d0465469f"
"size": 2341376
},
{
"sha256": "a546c2dfd811ee8a3cf4301b5b21d1d6a72dd7280c3eac272eebd7963bc48b32",
"path": "KSwitcher.Config.exe",
"size": 2251776,
"sha256": "016edc389007e0d15ee365fff7115fc91a820b46080cb39a3fb362f3b463bae7"
"size": 2341888
},
{
"sha256": "a5cc8809b12698b266857440eece154a26bba64570bc65be1cb77959f7b0ebef",
"path": "KSwitcher.Screenshot.exe",
"size": 3247616,
"sha256": "c8ef070ad01acc410802c62e0df6a96cb299fe036fe8e99f0cf6a398b7324ec6"
"size": 3319808
},
{
"sha256": "d827cf29385db23dbdc5729d4397d768c32dd5ed7581ef10cb9f8c81370ec8a0",
"path": "KSwitcher.Files.exe",
"size": 1826304
},
{
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6",
"path": "WebView2Loader.dll",
"size": 194912
},
{
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f",
"path": "terminal/addon-fit-LICENSE.txt",
"size": 1103
},
{
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527",
"path": "terminal/addon-fit.js",
"size": 1521
},
{
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef",
"path": "terminal/index.html",
"size": 527
},
{
"sha256": "f677e61d7d90d3023839ee131ddabed7a5c3510b18d47cc9a3c19353913ca5cc",
"path": "terminal/terminal.js",
"size": 3481
},
{
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd",
"path": "terminal/xterm-LICENSE.txt",
"size": 1261
},
{
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6",
"path": "terminal/xterm.css",
"size": 7112
},
{
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972",
"path": "terminal/xterm.js",
"size": 488663
},
{
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
"path": "LICENSE",
"size": 4401,
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c"
"size": 4401
},
{
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86",
"path": "json-LICENSE.txt",
"size": 1075,
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86"
"size": 1075
},
{
"sha256": "df6d40d0eb66ad7da8db1cffe6bd211de70dfee474cc228e041c821b3940718b",
"path": "README.md",
"size": 93885,
"sha256": "df6d40d0eb66ad7da8db1cffe6bd211de70dfee474cc228e041c821b3940718b"
"size": 93885
},
{
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada",
"path": "LAN-PREVIEW.md",
"size": 11418,
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada"
"size": 11418
},
{
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21",
"path": "dictionaries/en.txt",
"size": 682867,
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21"
"size": 682867
},
{
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5",
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
"size": 20138,
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5"
"size": 20138
},
{
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
"path": "dictionaries/README.md",
"size": 2230,
"sha256": "2df17fe4e17d527c67ab57269b07dce59a49c1fb128c429a1408decbb47109ec"
"size": 2265
},
{
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b",
"path": "dictionaries/ru.txt",
"size": 1060364,
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b"
"size": 1060364
},
{
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
"path": "dictionaries/sources.json",
"size": 1279,
"sha256": "d6e11a3aaba22a8a7eaba19f778d8a5bd66653ff306810e85a1b0e7fff3a6b86"
"size": 1280
},
{
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
"path": "dictionaries/uk.txt",
"size": 898248,
"sha256": "caeb097749d4afc84b68ad96b3d7dfb6a6569561cd32ccce0bee94a13b43ba86"
"size": 940799
},
{
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
"path": "tools/README.txt",
"size": 993,
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a"
"size": 993
},
{
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376",
"path": "tools/StayHere.exe",
"size": 134656,
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376"
"size": 134656
},
{
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
"path": "tools/StayHere-README.md",
"size": 2796,
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413"
"size": 2796
}
],
"schema": 1
]
}
+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,77,0
PRODUCTVERSION 0,6,77,0
FILEVERSION 0,6,80,0
PRODUCTVERSION 0,6,80,0
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
BEGIN
@@ -20,7 +20,7 @@ BEGIN
BEGIN
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
VALUE "ProductName", "KSwitcher"
VALUE "FileVersion", "0.6.77"
VALUE "FileVersion", "0.6.80"
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;
+6 -2
View File
@@ -29,11 +29,15 @@ void setOptionHistoryKeys(HWND dialog,const std::array<WORD,2> &keys)
SendDlgItemMessageW(dialog,323,HKM_SETHOTKEY,keys[1],0);
}
void loadCommandHistory()
void loadCommandHistoryKeys()
{
const std::array<WORD,2> keys{static_cast<WORD>(preference(L"PreviousCommandKey",defaultHistoryKeys[0])),
static_cast<WORD>(preference(L"CommandHistoryKey",defaultHistoryKeys[1]))};
if(validHistoryKeys(keys)) commandHistoryKeys=keys;
commandHistoryKeys=validHistoryKeys(keys)?keys:defaultHistoryKeys;
}
void loadCommandHistory()
{
DWORD bytes{};
if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,L"CommandHistory",RRF_RT_REG_BINARY,nullptr,nullptr,&bytes)
+43 -12
View File
@@ -139,6 +139,7 @@ INT_PTR CALLBACK folderShortcutEditDialog(HWND dialog,UINT message,WPARAM wp,LPA
return TRUE;
}
folderShortcuts=std::move(updated);
notifyFilesPreferences(GetAncestor(dialog,GA_ROOTOWNER));
finishFilesDialog(dialog,IDOK);
return TRUE;
}
@@ -186,13 +187,21 @@ struct FolderShortcutView
std::vector<int> digits;
int rowHeight{};
Panel *panel{};
bool all{};
std::vector<std::pair<int,size_t>> entries;
};
std::wstring shortcutLabel(const FolderShortcut &entry)
{
return entry.title.empty()?(entry.peerId.empty()?L"":entry.peerName+L" · ")+entry.path.native():entry.title;
}
void layoutQuickFolders(HWND dialog,FolderShortcutView &view,int selection)
{
const auto list=GetDlgItem(dialog,shortcutList);
SendMessageW(list,LB_RESETCONTENT,0,0);
view.digits.clear();
view.entries.clear();
const auto owner=GetWindow(dialog,GW_OWNER);
const auto dc=GetDC(dialog);
const auto font=SelectObject(dc,mono);
@@ -210,14 +219,23 @@ void layoutQuickFolders(HWND dialog,FolderShortcutView &view,int selection)
GetTextExtentPoint32W(dc,label.data(),static_cast<int>(label.size()),&size);
return static_cast<int>(size.cx);
};
int width=std::max(measure(L"Folder shortcuts"),measure(view.digit<0?L"Enter Esc":quickFolderHints))+3*cellWidth;
int width=std::max(measure(L"Folder shortcuts"),measure(view.all?L"Del F4":view.digit<0?L"Enter Esc":quickFolderHints))+3*cellWidth;
const int textInset=dialogRowTextInset(cellWidth);
const auto add=[&](const std::wstring &label)
{
width=std::max(width,measure(label)+2*textInset);
SendMessageW(list,LB_ADDSTRING,0,reinterpret_cast<LPARAM>(label.c_str()));
};
if(view.digit<0)
if(view.all)
{
for(int digit=0;digit<static_cast<int>(folderShortcuts.size());++digit)
for(size_t index=0;index<folderShortcuts[digit].size();++index)
{
view.entries.emplace_back(digit,index);
add(std::to_wstring(digit)+L" "+shortcutLabel(folderShortcuts[digit][index]));
}
}
else if(view.digit<0)
{
for(int digit=0;digit<10;++digit)
if(!folderShortcuts[digit].empty())
@@ -227,7 +245,7 @@ void layoutQuickFolders(HWND dialog,FolderShortcutView &view,int selection)
}
}
else for(const auto &entry:folderShortcuts[view.digit])
add(entry.title.empty()?(entry.peerId.empty()?L"":entry.peerName+L" · ")+entry.path.native():entry.title);
add(shortcutLabel(entry));
SelectObject(dc,font);
ReleaseDC(dialog,dc);
MONITORINFO monitor{sizeof(monitor)};
@@ -303,7 +321,7 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
const auto bounds=paintDialogFrame(dc,client,dialogFrameMetrics(GetDpiForWindow(dialog)),teal,frame);
DeleteObject(frame);
paintDialogFrameCaptions(dc,client,bounds,L"Folder shortcuts",teal,colors[Text],mono,
view->digit<0?L"Enter Esc":quickFolderHints);
view->all?L"Del F4":view->digit<0?L"Enter Esc":quickFolderHints);
EndPaint(dialog,&ps);
return TRUE;
}
@@ -330,7 +348,7 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
if(action!=quickRemove && action!=quickEdit && action!=quickInsert && action!=quickMoveUp &&
action!=quickMoveDown && action!=IDOK && !(action==shortcutList && HIWORD(wp)==LBN_DBLCLK)) return FALSE;
const auto row=SendDlgItemMessageW(dialog,shortcutList,LB_GETCURSEL,0,0);
if(view->digit<0)
if(!view->all && view->digit<0)
{
if((action==IDOK || action==shortcutList) && row>=0 && static_cast<size_t>(row)<view->digits.size())
{
@@ -339,7 +357,17 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
}
return TRUE;
}
const auto &group=folderShortcuts[view->digit];
int digit=view->digit;
size_t index=static_cast<size_t>(row);
if(view->all)
{
if(index>=view->entries.size() || action==quickInsert ||
action==quickMoveUp || action==quickMoveDown) return TRUE;
const auto &entry=view->entries[index];
digit=entry.first;
index=entry.second;
}
const auto &group=folderShortcuts[digit];
if(action==quickInsert)
{
const auto &panel=*view->panel;
@@ -350,7 +378,7 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
SetFocus(GetDlgItem(dialog,shortcutList));
return TRUE;
}
if(row<0 || static_cast<size_t>(row)>=group.size()) return TRUE;
if(index>=group.size()) return TRUE;
if(action==quickMoveUp || action==quickMoveDown)
{
const auto target=row+(action==quickMoveUp?-1:1);
@@ -362,13 +390,14 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
else
{
folderShortcuts=std::move(updated);
notifyFilesPreferences(GetAncestor(dialog,GA_ROOTOWNER));
layoutQuickFolders(dialog,*view,static_cast<int>(target));
}
return TRUE;
}
if(action==quickEdit)
{
FolderShortcutEdit edit{group[row],view->digit,static_cast<size_t>(row)};
FolderShortcutEdit edit{group[index],digit,index};
if(editFolderShortcut(dialog,edit)) layoutQuickFolders(dialog,*view,static_cast<int>(row));
SetFocus(GetDlgItem(dialog,shortcutList));
return TRUE;
@@ -376,24 +405,25 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
if(action==quickRemove)
{
auto updated=folderShortcuts;
updated[view->digit].erase(updated[view->digit].begin()+row);
updated[digit].erase(updated[digit].begin()+index);
if(!saveFolderShortcuts(updated))
{
MessageBoxW(dialog,L"Could not save folder shortcuts.",L"Folder shortcuts",MB_OK|MB_ICONWARNING);
return TRUE;
}
folderShortcuts=std::move(updated);
if(folderShortcuts[view->digit].empty()) finishFilesDialog(dialog,IDCANCEL);
notifyFilesPreferences(GetAncestor(dialog,GA_ROOTOWNER));
if(view->all?view->entries.size()==1:folderShortcuts[digit].empty()) finishFilesDialog(dialog,IDCANCEL);
else layoutQuickFolders(dialog,*view,static_cast<int>(row));
return TRUE;
}
view->chosen=group[row];
view->chosen=group[index];
finishFilesDialog(dialog,IDOK);
return TRUE;
}
void showFolderShortcuts(Panel &panel,int digit)
void showFolderShortcuts(Panel &panel,int digit,bool all)
{
if(digit>=0)
{
@@ -416,6 +446,7 @@ void showFolderShortcuts(Panel &panel,int digit)
}
FolderShortcutView view{digit};
view.panel=&panel;
view.all=all;
const auto result=runFilesDialog(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(109),GetParent(panel.list),
quickFolderDialog,reinterpret_cast<LPARAM>(&view));
SetFocus(panel.list);
+159 -38
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)
@@ -364,6 +369,13 @@ DWORD preference(const wchar_t *name, DWORD fallback)
== ERROR_SUCCESS ? value : fallback;
}
const UINT filesPreferencesChanged=RegisterWindowMessageW(L"KSwitcher.Files.PreferencesChanged");
void notifyFilesPreferences(HWND source)
{
SendNotifyMessageW(HWND_BROADCAST,filesPreferencesChanged,reinterpret_cast<WPARAM>(source),0);
}
void savePreference(const wchar_t *name, DWORD value)
{
RegSetKeyValueW(HKEY_CURRENT_USER,schemeKey,name,REG_DWORD,&value,sizeof(value));
@@ -393,6 +405,7 @@ void setMode(int index, bool wide)
{
panels[index].wide=wide;
savePreference(index ? L"RightWide" : L"LeftWide",wide);
notifyFilesPreferences(GetParent(panels[index].list));
layout(GetParent(panels[index].list));
InvalidateRect(panels[index].list,nullptr,TRUE);
showFileDetails(panels[active]);
@@ -789,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;
@@ -811,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);
@@ -847,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;
}
@@ -899,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"");
@@ -1202,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;
}
@@ -1250,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);
}
@@ -1273,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);
@@ -1339,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;
@@ -3144,6 +3219,35 @@ LRESULT CALLBACK panelKeys(HWND window, UINT message, WPARAM wp, LPARAM lp, UINT
#include "files_folder_shortcuts.inl"
#include "files_command_history_dialog.inl"
bool loadFilesPreferences(HWND window)
{
LOGFONTW font{};
DWORD bytes=sizeof(font);
if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,L"DisplayFont",RRF_RT_REG_BINARY,nullptr,&font,&bytes)
!=ERROR_SUCCESS || bytes!=sizeof(font))
{
font=defaultFilesFont();
}
font.lfFaceName[LF_FACESIZE-1]=0;
if(!applyFont(window,font,static_cast<int>(std::clamp(preference(L"FontPoints",defaultFontPoints),80UL,240UL)))) return false;
loadColors();
dialogFramePadding={
std::min(preference(dialogFrameOuterKey,defaultDialogFramePadding.outer),maxDialogFramePadding),
std::min(preference(dialogFrameInnerKey,defaultDialogFramePadding.inner),maxDialogFramePadding),
std::min(preference(dialogRowTextKey,defaultDialogFramePadding.rowText),maxDialogRowTextPadding)};
loadFileStyles();
loadFolderShortcuts();
panels[0].wide=preference(L"LeftWide",defaultWide)!=0;
panels[1].wide=preference(L"RightWide",defaultWide)!=0;
sizeWidth=static_cast<int>(std::clamp(preference(L"SizeWidth",defaultSizeWidth),6UL,16UL));
alignExtensions=preference(L"AlignExtensions",defaultAlignExtensions)!=0;
showDetails=preference(L"ShowDetails",defaultShowDetails)!=0;
alignFolders=preference(L"AlignFolders",defaultAlignFolders)!=0;
statusFields=preference(L"StatusFields",defaultStatusFields)&15;
loadCommandHistoryKeys();
return true;
}
#include "files_options.inl"
void openMenu(HWND window, int index)
@@ -3261,6 +3365,18 @@ bool handleFilesMenuKey(HWND window,UINT message,WPARAM key,LPARAM flags,bool mo
LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
{
if(message==filesPreferencesChanged)
{
if(reinterpret_cast<HWND>(wp)!=window)
{
loadFilesPreferences(window);
layout(window);
layoutOpenCommandHistory();
showFileDetails(panels[active]);
RedrawWindow(window,nullptr,nullptr,RDW_INVALIDATE|RDW_ALLCHILDREN);
}
return 0;
}
switch (message)
{
case WM_ACTIVATEAPP:
@@ -3275,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:
@@ -3297,29 +3416,7 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
case WM_CREATE:
{
auto instance = GetModuleHandleW(nullptr);
LOGFONTW font{};
DWORD bytes=sizeof(font);
if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,L"DisplayFont",RRF_RT_REG_BINARY,nullptr,&font,&bytes)
!=ERROR_SUCCESS || bytes!=sizeof(font))
{
font=defaultFilesFont();
}
font.lfFaceName[LF_FACESIZE-1]=0;
if(!applyFont(window,font,static_cast<int>(std::clamp(preference(L"FontPoints",defaultFontPoints),80UL,240UL)))) return -1;
loadColors();
dialogFramePadding={
std::min(preference(dialogFrameOuterKey,defaultDialogFramePadding.outer),maxDialogFramePadding),
std::min(preference(dialogFrameInnerKey,defaultDialogFramePadding.inner),maxDialogFramePadding),
std::min(preference(dialogRowTextKey,defaultDialogFramePadding.rowText),maxDialogRowTextPadding)};
loadFileStyles();
loadFolderShortcuts();
panels[0].wide=preference(L"LeftWide",defaultWide)!=0;
panels[1].wide=preference(L"RightWide",defaultWide)!=0;
sizeWidth=static_cast<int>(std::clamp(preference(L"SizeWidth",defaultSizeWidth),6UL,16UL));
alignExtensions=preference(L"AlignExtensions",defaultAlignExtensions)!=0;
showDetails=preference(L"ShowDetails",defaultShowDetails)!=0;
alignFolders=preference(L"AlignFolders",defaultAlignFolders)!=0;
statusFields=preference(L"StatusFields",defaultStatusFields)&15;
if(!loadFilesPreferences(window)) return -1;
menuBar=CreateWindowW(L"BUTTON",L"",WS_CHILD|WS_CLIPSIBLINGS|BS_OWNERDRAW,
0,0,0,0,window,reinterpret_cast<HMENU>(120),instance,nullptr);
const wchar_t *menus[]{L"Left",L"Files",L"Commands",L"Options",L"Right"};
@@ -3778,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;
}
@@ -3858,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))
{
@@ -3940,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)))
{
@@ -3954,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) &&
+100 -45
View File
@@ -52,8 +52,22 @@ void resetPanelProperty(HWND dialog,const PanelProperty &property)
else CheckDlgButton(dialog,property.control,property.defaultValue);
}
struct OptionsBaseline
{
LOGFONTW font=displayFont;
int points=fontPoints;
DialogFramePadding padding=dialogFramePadding;
std::array<COLORREF,ColorCount> palette=colors;
std::vector<FileStyle> styles=fileStyles;
std::array<WORD,2> keys=commandHistoryKeys;
DWORD width=static_cast<DWORD>(sizeWidth), fields=statusFields;
bool extensions=alignExtensions, folders=alignFolders, details=showDetails;
std::array<bool,2> wide{panels[0].wide,panels[1].wide};
};
struct OptionsDraft
{
OptionsBaseline baseline;
LOGFONTW font=displayFont;
int points=fontPoints;
DialogFramePadding framePadding=dialogFramePadding;
@@ -81,15 +95,28 @@ struct OptionsDraft
bool optionsFontChanged(const OptionsDraft &draft)
{
auto pending=draft.font, applied=displayFont;
auto pending=draft.font, applied=draft.baseline.font;
// Height follows the selected point size and DPI. applyFont always uses natural width.
pending.lfHeight=applied.lfHeight=0;
pending.lfWidth=applied.lfWidth=0;
return draft.points!=fontPoints ||
return draft.points!=draft.baseline.points ||
memcmp(&pending,&applied,offsetof(LOGFONTW,lfFaceName))!=0 ||
_wcsicmp(pending.lfFaceName,applied.lfFaceName)!=0;
}
struct OptionsWriteLock
{
HANDLE mutex=CreateMutexW(nullptr,FALSE,L"Local\\KSwitcher.Files.OptionsWrite");
DWORD result=mutex?WaitForSingleObject(mutex,2000):WAIT_FAILED;
bool acquired() const { return result==WAIT_OBJECT_0 || result==WAIT_ABANDONED; }
~OptionsWriteLock()
{
if(acquired()) ReleaseMutex(mutex);
if(mutex) CloseHandle(mutex);
}
};
void updateOptionsApply(HWND dialog,const OptionsDraft &draft)
{
BOOL valid{};
@@ -97,12 +124,13 @@ void updateOptionsApply(HWND dialog,const OptionsDraft &draft)
const auto checked=[&](int id){return IsDlgButtonChecked(dialog,id)==BST_CHECKED;};
DWORD fields=0;
for(int i=0;i<4;++i) if(checked(205+i)) fields|=1u<<i;
const bool dirty=!valid || width!=static_cast<UINT>(sizeWidth) ||
checked(273)!=panels[0].wide || checked(274)!=panels[1].wide ||
checked(202)!=alignExtensions || checked(204)!=alignFolders || checked(203)!=showDetails ||
fields!=statusFields || draft.palette!=colors || draft.styles!=fileStyles ||
draft.framePadding!=dialogFramePadding || optionsFontChanged(draft) ||
optionHistoryKeys(dialog)!=commandHistoryKeys;
const auto &base=draft.baseline;
const bool dirty=!valid || width!=base.width ||
checked(273)!=base.wide[0] || checked(274)!=base.wide[1] ||
checked(202)!=base.extensions || checked(204)!=base.folders || checked(203)!=base.details ||
fields!=base.fields || draft.palette!=base.palette || draft.styles!=base.styles ||
draft.framePadding!=base.padding || optionsFontChanged(draft) ||
optionHistoryKeys(dialog)!=base.keys;
EnableWindow(GetDlgItem(dialog,optionsApply),dirty);
InvalidateRect(GetDlgItem(dialog,fileStylePreview),nullptr,FALSE);
}
@@ -521,14 +549,31 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
SendDlgItemMessageW(dialog,201,EM_SETSEL,0,-1);
return TRUE;
}
const auto keys=optionHistoryKeys(dialog);
const auto owner=GetWindow(dialog,GW_OWNER);
const OptionsWriteLock writeLock;
if(!writeLock.acquired())
{
setStatus(L"Another window is saving Files settings. Try Apply again.");
return TRUE;
}
const auto &base=draft->baseline;
auto keys=optionHistoryKeys(dialog);
loadCommandHistoryKeys();
for(size_t i=0;i<keys.size();++i)
if(keys[i]==base.keys[i]) keys[i]=commandHistoryKeys[i];
if(!validHistoryKeys(keys))
{
MessageBoxW(dialog,L"Choose distinct shortcuts using Ctrl or Alt that are free in Files.",L"Command history",MB_OK|MB_ICONWARNING);
return TRUE;
}
const auto owner=GetWindow(dialog,GW_OWNER);
const bool stylesChanged=draft->styles!=fileStyles;
const bool stylesChanged=draft->styles!=base.styles;
loadFileStyles();
if(stylesChanged && fileStyles!=base.styles && fileStyles!=draft->styles)
{
MessageBoxW(dialog,L"File type styles changed in another window. Reopen Options before editing them again.",
L"KSwitcher Files Options",MB_OK|MB_ICONWARNING);
return TRUE;
}
if(stylesChanged && !saveFileStyles(draft->styles))
{
MessageBoxW(dialog,L"Could not save the file type styles.",L"KSwitcher Files Options",MB_OK|MB_ICONWARNING);
@@ -541,47 +586,57 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
MessageBoxW(dialog,L"Could not apply the selected font.",optionsTitles[Appearance],MB_OK|MB_ICONWARNING);
return TRUE;
}
commandHistoryKeys=keys;
savePreference(L"PreviousCommandKey",keys[0]);
savePreference(L"CommandHistoryKey",keys[1]);
if(stylesChanged) fileStyles=draft->styles;
sizeWidth=static_cast<int>(width);
alignExtensions=IsDlgButtonChecked(dialog,202)==BST_CHECKED;
showDetails=IsDlgButtonChecked(dialog,203)==BST_CHECKED;
alignFolders=IsDlgButtonChecked(dialog,204)==BST_CHECKED;
statusFields=0;
for(int i=0;i<4;++i)
if(IsDlgButtonChecked(dialog,205+i)==BST_CHECKED) statusFields|=1u<<i;
savePreference(L"SizeWidth",sizeWidth);
savePreference(L"AlignExtensions",alignExtensions);
savePreference(L"ShowDetails",showDetails);
savePreference(L"AlignFolders",alignFolders);
savePreference(L"StatusFields",statusFields);
for(int i=0;i<ColorCount;++i)
if(colors[i]!=draft->palette[i])
{
colors[i]=draft->palette[i];
savePreference(std::to_wstring(i).c_str(),colors[i]);
}
refreshColors();
if(draft->framePadding!=dialogFramePadding)
const auto saveChanged=[](const wchar_t *name,DWORD value,DWORD initial)
{
dialogFramePadding=draft->framePadding;
savePreference(dialogFrameOuterKey,dialogFramePadding.outer);
savePreference(dialogFrameInnerKey,dialogFramePadding.inner);
savePreference(dialogRowTextKey,dialogFramePadding.rowText);
if(value!=initial) savePreference(name,value);
};
const auto checked=[&](int id){return IsDlgButtonChecked(dialog,id)==BST_CHECKED;};
const auto inputKeys=optionHistoryKeys(dialog);
saveChanged(L"PreviousCommandKey",inputKeys[0],base.keys[0]);
saveChanged(L"CommandHistoryKey",inputKeys[1],base.keys[1]);
saveChanged(L"SizeWidth",width,base.width);
saveChanged(L"AlignExtensions",checked(202),base.extensions);
saveChanged(L"AlignFolders",checked(204),base.folders);
saveChanged(L"ShowDetails",checked(203),base.details);
DWORD fields=preference(L"StatusFields",defaultStatusFields)&15;
const auto initialFields=fields;
for(int i=0;i<4;++i)
{
const DWORD bit=1u<<i;
if(checked(205+i)!=((base.fields&bit)!=0)) fields=checked(205+i)?fields|bit:fields&~bit;
}
saveChanged(L"StatusFields",fields,initialFields);
for(int i=0;i<ColorCount;++i)
saveChanged(std::to_wstring(i).c_str(),draft->palette[i],base.palette[i]);
saveChanged(dialogFrameOuterKey,draft->framePadding.outer,base.padding.outer);
saveChanged(dialogFrameInnerKey,draft->framePadding.inner,base.padding.inner);
saveChanged(dialogRowTextKey,draft->framePadding.rowText,base.padding.rowText);
if(fontChanged)
{
savePreference(L"FontPoints",fontPoints);
RegSetKeyValueW(HKEY_CURRENT_USER,schemeKey,L"DisplayFont",REG_BINARY,&displayFont,sizeof(displayFont));
draft->font=displayFont;
}
for(int i=0;i<2;++i)
{
const bool wide=IsDlgButtonChecked(dialog,273+i)==BST_CHECKED;
if(panels[i].wide!=wide) setMode(i,wide);
}
saveChanged(L"LeftWide",checked(273),base.wide[0]);
saveChanged(L"RightWide",checked(274),base.wide[1]);
loadFilesPreferences(owner);
notifyFilesPreferences(owner);
draft->baseline=OptionsBaseline{};
draft->font=displayFont;
draft->points=fontPoints;
draft->styles=fileStyles;
draft->palette=colors;
draft->framePadding=dialogFramePadding;
SetDlgItemInt(dialog,201,sizeWidth,FALSE);
CheckDlgButton(dialog,202,alignExtensions);
CheckDlgButton(dialog,203,showDetails);
CheckDlgButton(dialog,204,alignFolders);
CheckDlgButton(dialog,273,panels[0].wide);
CheckDlgButton(dialog,274,panels[1].wide);
for(int i=0;i<4;++i) CheckDlgButton(dialog,205+i,(statusFields>>i)&1u);
setOptionHistoryKeys(dialog,commandHistoryKeys);
if(draft->page==PanelColors && draft->section==fileStyleSection)
updateFileStyleEditor(dialog,*draft,true);
showOptionsPage(dialog,*draft,draft->page,draft->section);
layout(owner);
showFileDetails(panels[active]);
RedrawWindow(owner,nullptr,nullptr,RDW_INVALIDATE|RDW_ERASE|RDW_ALLCHILDREN|RDW_UPDATENOW);
+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);
+3 -1
View File
@@ -1514,7 +1514,9 @@ Settings collectSettings(HWND w, SettingsView *v)
s.undo = value(w, 213);
s.clipboard = value(w, 214);
s.screenshot = value(w, 215);
s.filesHotkey = value(w, 216);
auto filesHotkey = value(w, 216);
if (s.filesHotkey || !filesHotkey.empty())
s.filesHotkey = std::move(filesHotkey);
s.maxItems = integer(w, 240);
s.maxMiB = integer(w, 241);
s.maxImageMiB = integer(w, 242);
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.77";
inline constexpr wchar_t AppVersion[] = L"0.6.80";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
+7 -2
View File
@@ -292,11 +292,16 @@ int main()
HWND picker = GetDlgItem(dialog, 728);
RECT dialogBounds{};
GetWindowRect(dialog, &dialogBounds);
MONITORINFO monitor{sizeof(monitor)};
require(GetMonitorInfoW(MonitorFromWindow(dialog, MONITOR_DEFAULTTONEAREST), &monitor),
"monitor work area available");
const auto targetX = std::clamp(dialogBounds.left + 32, monitor.rcWork.left, monitor.rcWork.right - 120);
const auto targetY = std::clamp(dialogBounds.top + 100, monitor.rcWork.top, monitor.rcWork.bottom - 48);
HWND target = CreateWindowExW(WS_EX_TOPMOST | WS_EX_NOACTIVATE, L"EDIT", L"Picker fixture",
WS_POPUP | WS_VISIBLE, dialogBounds.left + 32, dialogBounds.top + 100, 120, 48,
WS_POPUP | WS_VISIBLE, targetX, targetY, 120, 48,
nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
require(picker && target, "window picker and target available");
POINT drop{dialogBounds.left + 33, dialogBounds.top + 101};
POINT drop{targetX + 1, targetY + 1};
require(GetAncestor(WindowFromPoint(drop), GA_ROOT) == target, "fixture window is under the drop point");
ScreenToClient(picker, &drop);
SendMessageW(picker, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(8, 8));