Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56c6257465 | ||
|
|
64b03a2168 | ||
|
|
36baeeb081 |
No files matched your search
+1
-1
@@ -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()
|
||||
|
||||
@@ -69,6 +69,9 @@ Warning і початковий фокус на No.
|
||||
за тими ж правилами вставлення й лапок. Рядок `..` пропускається.
|
||||
- Ctrl+C копіює весь введений у командний рядок текст, навіть коли виділено
|
||||
лише його частину.
|
||||
- Ctrl+Insert копіює імена позначених файлів і папок активної панелі в порядку
|
||||
відображення, по одному на рядок. Якщо позначок немає, копіюється ім’я під
|
||||
курсором. Шляхи й рядок `..` пропускаються.
|
||||
|
||||
- Ctrl+E підставляє останню команду. Повторне натискання
|
||||
підставляє старішу, доки не досягне початку історії. Alt+F8 відкриває історію
|
||||
|
||||
Binary file not shown.
@@ -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 @@
|
||||
67db5adc069f847aa1d9987e4043dfd488b03ae15444a99b007774ac01556461 KSwitcher-win-x64.zip
|
||||
7db5db4c5d1f5c199d1cee7887336bdb2e34d4036b5af8cc20709957d6729d32 KSwitcher-win-x64.zip
|
||||
+60
-60
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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() }
|
||||
|
||||
Reference in new issue
Block a user