Compare commits

...
14 changed files with 169 additions and 73 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.87 LANGUAGES CXX RC)
project(KSwitcher VERSION 0.6.88 LANGUAGES CXX RC)
if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif()
+2
View File
@@ -43,6 +43,8 @@ Far Manager є зразком для оформлення файлового м
- Ctrl+F вставляє повний шлях елемента під курсором активної панелі в позицію
курсора командного рядка або замінює виділення. Подвійні лапки додаються
лише до шляхів із пробілами. Для `..` вставляється шлях батьківської папки.
- Ctrl+Enter вставляє лише назву файла або папки під курсором активної панелі
за тими ж правилами вставлення й лапок. Рядок `..` пропускається.
- Ctrl+C копіює весь введений у командний рядок текст, навіть коли виділено
лише його частину.
Binary file not shown.
+7
View File
@@ -1,5 +1,12 @@
# Latest Windows release
## v0.6.88 — 2026-11-16
In Files, Ctrl+Enter inserts the name of the file or folder under the active pane cursor into the command line. An open Files window can still request the LAN peer list after KSwitcher updates and restarts.
**Українською.** У Files Ctrl+Enter вставляє в командний рядок назву файла або папки під курсором активної панелі. Відкрите вікно Files і далі може отримувати список LAN-комп’ютерів після оновлення та автоматичного перезапуску KSwitcher.
## v0.6.87 — 2026-11-15
Remote PowerShell in Files now displays errors as text and hides progress metadata. Update the PC that runs the commands to remove raw CLIXML from terminal output. This release supersedes v0.6.86.
+1 -1
View File
@@ -1 +1 @@
8e69019173d92de7790917573c9e95bb96104994d518170cad320d294fca2153 KSwitcher-win-x64.zip
d48e590580801a39a08ac8f5192983610e0a3fec9e5b0cf7f01a40a063cbed78 KSwitcher-win-x64.zip
+59 -59
View File
@@ -1,136 +1,136 @@
{
"files": [
{
"size": 2462208,
"sha256": "fad74ffb05e9c3ce6ada562c610837539809682cef33d0be1e878dc45e4dea9d",
"path": "KSwitcher.exe"
"path": "KSwitcher.exe",
"sha256": "aa542387388904d2a8df98bdef0e0b25b12aa294ded279f3308553d762086fee",
"size": 5852660
},
{
"size": 2439168,
"sha256": "8c88bd7f4018d0f400e56f2ae21c63b259f2ab482adfd82bf2e067a48e7eb47a",
"path": "KSwitcher.Config.exe"
"path": "KSwitcher.Config.exe",
"sha256": "c128e8affc7485ba475228ca9bee6915948208405bd84110ffc776ebf8a3d0da",
"size": 5917446
},
{
"size": 3050496,
"sha256": "7d2a3d04436989411e1220569fbfef95a9916696ec97b62963c3c04f128a11ed",
"path": "KSwitcher.Screenshot.exe"
"path": "KSwitcher.Screenshot.exe",
"sha256": "82397c1e4d73415449ba7389a716d6e1ec4dab2310b1471369f467435b771762",
"size": 5747437
},
{
"size": 3660288,
"sha256": "7a5596ee03cce3000f83f13d8de1113be467590abcf03ef4a2bd8d13f1d3b5fe",
"path": "KSwitcher.Files.exe"
"path": "KSwitcher.Files.exe",
"sha256": "c86b7ff32468bafa6f658a810f475b557f47a23ba42a1739db09d7eeab6fb533",
"size": 8159101
},
{
"size": 194912,
"path": "WebView2Loader.dll",
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6",
"path": "WebView2Loader.dll"
"size": 194912
},
{
"size": 1103,
"path": "terminal/addon-fit-LICENSE.txt",
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f",
"path": "terminal/addon-fit-LICENSE.txt"
"size": 1103
},
{
"size": 1521,
"path": "terminal/addon-fit.js",
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527",
"path": "terminal/addon-fit.js"
"size": 1521
},
{
"size": 527,
"path": "terminal/index.html",
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef",
"path": "terminal/index.html"
"size": 527
},
{
"size": 4167,
"sha256": "d60e6ff8948b6616695297695b05ef6e74b497a8e32e6031fc2e4fe2cca39ca7",
"path": "terminal/terminal.js"
"path": "terminal/terminal.js",
"sha256": "fb302b2902fea6a815a8266811f350d1e4c108ca88fafd49e6eddcbfff90a5e5",
"size": 4268
},
{
"size": 1261,
"path": "terminal/xterm-LICENSE.txt",
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd",
"path": "terminal/xterm-LICENSE.txt"
"size": 1261
},
{
"size": 7112,
"path": "terminal/xterm.css",
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6",
"path": "terminal/xterm.css"
"size": 7112
},
{
"size": 488663,
"path": "terminal/xterm.js",
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972",
"path": "terminal/xterm.js"
"size": 488663
},
{
"size": 4401,
"path": "LICENSE",
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
"path": "LICENSE"
"size": 4401
},
{
"size": 1075,
"path": "json-LICENSE.txt",
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86",
"path": "json-LICENSE.txt"
"size": 1075
},
{
"size": 883,
"path": "files-text-license.txt",
"sha256": "185c5a37521316e940ead89cbf0b1ba5862d810c7c966e6c6a14a4a515b23625",
"path": "files-text-license.txt"
"size": 883
},
{
"size": 94223,
"path": "README.md",
"sha256": "7b34f2af7341f57d7bac1b5b7e55a40595a6bc20f461f98c0c6320336d0deba7",
"path": "README.md"
"size": 94223
},
{
"size": 11418,
"path": "LAN-PREVIEW.md",
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada",
"path": "LAN-PREVIEW.md"
"size": 11418
},
{
"size": 682867,
"path": "dictionaries/en.txt",
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21",
"path": "dictionaries/en.txt"
"size": 682867
},
{
"size": 20138,
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5",
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt"
"size": 20138
},
{
"size": 2265,
"path": "dictionaries/README.md",
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
"path": "dictionaries/README.md"
"size": 2265
},
{
"size": 1060364,
"path": "dictionaries/ru.txt",
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b",
"path": "dictionaries/ru.txt"
"size": 1060364
},
{
"size": 1280,
"path": "dictionaries/sources.json",
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
"path": "dictionaries/sources.json"
"size": 1280
},
{
"size": 940799,
"path": "dictionaries/uk.txt",
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
"path": "dictionaries/uk.txt"
"size": 940799
},
{
"size": 993,
"path": "tools/README.txt",
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
"path": "tools/README.txt"
"size": 993
},
{
"size": 133120,
"sha256": "759c4ad30bbcf1f99d086dbd88dd2f5c68012d6a793479e8712a6934e17e23f4",
"path": "tools/StayHere.exe"
"path": "tools/StayHere.exe",
"sha256": "bb595202cc4749f3c0fe47adc92d4b02a174abeb5765e493673de9f5e070251c",
"size": 540583
},
{
"size": 2796,
"path": "tools/StayHere-README.md",
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
"path": "tools/StayHere-README.md"
"size": 2796
}
],
"version": "0.6.87",
"version": "0.6.88",
"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,87,0
PRODUCTVERSION 0,6,87,0
FILEVERSION 0,6,88,0
PRODUCTVERSION 0,6,88,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.87"
VALUE "FileVersion", "0.6.88"
END
END
BLOCK "VarFileInfo"
+3
View File
@@ -58,6 +58,9 @@ terminal.attachCustomKeyEventHandler(event => {
else if (event.code === 'KeyF' || event.code === 'KeyC') {
if (!event.repeat) bridge.postMessage(event.code === 'KeyF' ? 'insert-path' : 'copy-command-line');
}
else if (event.key === 'Enter') {
if (!event.repeat) bridge.postMessage('insert-name');
}
else return true;
event.preventDefault();
return false;
+16 -5
View File
@@ -4,17 +4,25 @@
#include <cstring>
namespace ks
{
bool configurationPeer(HWND window, const wchar_t *filename)
namespace
{
std::filesystem::path processImage(HWND window)
{
DWORD pid{};
GetWindowThreadProcessId(window, &pid);
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!process) return false;
if (!process) return {};
wchar_t path[32768]; DWORD count = std::size(path);
bool ok = QueryFullProcessImageNameW(process, 0, path, &count) != 0;
CloseHandle(process);
auto expected = std::filesystem::path(executable()).parent_path() / filename;
return ok && _wcsicmp(path, expected.c_str()) == 0;
return ok ? std::filesystem::path(path) : std::filesystem::path{};
}
}
bool configurationPeer(HWND window, const wchar_t *filename)
{
const auto expected = std::filesystem::path(executable()).parent_path() / filename;
return _wcsicmp(processImage(window).c_str(), expected.c_str()) == 0;
}
void launchConfiguration(App &app, bool snippets, bool logs)
{
@@ -35,7 +43,10 @@ void launchConfiguration(App &app, bool snippets, bool logs)
LRESULT configurationRequest(App &app, HWND sender, const COPYDATASTRUCT &message)
{
const bool filesPeers = message.dwData == FilesLanPeers;
if (!configurationPeer(sender, filesPeers ? L"KSwitcher.Files.exe" : L"KSwitcher.Config.exe") || message.cbData > ConfigLimit ||
const bool knownSender = filesPeers
? isFilesPeerImage(processImage(sender), std::filesystem::path(executable()).parent_path())
: configurationPeer(sender, L"KSwitcher.Config.exe");
if (!knownSender || message.cbData > ConfigLimit ||
(message.cbData && !message.lpData)) return FALSE;
Bytes reply;
bool ok = false;
+24
View File
@@ -1,6 +1,30 @@
#pragma once
#include <windows.h>
#include <filesystem>
#include <string_view>
inline constexpr ULONG_PTR FilesLanPeers = 0x4b53010d;
inline constexpr ULONG_PTR FilesLanPeersReply = 0x4b53010e;
inline constexpr DWORD FilesLanPeersLimit = 64 * 1024;
namespace ks
{
inline bool isFilesPeerImage(const std::filesystem::path &image, const std::filesystem::path &install)
{
if (_wcsicmp(image.filename().c_str(), L"KSwitcher.Files.exe") != 0) return false;
const auto directory = image.parent_path();
if (_wcsicmp(directory.c_str(), install.c_str()) == 0) return true;
// Updating moves a still-running Files executable into the transaction backup.
// Windows then reports the moved path for its process image.
if (_wcsicmp(directory.filename().c_str(), L"backup") != 0) return false;
const auto transaction = directory.parent_path();
if (_wcsicmp(transaction.parent_path().c_str(), install.c_str()) != 0) return false;
constexpr std::wstring_view prefix = L".kswitcher-update-";
const auto name = transaction.filename().wstring();
if (name.size() != prefix.size() + 32 || _wcsnicmp(name.c_str(), prefix.data(), prefix.size()) != 0)
return false;
return std::wstring_view(name).substr(prefix.size()).find_first_not_of(L"0123456789abcdefABCDEF")
== std::wstring_view::npos;
}
}
+7 -3
View File
@@ -909,12 +909,14 @@ std::wstring powerShellPath(const fs::path &path)
return quoted+L"'";
}
void insertCurrentPanelPath()
void insertCurrentPanelPath(bool nameOnly = false)
{
const auto &panel=panels[active];
const auto row=currentPanelRow(panel);
if(row==LB_ERR) return;
const auto &path=panel.rows[row].native();
const auto &item=panel.rows[row];
if(nameOnly && row==0 && panel.directory.has_relative_path()) return;
const auto path=nameOnly?item.filename().native():item.native();
std::wstring quoted;
if(path.find(L' ')!=std::wstring::npos)
{
@@ -974,6 +976,7 @@ bool ensureTerminalView(HWND owner)
if(GetForegroundWindow()==owner) SetFocus(terminalInput);
}
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"toggle") showTerminal(owner,!terminalVisible);
else if(!terminalVisible && (message==L"grow" || message==L"shrink"))
@@ -5147,11 +5150,12 @@ 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'))
if(ctrl && !shift && (message.wParam=='F' || message.wParam=='C' || message.wParam==VK_RETURN))
{
if(!(message.lParam & (1LL<<30)))
{
if(message.wParam=='F') insertCurrentPanelPath();
else if(message.wParam==VK_RETURN) insertCurrentPanelPath(true);
else SendMessageW(terminalInput,WM_COPY,0,0);
}
continue;
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.87";
inline constexpr wchar_t AppVersion[] = L"0.6.88";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
+26
View File
@@ -20,6 +20,9 @@ public static class FilesActivationUi {
[DllImport("user32.dll")] public static extern IntPtr GetDlgItem(IntPtr window, int id);
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string type, string title);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr window);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr window);
[DllImport("user32.dll")] static extern void keybd_event(byte key, byte scan, uint flags, UIntPtr extra);
[DllImport("user32.dll")] public static extern bool PostMessage(IntPtr window, uint msg, IntPtr w, IntPtr l);
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr window, IntPtr process);
[DllImport("user32.dll")] static extern bool GetGUIThreadInfo(uint thread, ref GuiInfo info);
@@ -36,6 +39,14 @@ public static class FilesActivationUi {
return info.Focus;
}
public static bool Activate(IntPtr window) {
if (GetForegroundWindow() == window) return true;
keybd_event(0x12, 0, 0, UIntPtr.Zero);
keybd_event(0x12, 0, 2, UIntPtr.Zero);
SetForegroundWindow(window);
return GetForegroundWindow() == window;
}
public static void ClickRow(IntPtr list, int row, bool doubleClick) {
Rect rect;
if (SendRect(list, 0x198, new IntPtr(row), out rect).ToInt32() == -1)
@@ -179,6 +190,21 @@ try {
[FilesActivationUi]::Send($left, 0x188, [IntPtr]::Zero, [IntPtr]::Zero).ToInt32() -eq (Find-Row $left '..')
}
if ((Read-Text $inputBox) -cne 'draft command') { throw 'Pane navigation changed the command line.' }
$shell = New-Object -ComObject WScript.Shell
$fileName = [IO.Path]::GetFileName($document)
foreach ($name in $fileName, 'Test folder') {
$label = if ($name -eq $fileName) { $name } else { "[$name]" }
Select-Row $right $label
[void][FilesActivationUi]::Write($inputBox, 0x0C, [IntPtr]::Zero, 'draft command')
[void][FilesActivationUi]::Send($inputBox, 0xB1, [IntPtr]6, [IntPtr]13)
Wait-Until 'Files activation for Ctrl+Enter' {
[FilesActivationUi]::Activate($window) -and
[FilesActivationUi]::Focus($window) -eq $inputBox
}
$shell.SendKeys('^{ENTER}')
$expected = 'draft "{0}"' -f $name
Wait-Until "Ctrl+Enter inserting $name" { (Read-Text $inputBox) -ceq $expected }
}
[void][FilesActivationUi]::Write($inputBox, 0x0C, [IntPtr]::Zero, '')
Write-Output 'Tab and Down navigated the panes while preserving command-line focus and text.'
+19
View File
@@ -1,6 +1,7 @@
#include <winsock2.h>
#include <sddl.h>
#include "lan_files.h"
#include "files_lan_ipc.h"
#include <bit>
#include <atomic>
#include <fstream>
@@ -18,6 +19,23 @@ void require(bool condition,const char *message)
if(!condition) throw std::runtime_error(message);
}
void verifyFilesPeerImage()
{
const fs::path install = LR"(C:\Tools\KSwitcher)";
const auto transaction = install / L".kswitcher-update-0123456789abcdef0123456789abcdef";
constexpr auto files = L"KSwitcher.Files.exe";
require(isFilesPeerImage(install / files, install), "installed Files can request LAN peers");
require(isFilesPeerImage(transaction / L"backup" / files, install),
"Files open during an update can still request LAN peers");
require(!isFilesPeerImage(transaction / L"stage" / files, install), "staged Files image is rejected");
require(!isFilesPeerImage(transaction / L"backup" / L"KSwitcher.Config.exe", install),
"another executable in the backup is rejected");
require(!isFilesPeerImage(install / L".kswitcher-update-invalid" / L"backup" / files, install),
"malformed transaction directory is rejected");
require(!isFilesPeerImage(install.parent_path() / transaction.filename() / L"backup" / files, install),
"backup outside the active installation is rejected");
}
void save(const fs::path &path,const std::string &text)
{
std::ofstream file(path,std::ios::binary|std::ios::trunc);
@@ -111,6 +129,7 @@ int main()
int result=0;
try
{
verifyFilesPeerImage();
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{};};