Release v0.6.45: fix Far snippets and ship portable Utilities folder
This commit is contained in:
1 parent
90b2e0841f
commit
c109408a98
16 files changed
+422
-34
No files matched your search
+102
-16
@@ -6,6 +6,7 @@
|
||||
#include "configuration.h"
|
||||
#include "diagnostics.h"
|
||||
#include "paste_probe.h"
|
||||
#include "utilities.h"
|
||||
#include "screenshot_notifications.h"
|
||||
#include <algorithm>
|
||||
#include <commctrl.h>
|
||||
@@ -82,9 +83,12 @@ class Transaction
|
||||
bool trustedContentsObserved{};
|
||||
std::unique_ptr<PasteProbe> selectionProof;
|
||||
std::wstring provenSelection;
|
||||
bool farSnippet{};
|
||||
size_t farSelectedSteps{};
|
||||
|
||||
public:
|
||||
Transaction(App &a, HWND t, uint64_t v) : app(a), target(t), version(v)
|
||||
Transaction(App &a, HWND t, uint64_t v, bool farEditor = false)
|
||||
: app(a), target(t), version(v), farSnippet(farEditor)
|
||||
{
|
||||
app.suspended = true;
|
||||
try
|
||||
@@ -119,6 +123,7 @@ class Transaction
|
||||
}
|
||||
~Transaction()
|
||||
{
|
||||
if (farSnippet) finishSelection(false);
|
||||
if (captured && GetClipboardSequenceNumber() == expected)
|
||||
try
|
||||
{
|
||||
@@ -200,7 +205,7 @@ class Transaction
|
||||
diagnosticEvent(L"Copy begin number=" + std::to_wstring(number) + L" sentinelSequence=" +
|
||||
std::to_wstring(before) + L" target=" + std::to_wstring((uintptr_t)target) +
|
||||
L" sentinel=" + diagnosticText(sentinel));
|
||||
if (!valid() || !chord(L'C', MOD_CONTROL))
|
||||
if (!valid() || !chord(farSnippet ? VK_INSERT : L'C', MOD_CONTROL))
|
||||
{
|
||||
diagnosticEvent(L"Copy failed: context changed or simulated Ctrl+C blocked");
|
||||
return {};
|
||||
@@ -250,7 +255,7 @@ class Transaction
|
||||
check();
|
||||
if (!valid() || GetClipboardSequenceNumber() != before)
|
||||
return {};
|
||||
if (!chord(L'C', MOD_CONTROL))
|
||||
if (!chord(farSnippet ? VK_INSERT : L'C', MOD_CONTROL))
|
||||
return {};
|
||||
diagnosticEvent(L"Copy retry: clipboard unchanged after Ctrl+C number=" + std::to_wstring(number) +
|
||||
L" attempt=" + std::to_wstring(attempts + 1) + L" elapsedMs=" + std::to_wstring(GetTickCount64() - started));
|
||||
@@ -337,8 +342,44 @@ class Transaction
|
||||
bool recoveredSelection() const { return selectionProof != nullptr; }
|
||||
bool select(size_t n)
|
||||
{
|
||||
if (farSnippet)
|
||||
{
|
||||
if (n > 4096 || farSelectedSteps) return false;
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
{
|
||||
if (!valid() || !chord(VK_LEFT, MOD_ALT | MOD_SHIFT)) return false;
|
||||
++farSelectedSteps;
|
||||
}
|
||||
return valid();
|
||||
}
|
||||
return selectPrevious(n, [&] { return valid(); });
|
||||
}
|
||||
bool finishSelection(bool atStart)
|
||||
{
|
||||
if (!farSnippet) return valid() && chord(atStart ? VK_LEFT : VK_RIGHT);
|
||||
if (!farSelectedSteps) return valid();
|
||||
if (atStart)
|
||||
{
|
||||
// Far arrows move one character, rather than collapsing a selection
|
||||
// to its edge. Move into the nonempty selected range and back.
|
||||
if (!valid() || !chord(VK_RIGHT)) return false;
|
||||
farSelectedSteps = 0;
|
||||
return valid() && chord(VK_LEFT);
|
||||
}
|
||||
while (farSelectedSteps && valid())
|
||||
{
|
||||
if (!chord(VK_RIGHT, MOD_ALT | MOD_SHIFT)) return false;
|
||||
--farSelectedSteps;
|
||||
}
|
||||
return !farSelectedSteps && valid();
|
||||
}
|
||||
std::optional<std::wstring> readFarSnippet(size_t count)
|
||||
{
|
||||
if (!farSnippet || !count || !select(count)) return {};
|
||||
auto observed = copy();
|
||||
if (!finishSelection(false) || observed.empty()) return {};
|
||||
return observed;
|
||||
}
|
||||
std::wstring copyRange(size_t count, const std::wstring &wordCandidate,
|
||||
const std::function<bool(const std::wstring &)> &matches,
|
||||
const std::function<bool(const std::wstring &)> &suffixMatches = {})
|
||||
@@ -441,8 +482,11 @@ class Transaction
|
||||
if (!valid())
|
||||
return false;
|
||||
}
|
||||
if (!chord(L'V', MOD_CONTROL))
|
||||
if (!chord(farSnippet ? VK_INSERT : L'V', farSnippet ? MOD_SHIFT : MOD_CONTROL))
|
||||
throw std::runtime_error("Windows blocked simulated input; check application privilege levels");
|
||||
// The selected trigger was replaced. Do not restore its old caret range
|
||||
// if clipboard verification below subsequently rejects a concurrent copy.
|
||||
if (farSnippet) farSelectedSteps = 0;
|
||||
Sleep(300);
|
||||
// A clipboard synchronizer may replace our payload after Ctrl+V is
|
||||
// queued but before the editor consumes it. Do not run snippet actions
|
||||
@@ -1294,14 +1338,16 @@ void App::transform(int command, int snippet)
|
||||
}
|
||||
// Collapse a temporary selection on every failed auto-correction,
|
||||
// including clipboard exceptions, before queued typing is replayed.
|
||||
const bool farSnippet = snippet >= 0 && lower(processName(target)) == L"far";
|
||||
bool autoSelected = false;
|
||||
struct SelectionReset
|
||||
{
|
||||
bool &selected;
|
||||
std::function<bool()> valid;
|
||||
~SelectionReset() { if (selected && valid()) chord(VK_RIGHT); }
|
||||
} selectionReset{autoSelected, valid};
|
||||
Transaction tx(*this, target, version);
|
||||
bool farEditor;
|
||||
~SelectionReset() { if (selected && !farEditor && valid()) chord(VK_RIGHT); }
|
||||
} selectionReset{autoSelected, valid, farSnippet};
|
||||
Transaction tx(*this, target, version, farSnippet);
|
||||
reason = L"selection-unavailable";
|
||||
diagnosticEvent(L"Transform target=" + processName(target) + L" command=" + std::to_wstring(command) +
|
||||
L" recent=" + std::to_wstring(config.recent) + L" keys=" +
|
||||
@@ -1381,6 +1427,15 @@ void App::transform(int command, int snippet)
|
||||
const auto &sn = config.snippets[snippet];
|
||||
if (recent.keys.size() < sn.trigger.size())
|
||||
return;
|
||||
result = parseTemplate(sn.text).text;
|
||||
// A multiline clipboard paste can execute a console command
|
||||
// before readback. Only explicit, verified {enter} may do so.
|
||||
if (farSnippet && (result.empty() || result.find_first_of(L"\r\n") != result.npos ||
|
||||
caretSteps(result) > 4096))
|
||||
{
|
||||
reason = L"far-snippet-template-unsupported";
|
||||
return;
|
||||
}
|
||||
Run tail = recent;
|
||||
tail.keys = tail.keys.substr(tail.keys.size() - sn.trigger.size());
|
||||
if (!tx.select(sn.trigger.size()))
|
||||
@@ -1390,7 +1445,6 @@ void App::transform(int command, int snippet)
|
||||
text = tx.copy();
|
||||
if (tail.match(text, available, physicalLayout()) < 0)
|
||||
return;
|
||||
result = parseTemplate(sn.text).text;
|
||||
automatic = true;
|
||||
}
|
||||
else if (config.recent && recent.window == target && !recent.keys.empty() && !recent.overflow)
|
||||
@@ -1480,9 +1534,10 @@ void App::transform(int command, int snippet)
|
||||
}
|
||||
bool sent = false;
|
||||
const bool requireReadback = tx.recoveredSelection() || snippet >= 0;
|
||||
PasteProbe probe(((config.writeLog && manual) || requireReadback) && result != text, text, target);
|
||||
if (requireReadback && result != text && probe.status != L"ready")
|
||||
PasteProbe probe(!farSnippet && ((config.writeLog && manual) || requireReadback) && result != text, text, target);
|
||||
if (requireReadback && !farSnippet && result != text && probe.status != L"ready")
|
||||
{
|
||||
verification = probe.status;
|
||||
reason = snippet >= 0 ? L"snippet-verification-unavailable" : L"recovered-selection-unavailable";
|
||||
std::lock_guard lock(stateMutex);
|
||||
last.reset();
|
||||
@@ -1494,7 +1549,7 @@ void App::transform(int command, int snippet)
|
||||
if (result != text)
|
||||
sent = tx.paste(textClip(result));
|
||||
else if (automatic && tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
tx.finishSelection(false);
|
||||
if (requireReadback && result != text && !sent)
|
||||
{
|
||||
reason = snippet >= 0 ? L"snippet-paste-cancelled" : L"recovered-selection-paste-cancelled";
|
||||
@@ -1517,11 +1572,12 @@ void App::transform(int command, int snippet)
|
||||
verification = probe.status;
|
||||
if (tx.valid())
|
||||
{
|
||||
auto observed = probe.read((int)caretSteps(result));
|
||||
auto observed = farSnippet ? tx.readFarSnippet(caretSteps(result)) :
|
||||
probe.read((int)caretSteps(result));
|
||||
// A native Edit may silently ignore Ctrl+V while the clipboard is busy.
|
||||
// Retry once only with fresh native proof that the exact original range
|
||||
// is still selected. Never retry an unknown or changed editor state.
|
||||
if (observed && newlines(*observed) == newlines(text) && tx.nativeSelectionIs(text))
|
||||
if (!farSnippet && observed && newlines(*observed) == newlines(text) && tx.nativeSelectionIs(text))
|
||||
{
|
||||
diagnosticEvent(L"Paste retry: native Edit still selects the unchanged original");
|
||||
if (tx.paste(textClip(result)) && tx.valid())
|
||||
@@ -1529,7 +1585,8 @@ void App::transform(int command, int snippet)
|
||||
else
|
||||
observed.reset();
|
||||
}
|
||||
verification = probe.status;
|
||||
verification = farSnippet ? (observed ? L"read-back-via-far-clipboard" :
|
||||
L"far-readback-unavailable") : probe.status;
|
||||
if (observed && tx.valid())
|
||||
{
|
||||
actual = *observed;
|
||||
@@ -1573,6 +1630,7 @@ void App::transform(int command, int snippet)
|
||||
}
|
||||
if (snippet >= 0 && sent && tx.valid())
|
||||
{
|
||||
tx.check();
|
||||
auto t = parseTemplate(config.snippets[snippet].text);
|
||||
if (!t.suffix.empty())
|
||||
{
|
||||
@@ -1581,15 +1639,18 @@ void App::transform(int command, int snippet)
|
||||
if (!tx.valid()) return;
|
||||
if (newlines(suffix) != newlines(t.suffix))
|
||||
{
|
||||
chord(VK_RIGHT);
|
||||
tx.finishSelection(false);
|
||||
return;
|
||||
}
|
||||
if (!chord(VK_LEFT)) return;
|
||||
if (!tx.finishSelection(true)) return;
|
||||
}
|
||||
// Action markers run after insertion and cursor placement,
|
||||
// before any physical typing buffered during expansion.
|
||||
for (int i = 0; i < t.enters && tx.valid(); ++i)
|
||||
{
|
||||
tx.check();
|
||||
if (!chord(VK_RETURN)) break;
|
||||
}
|
||||
}
|
||||
if (to >= 0 && config.switchLayout && tx.valid() && (sent || result == text))
|
||||
{
|
||||
@@ -1761,6 +1822,10 @@ void App::recognizeEntry(std::wstring id)
|
||||
}
|
||||
void App::trayMenu()
|
||||
{
|
||||
constexpr int utilityCommandBase = 1000;
|
||||
std::error_code utilitiesError;
|
||||
auto utilities = discoverUtilities(std::filesystem::path(executable()).parent_path() / L"tools", utilitiesError);
|
||||
if (utilities.size() > 50000) utilities.resize(50000);
|
||||
HMENU menu = CreatePopupMenu();
|
||||
AppendMenuW(menu, MF_STRING, 10, L"Clipboard &history");
|
||||
AppendMenuW(menu, MF_STRING, 24, L"&Capture screen region…");
|
||||
@@ -1768,6 +1833,13 @@ void App::trayMenu()
|
||||
SetMenuDefaultItem(menu, 11, FALSE);
|
||||
AppendMenuW(menu, MF_STRING, 12, L"S&nippets…");
|
||||
AppendMenuW(menu, MF_STRING, 13, L"&Keyboard shortcuts…");
|
||||
HMENU utilityMenu = CreatePopupMenu();
|
||||
for (size_t i = 0; i < utilities.size(); ++i)
|
||||
AppendMenuW(utilityMenu, MF_STRING, utilityCommandBase + i, utilityMenuLabel(utilities[i]).c_str());
|
||||
if (utilities.empty())
|
||||
AppendMenuW(utilityMenu, MF_STRING | MF_GRAYED, 0,
|
||||
utilitiesError ? L"Cannot read tools folder" : L"No utilities in tools folder");
|
||||
AppendMenuW(menu, MF_POPUP, (UINT_PTR)utilityMenu, L"&Utilities");
|
||||
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
|
||||
AppendMenuW(menu, MF_STRING | (settings.awake ? MF_CHECKED : 0), 14, L"Keep screen a&wake");
|
||||
HMENU pause = CreatePopupMenu();
|
||||
@@ -1794,6 +1866,20 @@ void App::trayMenu()
|
||||
PostMessageW(window, WM_NULL, 0, 0);
|
||||
try
|
||||
{
|
||||
if (cmd >= utilityCommandBase && size_t(cmd - utilityCommandBase) < utilities.size())
|
||||
{
|
||||
const auto &path = utilities[cmd - utilityCommandBase];
|
||||
auto command = L"\"" + path.wstring() + L"\"";
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
PROCESS_INFORMATION process{};
|
||||
if (!CreateProcessW(path.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr,
|
||||
path.parent_path().c_str(), &startup, &process))
|
||||
throw std::runtime_error(utf8(lastError(L"Cannot start utility " + path.filename().wstring())));
|
||||
AllowSetForegroundWindow(process.dwProcessId);
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
return;
|
||||
}
|
||||
switch (cmd)
|
||||
{
|
||||
case 24:
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.44";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.45";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cwchar>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ks
|
||||
{
|
||||
// Scan each time the menu opens, relative to the resident executable, not its working directory.
|
||||
inline std::vector<std::filesystem::path> discoverUtilities(const std::filesystem::path &folder,
|
||||
std::error_code &error)
|
||||
{
|
||||
error.clear();
|
||||
std::vector<std::filesystem::path> result;
|
||||
std::filesystem::directory_iterator it(folder, error), end;
|
||||
if (error == std::errc::no_such_file_or_directory) error.clear();
|
||||
while (!error && it != end)
|
||||
{
|
||||
std::error_code entryError;
|
||||
if (_wcsicmp(it->path().extension().c_str(), L".exe") == 0 && it->is_regular_file(entryError))
|
||||
result.push_back(it->path());
|
||||
it.increment(error);
|
||||
}
|
||||
if (error) result.clear();
|
||||
std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) {
|
||||
return _wcsicmp(a.filename().c_str(), b.filename().c_str()) < 0;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
inline std::wstring utilityMenuLabel(const std::filesystem::path &path)
|
||||
{
|
||||
std::wstring label;
|
||||
for (wchar_t ch : path.stem().wstring())
|
||||
{
|
||||
label += ch;
|
||||
if (ch == L'&') label += ch; // Win32 uses ampersands for access keys.
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user