Compare commits

..
Author SHA1 Message Date
agrynco 56c6257465 Merge pull request 'Release KSwitcher v0.6.91' (#35)
Release KSwitcher v0.6.91
2026-11-16 01:09:12 +02:00
agrynco 64b03a2168 Release KSwitcher v0.6.91 2026-11-16 01:08:40 +02:00
agrynco 36baeeb081 Merge pull request 'Release KSwitcher v0.6.90' (#34)
Release KSwitcher v0.6.90
2026-11-16 00:46:44 +02:00
18 changed files with 274 additions and 122 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.90 LANGUAGES CXX RC)
project(KSwitcher VERSION 0.6.91 LANGUAGES CXX RC)
if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif()
+3
View File
@@ -69,6 +69,9 @@ Warning і початковий фокус на No.
за тими ж правилами вставлення й лапок. Рядок `..` пропускається.
- Ctrl+C копіює весь введений у командний рядок текст, навіть коли виділено
лише його частину.
- Ctrl+Insert копіює імена позначених файлів і папок активної панелі в порядку
відображення, по одному на рядок. Якщо позначок немає, копіюється ім’я під
курсором. Шляхи й рядок `..` пропускаються.
- Ctrl+E підставляє останню команду. Повторне натискання
підставляє старішу, доки не досягне початку історії. Alt+F8 відкриває історію
Binary file not shown.
+6
View File
@@ -1,5 +1,11 @@
# Latest Windows release
## 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.
**Українською.** Ctrl+Insert копіює імена позначених файлів і папок активної панелі, по одному на рядок. Якщо позначок немає, копіюється ім’я під курсором. LAN Files повторює відкриття TCP 45885, якщо порт зайнятий під час старту, а Change drive повторює невдалі запити дисків. Запит віддаленого оновлення чекає завершення поточної перевірки. Оновлювач визначає й закриває процеси своєї інсталяції, зокрема Files із резервної папки, і записує підтверджене завершення в журнал.
## v0.6.90 — 2026-11-16
Files groups Commands into navigation, viewing and editing, and remote PC actions. The remote group appears in an active LAN pane. Shared notices fit their contents and show a scrollbar only when needed. Remote hibernation and update confirmations use a compact red warning with No focused by default.
+1 -1
View File
@@ -1 +1 @@
67db5adc069f847aa1d9987e4043dfd488b03ae15444a99b007774ac01556461 KSwitcher-win-x64.zip
7db5db4c5d1f5c199d1cee7887336bdb2e34d4036b5af8cc20709957d6729d32 KSwitcher-win-x64.zip
+60 -60
View File
@@ -1,136 +1,136 @@
{
"version": "0.6.90",
"version": "0.6.91",
"schema": 1,
"files": [
{
"size": 2465792,
"sha256": "7f6d0d1508a324ccc793455f6667084dcd1003cbc472dca9f6262c014068f99b",
"path": "KSwitcher.exe"
"size": 2467840,
"path": "KSwitcher.exe",
"sha256": "eb4f1f7be3027a8ba073443932b0ea930a5d77f274f9603854f1331bd0954765"
},
{
"size": 2445824,
"sha256": "2ae249c373b6f8a8f232daffae84738751d9aad7d1f39bd7335fb379a415e50c",
"path": "KSwitcher.Config.exe"
"size": 2446848,
"path": "KSwitcher.Config.exe",
"sha256": "00a4817543f98643f679c4a92d197730bf9a62a114b918ab8540c179958b7004"
},
{
"size": 3052544,
"sha256": "bd1fca6c5dfa35ac2ae4afab5becb67ed7701d011ce6e671a05340f0634150e2",
"path": "KSwitcher.Screenshot.exe"
"size": 3053568,
"path": "KSwitcher.Screenshot.exe",
"sha256": "68b430e7c3c7f84c21ec843849a5f763ee3563c523cfd0507da6a4bef3db3810"
},
{
"size": 3675136,
"sha256": "bbcddc17a1cd69824a8cbdff7301854739c295b6b4fb27227acb00d375a9ab3a",
"path": "KSwitcher.Files.exe"
"size": 3676672,
"path": "KSwitcher.Files.exe",
"sha256": "e9257342788c8212a0d89eb62ed200115d88272225841181fc30feeeba8064c4"
},
{
"size": 194912,
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6",
"path": "WebView2Loader.dll"
"path": "WebView2Loader.dll",
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6"
},
{
"size": 1103,
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f",
"path": "terminal/addon-fit-LICENSE.txt"
"path": "terminal/addon-fit-LICENSE.txt",
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f"
},
{
"size": 1521,
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527",
"path": "terminal/addon-fit.js"
"path": "terminal/addon-fit.js",
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527"
},
{
"size": 527,
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef",
"path": "terminal/index.html"
"path": "terminal/index.html",
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef"
},
{
"size": 4268,
"sha256": "fb302b2902fea6a815a8266811f350d1e4c108ca88fafd49e6eddcbfff90a5e5",
"path": "terminal/terminal.js"
"size": 4375,
"path": "terminal/terminal.js",
"sha256": "7a31fe8679b7942775c7a9a3e34e836744111b7337e750d6ec7b21188dc4cd61"
},
{
"size": 1261,
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd",
"path": "terminal/xterm-LICENSE.txt"
"path": "terminal/xterm-LICENSE.txt",
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd"
},
{
"size": 7112,
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6",
"path": "terminal/xterm.css"
"path": "terminal/xterm.css",
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6"
},
{
"size": 488663,
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972",
"path": "terminal/xterm.js"
"path": "terminal/xterm.js",
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972"
},
{
"size": 4401,
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
"path": "LICENSE"
"path": "LICENSE",
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c"
},
{
"size": 1075,
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86",
"path": "json-LICENSE.txt"
"path": "json-LICENSE.txt",
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86"
},
{
"size": 883,
"sha256": "185c5a37521316e940ead89cbf0b1ba5862d810c7c966e6c6a14a4a515b23625",
"path": "files-text-license.txt"
"path": "files-text-license.txt",
"sha256": "185c5a37521316e940ead89cbf0b1ba5862d810c7c966e6c6a14a4a515b23625"
},
{
"size": 94223,
"sha256": "7b34f2af7341f57d7bac1b5b7e55a40595a6bc20f461f98c0c6320336d0deba7",
"path": "README.md"
"path": "README.md",
"sha256": "7b34f2af7341f57d7bac1b5b7e55a40595a6bc20f461f98c0c6320336d0deba7"
},
{
"size": 11418,
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada",
"path": "LAN-PREVIEW.md"
"path": "LAN-PREVIEW.md",
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada"
},
{
"size": 682867,
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21",
"path": "dictionaries/en.txt"
"path": "dictionaries/en.txt",
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21"
},
{
"size": 20138,
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5",
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt"
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5"
},
{
"size": 2265,
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
"path": "dictionaries/README.md"
"path": "dictionaries/README.md",
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311"
},
{
"size": 1060364,
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b",
"path": "dictionaries/ru.txt"
"path": "dictionaries/ru.txt",
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b"
},
{
"size": 1280,
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
"path": "dictionaries/sources.json"
"path": "dictionaries/sources.json",
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408"
},
{
"size": 940799,
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
"path": "dictionaries/uk.txt"
"path": "dictionaries/uk.txt",
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf"
},
{
"size": 993,
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
"path": "tools/README.txt"
"path": "tools/README.txt",
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a"
},
{
"size": 133120,
"sha256": "9e914d996864d7f982ba50e481381d673e4e84ee7ad5d1f477f0898aa1816256",
"path": "tools/StayHere.exe"
"path": "tools/StayHere.exe",
"sha256": "2fb100dab7de5609505091df539278c7966c229ed51458132a93736db766f2ac"
},
{
"size": 2796,
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
"path": "tools/StayHere-README.md"
"path": "tools/StayHere-README.md",
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413"
}
],
"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,90,0
PRODUCTVERSION 0,6,90,0
FILEVERSION 0,6,91,0
PRODUCTVERSION 0,6,91,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.90"
VALUE "FileVersion", "0.6.91"
END
END
BLOCK "VarFileInfo"
+3
View File
@@ -61,6 +61,9 @@ terminal.attachCustomKeyEventHandler(event => {
else if (event.key === 'Enter') {
if (!event.repeat) bridge.postMessage('insert-name');
}
else if (event.key === 'Insert') {
if (!event.repeat) bridge.postMessage('copy-panel-names');
}
else return true;
event.preventDefault();
return false;
+30 -1
View File
@@ -415,16 +415,45 @@ function Write-UpdateJournal([string]$Message) {
[Text.UTF8Encoding]::new($false))
}
function Stop-UpdateCompanions([string]$Directory) {
if (!('KSwitcherUpdateProcessImage' -as [type])) {
Add-Type @'
using System;
using System.Runtime.InteropServices;
using System.Text;
public static class KSwitcherUpdateProcessImage {
[DllImport("kernel32.dll")] static extern IntPtr OpenProcess(uint access, bool inherit, int pid);
[DllImport("kernel32.dll", CharSet=CharSet.Unicode)]
static extern bool QueryFullProcessImageName(IntPtr process, int flags, StringBuilder image, ref int length);
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
public static string Read(int pid) {
var process = OpenProcess(0x1000, false, pid);
if (process == IntPtr.Zero) return null;
try {
var length = 32768;
var image = new StringBuilder(length);
return QueryFullProcessImageName(process, 0, image, ref length) ? image.ToString() : null;
} finally { CloseHandle(process); }
}
}
'@
}
$root = [IO.Path]::GetFullPath($Directory).TrimEnd('\')
$prefix = [regex]::Escape($root + '\')
$pattern = '^' + $prefix + '(?:\.kswitcher-update-[0-9a-f]{32}\\backup\\)?KSwitcher\.(?:Config|Screenshot|Files)\.exe$'
foreach ($process in @(Get-Process 'KSwitcher.Config','KSwitcher.Screenshot','KSwitcher.Files' -ErrorAction SilentlyContinue)) {
if ($process.Path -notmatch $pattern) { continue }
$image = [KSwitcherUpdateProcessImage]::Read($process.Id)
if (!$image -and !$process.HasExited) {
Write-UpdateJournal "Could not identify $($process.ProcessName) (PID $($process.Id))."
}
if ($image -notmatch $pattern) { continue }
Write-UpdateJournal "Closing $($process.ProcessName) (PID $($process.Id))."
if (!$process.HasExited) {
try { $process.Kill() } catch { if (!$process.HasExited) { throw } }
if (!$process.WaitForExit(10000)) { throw "Could not close $($process.ProcessName) (PID $($process.Id))." }
}
Write-UpdateJournal "Closed $($process.ProcessName) (PID $($process.Id))."
}
}
if ($Library) { return }
+13 -1
View File
@@ -3010,7 +3010,13 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
case LanUpdateRequestedMessage:
if (!lan::remoteUpdateAllowed()) return 0;
if (app->updateRunning)
log(L"LAN update request ignored because an update is already running.");
{
if (!app->installingUpdate)
{
app->lanUpdatePending = true;
log(L"LAN update queued behind an update check.");
}
}
else
{
log(L"Starting unattended KSwitcher update requested over LAN.");
@@ -3030,6 +3036,12 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
// The helper has verified and staged the package; it waits for process exit.
DestroyWindow(hwnd);
}
else if (std::exchange(app->lanUpdatePending, false) && lan::remoteUpdateAllowed() &&
result->state != L"current")
{
log(L"Starting queued unattended KSwitcher update.");
app->checkUpdate(false, true);
}
else if (result->state == L"available")
{
app->availableUpdate = result->version;
+1 -1
View File
@@ -155,7 +155,7 @@ class App
std::vector<SyncConflict> pendingSyncConflicts;
Bytes syncSnapshot;
std::wstring syncStatus = L"Not connected.";
bool updateRunning{}, installingUpdate{};
bool updateRunning{}, installingUpdate{}, lanUpdatePending{};
std::wstring availableUpdate, notifiedUpdate;
bool (*updateNotifier)(const std::wstring &){};
bool stop{};
+63 -37
View File
@@ -345,8 +345,7 @@ struct DriveChoice { FilesPeer peer; fs::path path; };
std::vector<DriveQuery> driveQueries;
std::map<LRESULT,DriveChoice> remoteDriveRows;
constexpr UINT_PTR driveQueryTimer=3;
bool refreshDriveAddresses{};
ULONGLONG driveOpened{};
ULONGLONG nextDriveRefresh{},driveRefreshDeadline{};
int drivePickerWidth{};
int pickerPanel{};
std::wstring panelSearchPrefix;
@@ -905,6 +904,33 @@ LRESULT currentPanelRow(const Panel &panel)
return row>=0 && static_cast<size_t>(row)<panel.rows.size()?row:LB_ERR;
}
std::vector<fs::path> selectedPanelPaths(const Panel &panel)
{
std::vector<fs::path> paths;
for(size_t i=0;i<panel.rows.size();++i)
if(panel.marked[i] && panel.sizes[i]!=L"Up") paths.push_back(panel.rows[i]);
if(paths.empty())
{
const auto row=currentPanelRow(panel);
if(row!=LB_ERR && panel.sizes[row]!=L"Up") paths.push_back(panel.rows[row]);
}
return paths;
}
void copyPanelNames(HWND owner)
{
const auto paths=selectedPanelPaths(panels[active]);
if(paths.empty()) return;
std::wstring names;
for(const auto &path:paths)
{
if(!names.empty()) names+=L"\r\n";
names+=path.filename().native();
}
try { ks::writeClipboard(owner,ks::textClip(names)); }
catch(const std::exception &) { setStatus(L"Could not copy file names."); }
}
std::wstring powerShellPath(const fs::path &path)
{
std::wstring quoted=L"'";
@@ -986,6 +1012,7 @@ bool ensureTerminalView(HWND owner)
else if(message==L"insert-path") insertCurrentPanelPath();
else if(message==L"insert-name") insertCurrentPanelPath(true);
else if(message==L"copy-command-line") SendMessageW(terminalInput,WM_COPY,0,0);
else if(message==L"copy-panel-names") copyPanelNames(owner);
else if(message==L"toggle") showTerminal(owner,!terminalVisible);
else if(!terminalVisible && (message==L"grow" || message==L"shrink"))
{
@@ -1712,7 +1739,7 @@ void showRemoteDrives(bool focus=false,int initialRow=0)
add(query.peer.name);
if(query.pending.valid()) add(L" Loading drives...");
else if(!query.error.empty()) add(L" "+query.error);
else if(query.peer.address.empty()) add(refreshDriveAddresses?L" Looking for PC...":L" Offline");
else if(query.peer.address.empty()) add(nextDriveRefresh?L" Looking for PC...":L" Offline");
else if(query.drives.empty()) add(L" No drives");
for(const auto &entry:query.drives)
{
@@ -1739,26 +1766,30 @@ void pollDriveQueries(HWND owner)
return;
}
bool changed=false, pending=false;
if(refreshDriveAddresses && GetTickCount64()-driveOpened>=500)
const auto now=GetTickCount64();
if(nextDriveRefresh && now>=nextDriveRefresh)
{
refreshDriveAddresses=false;
nextDriveRefresh=now<driveRefreshDeadline?now+2000:0;
queryDrivePeers();
for(const auto &peer:drivePeers)
if(std::none_of(driveQueries.begin(),driveQueries.end(),[&](const DriveQuery &query){return query.peer.id==peer.id;}))
{
auto entry=std::find_if(driveQueries.begin(),driveQueries.end(),[&](const DriveQuery &query)
{return query.peer.id==peer.id;});
if(entry==driveQueries.end())
{
auto &query=driveQueries.emplace_back();
query.peer=peer;
startDriveQuery(query);
}
for(auto &query:driveQueries)
if(query.peer.address.empty())
for(const auto &peer:drivePeers)
if(peer.id==query.peer.id && !peer.address.empty())
{
query.peer=peer;
startDriveQuery(query);
break;
}
else if(!entry->pending.valid() && (entry->peer.address!=peer.address ||
entry->peer.address.empty() || !entry->error.empty()))
{
entry->peer=peer;
entry->drives=Json::array();
entry->error.clear();
startDriveQuery(*entry);
}
}
changed=true;
}
for(auto &query:driveQueries)
@@ -1772,7 +1803,8 @@ void pollDriveQueries(HWND owner)
try {query.drives=query.pending.get();}
catch(const std::system_error &e)
{
ks::log(L"LAN Files drive query failed: Windows error "+std::to_wstring(e.code().value()));
ks::log(L"LAN Files drive query for "+query.peer.name+L" ("+ks::wide(query.peer.address)+
L") 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
@@ -1785,8 +1817,12 @@ void pollDriveQueries(HWND owner)
}
changed=true;
}
if(!pending && drivePeerLookupError.empty() &&
std::none_of(driveQueries.begin(),driveQueries.end(),[](const DriveQuery &query)
{return query.peer.address.empty() || !query.error.empty();}))
nextDriveRefresh=0;
if(changed) showRemoteDrives();
if(!pending && !refreshDriveAddresses) KillTimer(owner,driveQueryTimer);
if(!pending && !nextDriveRefresh) KillTimer(owner,driveQueryTimer);
}
void chooseDrive(int index,bool fromMenu)
@@ -1806,8 +1842,9 @@ void chooseDrive(int index,bool fromMenu)
query.peer=peer;
startDriveQuery(query);
}
refreshDriveAddresses=true;
driveOpened=GetTickCount64();
const auto now=GetTickCount64();
nextDriveRefresh=now+500;
driveRefreshDeadline=now+20000;
const DWORD disks=GetLogicalDrives();
int selected=0;
int textWidth=0;
@@ -3523,14 +3560,8 @@ void copySelection(HWND owner,Panel &source,Panel &destination,bool move)
request.otherDirectory=destination.directory;
request.sourcePeer=source.peer?&*source.peer:nullptr;
request.otherPeer=request.destinationPeer=destination.peer?&*destination.peer:nullptr;
for(size_t i=0;i<source.rows.size();++i)
if(source.marked[i]) request.sources.push_back(source.rows[i]);
if(request.sources.empty())
{
const auto row=SendMessageW(source.list,LB_GETCURSEL,0,0);
if(row<0 || static_cast<size_t>(row)>=source.rows.size() || source.sizes[row]==L"Up") return;
request.sources.push_back(source.rows[row]);
}
request.sources=selectedPanelPaths(source);
if(request.sources.empty()) return;
const auto result=FilesModalDialog::run(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(102),owner,copyDialog,
reinterpret_cast<LPARAM>(&request));
if(result==-1) setStatus(L"Could not open the Copy dialog.");
@@ -3834,15 +3865,8 @@ bool pumpDeletion(HWND window,bool &canceled)
void deleteSelection(HWND owner,Panel &panel,bool permanent)
{
if(copyInProgress) return;
std::vector<fs::path> paths;
for(size_t i=0;i<panel.rows.size();++i)
if(panel.marked[i] && panel.sizes[i]!=L"Up") paths.push_back(panel.rows[i]);
if(paths.empty())
{
const auto row=SendMessageW(panel.list,LB_GETCURSEL,0,0);
if(row<0 || static_cast<size_t>(row)>=panel.rows.size() || panel.sizes[row]==L"Up") return;
paths.push_back(panel.rows[row]);
}
auto paths=selectedPanelPaths(panel);
if(paths.empty()) return;
const auto shown=std::min<size_t>(paths.size(),10);
std::wstring entries;
for(size_t i=0;i<shown;++i)
@@ -5329,12 +5353,14 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
{
const bool ctrl=(GetKeyState(VK_CONTROL)&0x8000)!=0;
const bool shift=(GetKeyState(VK_SHIFT)&0x8000)!=0;
if(ctrl && !shift && (message.wParam=='F' || message.wParam=='C' || message.wParam==VK_RETURN))
if(ctrl && !shift &&
(message.wParam=='F' || message.wParam=='C' || message.wParam==VK_RETURN || message.wParam==VK_INSERT))
{
if(!(message.lParam & (1LL<<30)))
{
if(message.wParam=='F') insertCurrentPanelPath();
else if(message.wParam==VK_RETURN) insertCurrentPanelPath(true);
else if(message.wParam==VK_INSERT) copyPanelNames(window);
else SendMessageW(terminalInput,WM_COPY,0,0);
}
continue;
+7 -3
View File
@@ -305,10 +305,15 @@ struct LanService::Impl
std::lock_guard lock(mutex);
if(shareInput&&trusted.contains(target)&&peers.contains(target))send(socket,peers.at(target).address,packet);
});
const bool filesListening=files.port()!=0;
if(!filesListening) diagnosticEvent(L"LAN Files could not listen on TCP 45885.");
bool filesListening=files.port()!=0;
while(!stopping)
{
files.poll();
if(filesListening!=(files.port()!=0))
{
filesListening=files.port()!=0;
nextHello=0;
}
auto now=GetTickCount64();bool clipboardActive=false;
std::vector<lan::InputEndpoint> endpoints;
{
@@ -346,7 +351,6 @@ struct LanService::Impl
{std::lock_guard lock(inputMutex);out.swap(inputOut);}
if(abortInput.exchange(false)){hub.disconnect();out.clear();}
hub.poll(endpoints,out,in,now);
files.poll();
auto readyPeers=hub.ready();inputConnected=!readyPeers.empty();
{std::lock_guard lock(inputMutex);inputReady=std::move(readyPeers);
for(auto &frame:in){
+27 -13
View File
@@ -1031,30 +1031,43 @@ struct FilesListener::Impl
HostAction hibernate,update;
WinSocket listener;
std::vector<std::unique_ptr<Client>> clients;
uint16_t boundPort{};
uint16_t requestedPort{},boundPort{};
ULONGLONG nextBind{};
DWORD lastBindError{};
Impl(std::string id,PairKey lookup,uint16_t port,TerminalPermission permission,HostAction hibernateAction,
HostAction updateAction):
local(std::move(id)),key(std::move(lookup)),terminalPermission(std::move(permission)),
hibernate(std::move(hibernateAction)),update(std::move(updateAction))
hibernate(std::move(hibernateAction)),update(std::move(updateAction)),requestedPort(port)
{
listener.value=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(listener.value==INVALID_SOCKET) return;
bindListener();
}
void bindListener()
{
nextBind=GetTickCount64()+1000;
WinSocket candidate{socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)};
BOOL exclusive=TRUE;
u_long mode=1;
sockaddr_in address{};
address.sin_family=AF_INET;
address.sin_port=htons(port);
if(setsockopt(listener.value,SOL_SOCKET,SO_EXCLUSIVEADDRUSE,reinterpret_cast<const char*>(&exclusive),sizeof(exclusive)) ||
ioctlsocket(listener.value,FIONBIO,&mode) ||
bind(listener.value,reinterpret_cast<sockaddr*>(&address),sizeof(address)) ||
listen(listener.value,8))
address.sin_port=htons(requestedPort);
int length=sizeof(address);
if(candidate.value==INVALID_SOCKET ||
setsockopt(candidate.value,SOL_SOCKET,SO_EXCLUSIVEADDRUSE,reinterpret_cast<const char*>(&exclusive),sizeof(exclusive)) ||
ioctlsocket(candidate.value,FIONBIO,&mode) ||
bind(candidate.value,reinterpret_cast<sockaddr*>(&address),sizeof(address)) ||
listen(candidate.value,8) ||
getsockname(candidate.value,reinterpret_cast<sockaddr*>(&address),&length))
{
closesocket(std::exchange(listener.value,INVALID_SOCKET));
const auto error=static_cast<DWORD>(WSAGetLastError());
if(requestedPort==FilesPort && lastBindError!=error)
log(L"LAN Files TCP 45885 unavailable; retrying. Windows error "+std::to_wstring(error));
lastBindError=error;
return;
}
int length=sizeof(address);
if(!getsockname(listener.value,reinterpret_cast<sockaddr*>(&address),&length))
boundPort=ntohs(address.sin_port);
listener.value=std::exchange(candidate.value,INVALID_SOCKET);
boundPort=ntohs(address.sin_port);
if(requestedPort==FilesPort) log(L"LAN Files listener ready on TCP 45885.");
lastBindError=0;
}
void serve(SOCKET socket)
{
@@ -1089,6 +1102,7 @@ struct FilesListener::Impl
void poll()
{
std::erase_if(clients,[](const auto &client){return client->done.load();});
if(listener.value==INVALID_SOCKET && GetTickCount64()>=nextBind) bindListener();
if(listener.value==INVALID_SOCKET || clients.size()>=16) return;
sockaddr_in address{};
int size=sizeof(address);
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.90";
inline constexpr wchar_t AppVersion[] = L"0.6.91";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
+29
View File
@@ -270,6 +270,35 @@ try {
}
if ([FilesActivationUi]::Focus($window) -ne $inputBox) { throw 'Deletion moved focus.' }
Write-Output 'Both panels refreshed external creation, modification and deletion while preserving focus.'
Add-Type -AssemblyName System.Windows.Forms
$savedClipboard=[Windows.Forms.Clipboard]::GetDataObject()
$programName=[IO.Path]::GetFileName($program)
$expectedNames="Test folder`r`n$programName"
try {
Select-Row $left '[Test folder]'
[void][FilesActivationUi]::Send($left,0x100,[IntPtr]45,[IntPtr]::Zero)
Select-Row $left $programName
[void][FilesActivationUi]::Send($left,0x100,[IntPtr]45,[IntPtr]::Zero)
Wait-Until 'Files activation for Ctrl+Insert' {
[FilesActivationUi]::Activate($window) -and [FilesActivationUi]::Focus($window) -eq $inputBox
}
$shell.SendKeys('^{INSERT}')
Wait-Until 'Ctrl+Insert copying marked names in panel order' {
[Windows.Forms.Clipboard]::GetText() -ceq $expectedNames
}
Select-Row $right $fileName
$shell.SendKeys('^{INSERT}')
Wait-Until 'Ctrl+Insert copying the current item in the other pane' {
[Windows.Forms.Clipboard]::GetText() -ceq $fileName
}
Write-Output 'Ctrl+Insert copied marked names in order and used the cursor in the other pane.'
} finally {
if ([Windows.Forms.Clipboard]::GetText() -cin @($expectedNames,$fileName)) {
if ($savedClipboard) { [Windows.Forms.Clipboard]::SetDataObject($savedClipboard,$true) }
else { [Windows.Forms.Clipboard]::Clear() }
}
}
} finally {
if ($process -and !$process.HasExited) {
[void]$process.CloseMainWindow()
+21
View File
@@ -133,6 +133,27 @@ int main()
fs::create_directory(root);
const auto client=randomHex(16),first=randomHex(16),second=randomHex(16),key=randomHex(32);
auto lookup=[&](const std::string &id){return id==client?key:std::string{};};
{
auto previous=std::make_unique<FilesListener>(first,lookup,0);
const auto port=previous->port();
require(port!=0,"old listener bound before restart");
FilesListener restarting(first,lookup,port);
require(restarting.port()==0,"new listener must not take over an occupied port");
previous.reset();
const auto deadline=GetTickCount64()+4000;
while(!restarting.port() && GetTickCount64()<deadline)
{
restarting.poll();
Sleep(20);
}
require(restarting.port()==port,"listener automatically recovers after its port becomes available");
std::jthread pollingRestarted([&](std::stop_token stop)
{
while(!stop.stop_requested()){restarting.poll();Sleep(5);}
});
FilesConnection recovered(FilesPeer{first,"127.0.0.1",L"Recovered",port},client,key);
checkFileReply(recovered.call({{"action","drives"}}));
}
std::atomic<bool> allowTerminal{};
std::atomic<unsigned> hibernateRequests{};
std::atomic<unsigned> updateRequests{};
+5
View File
@@ -23,10 +23,15 @@ try {
Copy-Item -LiteralPath (Join-Path ([Environment]::SystemDirectory) 'ping.exe') -Destination $path
$companions+=Start-Process -FilePath $path -ArgumentList '-t','127.0.0.1' -WindowStyle Hidden -PassThru
}
$oldFiles=Join-Path $companionRoot 'KSwitcher.Files.exe'
$backup=Join-Path $companionRoot '.kswitcher-update-0123456789abcdef0123456789abcdef/backup'
[IO.Directory]::CreateDirectory($backup) | Out-Null
Move-Item -LiteralPath $oldFiles -Destination $backup
Stop-UpdateCompanions $companionRoot
Assert (@($companions[0..2] | Where-Object HasExited).Count -eq 3) 'unattended update closes all companions in its installation'
Assert (!$companions[3].HasExited) 'unattended update leaves a companion from another installation running'
Assert (@(Get-Content -LiteralPath $Journal | Where-Object { $_ -match 'LAN update: Closing KSwitcher\.' }).Count -eq 3) 'unattended update journals every companion closure'
Assert (@(Get-Content -LiteralPath $Journal | Where-Object { $_ -match 'LAN update: Closed KSwitcher\.' }).Count -eq 3) 'unattended update confirms each companion exited'
} finally {
foreach ($companion in $companions) {
if (!$companion.HasExited) { $companion.Kill(); $companion.WaitForExit() }