Compare commits

...
Author SHA1 Message Date
agrynco 3aafc1f937 Merge pull request 'Release KSwitcher v0.6.92' (#36)
Release KSwitcher v0.6.92
2026-11-16 01:19:49 +02:00
agrynco 6215675cb4 Release KSwitcher v0.6.92 2026-11-16 01:19:24 +02:00
agrynco 56c6257465 Merge pull request 'Release KSwitcher v0.6.91' (#35)
Release KSwitcher v0.6.91
2026-11-16 01:09:12 +02:00
10 changed files with 216 additions and 43 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.91 LANGUAGES CXX RC)
project(KSwitcher VERSION 0.6.92 LANGUAGES CXX RC)
if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif()
+5
View File
@@ -132,6 +132,11 @@ Warning і початковий фокус на No.
відкриваються в окремих немодальних вікнах із власними кнопками на панелі
завдань Windows. При повторному відкритті Files пропонує перейти до вже
відкритого вікна. Закриття менеджера залишає вікна файлів відкритими.
- Коли курсор на папці, F3 у фоні підраховує сумарний розмір файлів у ній
та вкладених папках. Якщо позначено папки, розмір рахується для кожної з
них окремо. Результат видно в колонці Size у широкому режимі та в рядку
стану, коли там увімкнено розмір. Це працює і в LAN-панелі. Посилання й
junction не обходяться. Недоступний вміст позначається як частковий результат.
- F3 і F4 працюють і на панелі LAN. Файл завантажується у тимчасову копію
на диску у фоновому режимі. Збереження передає зміни назад на віддалений ПК.
Якщо розмір або час зміни віддаленого файла відрізняється від відкритої
Binary file not shown.
+6
View File
@@ -1,5 +1,11 @@
# Latest Windows release
## v0.6.92 — 2026-11-16
In Files, F3 on a folder measures file sizes throughout the directory tree. If folders are marked, Files measures each marked folder separately. Results appear in the Size column in Wide mode. F3 on a file opens the viewer even when folders are marked. Folder sizing runs in the background and supports LAN panes.
**Українською.** У Files F3 на папці підраховує сумарний розмір файлів у всьому дереві. Якщо позначено папки, Files рахує розмір кожної окремо. Результат з’являється в колонці Size у широкому режимі. F3 на файлі відкриває переглядач, навіть коли позначено папки. Підрахунок виконується у фоні й підтримує LAN-панелі.
## v0.6.91 — 2026-11-16
Ctrl+Insert copies the marked file and folder names from the active pane, one per line. Without marks, it copies the name under the cursor. LAN Files retries opening TCP 45885 if the port is busy at startup, and Change drive retries failed drive queries. A remote update request waits for an in-progress update check. The updater identifies and closes companion processes from its installation, including Files running from an update backup, and records confirmed exits in the journal.
+1 -1
View File
@@ -1 +1 @@
7db5db4c5d1f5c199d1cee7887336bdb2e34d4036b5af8cc20709957d6729d32 KSwitcher-win-x64.zip
448c242568f80cb07788173b1182f3f5b20cf152290f5de85efa148456406cff KSwitcher-win-x64.zip
+32 -32
View File
@@ -1,135 +1,135 @@
{
"version": "0.6.91",
"version": "0.6.92",
"schema": 1,
"files": [
{
"size": 2467840,
"path": "KSwitcher.exe",
"sha256": "eb4f1f7be3027a8ba073443932b0ea930a5d77f274f9603854f1331bd0954765"
"size": 2467840,
"sha256": "daaf3c8f0b1090f89416bcc6771985c006e80a40c7027172d20bfbefaba1dde5"
},
{
"size": 2446848,
"path": "KSwitcher.Config.exe",
"sha256": "00a4817543f98643f679c4a92d197730bf9a62a114b918ab8540c179958b7004"
"size": 2446848,
"sha256": "eeb98f92a1a6ef53fa86f3b914a0445e03b757832bfde52853740dc7caebd951"
},
{
"size": 3053568,
"path": "KSwitcher.Screenshot.exe",
"sha256": "68b430e7c3c7f84c21ec843849a5f763ee3563c523cfd0507da6a4bef3db3810"
"size": 3053568,
"sha256": "ed378e0b6868489f05287e34c8d354c5bd7dec887552309aa918beaacd8f3f0a"
},
{
"size": 3676672,
"path": "KSwitcher.Files.exe",
"sha256": "e9257342788c8212a0d89eb62ed200115d88272225841181fc30feeeba8064c4"
"size": 3686912,
"sha256": "78068ae7cf5c645508f8e5bbf9e5d9c7f75501445e5ee5e1b745e6abed387052"
},
{
"size": 194912,
"path": "WebView2Loader.dll",
"size": 194912,
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6"
},
{
"size": 1103,
"path": "terminal/addon-fit-LICENSE.txt",
"size": 1103,
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f"
},
{
"size": 1521,
"path": "terminal/addon-fit.js",
"size": 1521,
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527"
},
{
"size": 527,
"path": "terminal/index.html",
"size": 527,
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef"
},
{
"size": 4375,
"path": "terminal/terminal.js",
"size": 4375,
"sha256": "7a31fe8679b7942775c7a9a3e34e836744111b7337e750d6ec7b21188dc4cd61"
},
{
"size": 1261,
"path": "terminal/xterm-LICENSE.txt",
"size": 1261,
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd"
},
{
"size": 7112,
"path": "terminal/xterm.css",
"size": 7112,
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6"
},
{
"size": 488663,
"path": "terminal/xterm.js",
"size": 488663,
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972"
},
{
"size": 4401,
"path": "LICENSE",
"size": 4401,
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c"
},
{
"size": 1075,
"path": "json-LICENSE.txt",
"size": 1075,
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86"
},
{
"size": 883,
"path": "files-text-license.txt",
"size": 883,
"sha256": "185c5a37521316e940ead89cbf0b1ba5862d810c7c966e6c6a14a4a515b23625"
},
{
"size": 94223,
"path": "README.md",
"size": 94223,
"sha256": "7b34f2af7341f57d7bac1b5b7e55a40595a6bc20f461f98c0c6320336d0deba7"
},
{
"size": 11418,
"path": "LAN-PREVIEW.md",
"size": 11418,
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada"
},
{
"size": 682867,
"path": "dictionaries/en.txt",
"size": 682867,
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21"
},
{
"size": 20138,
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
"size": 20138,
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5"
},
{
"size": 2265,
"path": "dictionaries/README.md",
"size": 2265,
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311"
},
{
"size": 1060364,
"path": "dictionaries/ru.txt",
"size": 1060364,
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b"
},
{
"size": 1280,
"path": "dictionaries/sources.json",
"size": 1280,
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408"
},
{
"size": 940799,
"path": "dictionaries/uk.txt",
"size": 940799,
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf"
},
{
"size": 993,
"path": "tools/README.txt",
"size": 993,
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a"
},
{
"size": 133120,
"path": "tools/StayHere.exe",
"sha256": "2fb100dab7de5609505091df539278c7966c229ed51458132a93736db766f2ac"
"size": 133120,
"sha256": "a67a06d20b280c55f02f70b9aae3a967e510bb21d3e1430ef080d628b9e1a484"
},
{
"size": 2796,
"path": "tools/StayHere-README.md",
"size": 2796,
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413"
}
]
+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,91,0
PRODUCTVERSION 0,6,91,0
FILEVERSION 0,6,92,0
PRODUCTVERSION 0,6,92,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.91"
VALUE "FileVersion", "0.6.92"
END
END
BLOCK "VarFileInfo"
+135 -5
View File
@@ -247,6 +247,49 @@ DWORD requestElevatedFolder(HWND owner,const fs::path &directory,
}
return ERROR_TIMEOUT;
}
struct FolderSize
{
uint64_t bytes{};
bool partial{};
};
using FolderSizes=std::map<fs::path,FolderSize>;
FolderSize measureFolder(FilesConnection &host,const fs::path &root,std::stop_token stop)
{
FolderSize size;
std::vector<fs::path> pending{root};
while(!pending.empty() && !stop.stop_requested())
{
const auto directory=std::move(pending.back());
pending.pop_back();
try
{
const auto info=statFile(host,directory);
if(!info || !(info->attributes&FILE_ATTRIBUTE_DIRECTORY))
{
size.partial=true;
continue;
}
if(info->attributes&FILE_ATTRIBUTE_REPARSE_POINT)
{
if(directory==root) size.partial=true;
continue;
}
listFiles(host,directory,[&](const fs::path &path,const FileInfo &entry)
{
if(stop.stop_requested()) return false;
if(entry.attributes&FILE_ATTRIBUTE_REPARSE_POINT) return true;
if(entry.attributes&FILE_ATTRIBUTE_DIRECTORY) pending.push_back(path);
else if(entry.size>UINT64_MAX-size.bytes) size.partial=true;
else size.bytes+=entry.size;
return true;
});
}
catch(const std::exception &) {size.partial=true;}
}
return size;
}
struct Panel
{
HWND path{}, list{}, heading{};
@@ -274,6 +317,9 @@ struct Panel
std::vector<std::wstring> sizes;
std::vector<bool> dimmed;
std::vector<bool> marked;
FolderSizes folderSizes;
std::future<FolderSizes> folderSizeQuery;
std::stop_source folderSizeStop;
std::set<fs::path> transferResults;
size_t fileExtensionLength{}, folderExtensionLength{};
bool wide{};
@@ -1260,7 +1306,11 @@ void showFileDetails(const Panel &panel)
if(statusFields&2)
{
const auto &size=panel.sizes[row];
details+=L" "+(size==L"Folder" || size==L"Up"?size:formatSize(std::stoull(size)));
const auto measured=panel.folderSizes.find(path);
if(size==L"Folder" && measured!=panel.folderSizes.end())
details+=L" "+formatSize(measured->second.bytes)+(measured->second.partial?L" (partial)":L"");
else
details+=L" "+(size==L"Folder" || size==L"Up"?size:formatSize(std::stoull(size)));
}
std::wstring timestamp=date;
if((statusFields&4) && timestamp.size()>=12) details+=timestamp.substr(0,12);
@@ -1360,6 +1410,12 @@ void openOn(Panel &panel,const fs::path &directory,bool offerElevation,std::opti
}
panel.directory = directory;
panel.peer=std::move(peer);
if(!sameDirectory)
{
panel.folderSizeStop.request_stop();
panel.folderSizeQuery={};
panel.folderSizes.clear();
}
if(!sameDirectory || nextWatch.handle!=INVALID_HANDLE_VALUE)
{
panel.watch.reset();
@@ -1443,6 +1499,61 @@ void refreshChangedPanels()
}
}
void calculateFolderSizes(Panel &panel,LRESULT current)
{
if(panel.folderSizeQuery.valid())
{
setStatus(L"Calculating folder sizes...");
return;
}
std::vector<fs::path> folders;
for(size_t i=0;i<panel.rows.size();++i)
if(panel.marked[i] && panel.sizes[i]==L"Folder") folders.push_back(panel.rows[i]);
if(folders.empty()) folders.push_back(panel.rows[current]);
panel.folderSizeStop=std::stop_source{};
std::packaged_task<FolderSizes()> task(
[folders=std::move(folders),peer=panel.peer,stop=panel.folderSizeStop.get_token()]
{
FolderSizes results;
try
{
FilesConnection host(peer?&*peer:nullptr);
for(const auto &folder:folders)
{
if(stop.stop_requested()) break;
results.emplace(folder,measureFolder(host,folder,stop));
}
}
catch(const std::exception &)
{
for(const auto &folder:folders) results.try_emplace(folder,FolderSize{0,true});
}
return results;
});
panel.folderSizeQuery=task.get_future();
try {std::thread(std::move(task)).detach();}
catch(const std::system_error &error)
{
panel.folderSizeQuery={};
setStatus(fileError(error));
return;
}
setStatus(L"Calculating folder sizes...");
}
void collectFolderSizes()
{
for(auto &panel:panels)
{
if(!panel.folderSizeQuery.valid() ||
panel.folderSizeQuery.wait_for(std::chrono::seconds(0))!=std::future_status::ready) continue;
for(const auto &[path,size]:panel.folderSizeQuery.get()) panel.folderSizes.insert_or_assign(path,size);
InvalidateRect(panel.list,nullptr,FALSE);
if(&panel==&panels[active]) showFileDetails(panel);
}
}
void openSelectedText(const Panel &panel, bool edit, bool external);
INT_PTR confirmTextSave(HWND owner);
INT_PTR paintTextDialog(HWND window, UINT message, WPARAM wp, LPARAM lp, const wchar_t *caption);
@@ -4312,11 +4423,22 @@ LRESULT CALLBACK panelKeys(HWND window, UINT message, WPARAM wp, LPARAM lp, UINT
{
if(wp==VK_TAB) { activatePanel(static_cast<int>(1-index)); SetFocus(terminalInput); return 0; }
if (wp == VK_RETURN) { enter(panels[index]); return 0; }
if (wp == VK_F2) { open(panels[index], panels[index].directory); return 0; }
if (wp == VK_F2)
{
auto &panel=panels[index];
panel.folderSizeStop.request_stop();
panel.folderSizeQuery={};
panel.folderSizes.clear();
open(panel,panel.directory);
return 0;
}
if (wp == VK_F10) { SendMessageW(GetParent(window), WM_CLOSE, 0, 0); return 0; }
if (wp == VK_F3 || wp == VK_F4)
{
openSelectedText(panels[index],wp==VK_F4,false);
auto &panel=panels[index];
const auto row=currentPanelRow(panel);
if(wp==VK_F3 && row!=LB_ERR && panel.sizes[row]==L"Folder") calculateFolderSizes(panel,row);
else openSelectedText(panel,wp==VK_F4,false);
return 0;
}
if (wp == VK_F5 || wp == VK_F6)
@@ -4646,7 +4768,7 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
updateTerminalOutput();
return 0;
}
if(wp==directoryWatchTimer) { refreshChangedPanels(); return 0; }
if(wp==directoryWatchTimer) { refreshChangedPanels(); collectFolderSizes(); return 0; }
if(wp!=transferHighlightTimer) break;
++transferHighlightPhase;
for(auto &panel:panels)
@@ -5122,7 +5244,14 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
paintText(item.hDC,name,text.c_str(),foreground,rowBackground,false,rowFont);
const auto &amount=panel.sizes[item.itemID];
auto visible=amount;
if(amount!=L"Folder" && amount!=L"Up")
const auto measured=folder?panel.folderSizes.find(panel.rows[item.itemID]):panel.folderSizes.end();
if(measured!=panel.folderSizes.end())
{
const auto partial=measured->second.partial;
visible=formatSize(measured->second.bytes,static_cast<size_t>(sizeWidth)-(partial?1:0));
if(partial) visible+=L"+";
}
else if(amount!=L"Folder" && amount!=L"Up")
visible=formatSize(std::stoull(amount),static_cast<size_t>(sizeWidth));
paintText(item.hDC,size,visible.c_str(),foreground,rowBackground,true);
RECT line{size.left,size.top,size.left+1,size.bottom};
@@ -5192,6 +5321,7 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
DestroyWindow(window);
return 0;
case WM_DESTROY:
for(auto &panel:panels) panel.folderSizeStop.request_stop();
KillTimer(window,terminalTimer);
KillTimer(window,directoryWatchTimer);
for(auto &panel:panels) panel.watch.reset();
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.91";
inline constexpr wchar_t AppVersion[] = L"0.6.92";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
+32
View File
@@ -299,6 +299,38 @@ try {
else { [Windows.Forms.Clipboard]::Clear() }
}
}
$sizeA=Join-Path $rightDir 'Size A'
$sizeB=Join-Path $rightDir 'Size B'
$sizeC=Join-Path $rightDir 'Size C'
New-Item -ItemType Directory -Path "$sizeA\nested", $sizeB, $sizeC | Out-Null
[IO.File]::WriteAllBytes("$sizeA\file.bin",[byte[]]::new(4))
[IO.File]::WriteAllBytes("$sizeA\nested\file.bin",[byte[]]::new(7))
[IO.File]::WriteAllBytes("$sizeB\file.bin",[byte[]]::new(23))
[IO.File]::WriteAllBytes("$sizeC\file.bin",[byte[]]::new(31))
Select-Row $right '[Size A]'
[void][FilesActivationUi]::PostMessage($right,0x100,[IntPtr]0x72,[IntPtr]::Zero)
Wait-Until 'F3 recursive size for the current folder' { (Read-Text $status) -ceq 'Size A 11 B' }
foreach($name in 'Size A','Size B'){
Select-Row $right "[$name]"
[void][FilesActivationUi]::Send($right,0x100,[IntPtr]45,[IntPtr]::Zero)
}
Select-Row $right '[Size C]'
[void][FilesActivationUi]::PostMessage($right,0x100,[IntPtr]0x72,[IntPtr]::Zero)
Wait-Until 'completion of the marked size batch' { (Read-Text $status) -ceq 'Size C Folder' }
Select-Row $right '[Size B]'
Wait-Until 'F3 size for a marked folder' { (Read-Text $status) -ceq 'Size B 23 B' }
Select-Row $right '[Size C]'
if ((Read-Text $status) -cne 'Size C Folder') { throw 'F3 measured the unmarked cursor folder.' }
Select-Row $right $fileName
[void][FilesActivationUi]::PostMessage($right,0x100,[IntPtr]0x72,[IntPtr]::Zero)
$viewerTitle='Viewer '+[char]0x2014+" $document"
Wait-Until 'F3 opening a file when folders are marked' {
[FilesActivationUi]::FindWindowEx([IntPtr]::Zero,[IntPtr]::Zero,'KSwitcher.Files.Text',$viewerTitle) -ne [IntPtr]::Zero
}
$viewer=[FilesActivationUi]::FindWindowEx([IntPtr]::Zero,[IntPtr]::Zero,'KSwitcher.Files.Text',$viewerTitle)
[void][FilesActivationUi]::PostMessage($viewer,0x10,[IntPtr]::Zero,[IntPtr]::Zero)
Write-Output 'F3 measured the current folder recursively and measured marked folders without measuring the unmarked cursor folder.'
} finally {
if ($process -and !$process.HasExited) {
[void]$process.CloseMainWindow()