Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a830f55575 | |||
| 07e31228d7 | |||
| 39d3876fd2 |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.6.13 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.6.16 LANGUAGES CXX RC)
|
||||||
if(NOT WIN32)
|
if(NOT WIN32)
|
||||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -224,7 +224,10 @@ thumbnails and source labels aligned with the scaled fonts and spacing.
|
|||||||
- Clipboard snapshot/restoration with concurrent-copy, foreground and input-version checks.
|
- Clipboard snapshot/restoration with concurrent-copy, foreground and input-version checks.
|
||||||
- Case cycling, Russian/Ukrainian transliteration, verified undo and repeated conversion.
|
- Case cycling, Russian/Ukrainian transliteration, verified undo and repeated conversion.
|
||||||
- Text snippets across layouts, multiline insertion and a verified `{cursor}` position;
|
- Text snippets across layouts, multiline insertion and a verified `{cursor}` position;
|
||||||
`{{cursor}}` inserts the literal marker. Snippets are stored as plaintext settings, as in [Typlo](https://github.com/bitcodepro/Typlo).
|
`{enter}` presses Enter after insertion and cursor placement (once per marker), before buffered typing resumes.
|
||||||
|
For example, `rider .{enter}` inserts `rider .` and presses Enter.
|
||||||
|
Automatic correction is suppressed during snippet expansion and for typing buffered by that expansion; the global setting is preserved.
|
||||||
|
`{{cursor}}` and `{{enter}}` insert literal markers. Snippets are stored as plaintext settings, as in [Typlo](https://github.com/bitcodepro/Typlo).
|
||||||
- Clipboard history for text, HTML/RTF, file lists, DIB and PNG images; plain-text paste;
|
- Clipboard history for text, HTML/RTF, file lists, DIB and PNG images; plain-text paste;
|
||||||
full-text search, pinning, deletion, full text viewing and image previews.
|
full-text search, pinning, deletion, full text viewing and image previews.
|
||||||
- Image thumbnails and text/file icons appear directly in the history list. Only visible rows
|
- Image thumbnails and text/file icons appear directly in the history list. Only visible rows
|
||||||
|
|||||||
Binary file not shown.
+10
-5
@@ -1,14 +1,19 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.13 — 2026-10-30
|
## v0.6.16 — 2026-10-30
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
- Correct words containing punctuation keys that become letters in the target layout, such as `hj,bnm` → `робить`. Validate letters and capitalization after conversion so personal dictionary entries can match.
|
- Fix repeated offers to install the same release: the runtime updater version now matches the packaged executable version.
|
||||||
- Extend automatic word selection across internal punctuation only while the selected text matches a suffix of the tracked input. Verify the complete tracked word before replacing it.
|
- Add regression checks comparing updater, executable and CMake versions, and rejecting the already-installed modern release.
|
||||||
- Give the fixed-layout automatic-correction settings dialog a fixed window size, retaining scrolling on small displays instead of allowing empty space when resized.
|
- Preserve typing that arrives while a snippet is being expanded.
|
||||||
|
- Add `{enter}` to press Enter after insertion and cursor placement, with an Insert Enter marker button. Use `{{enter}}` for literal text.
|
||||||
|
- Suppress automatic correction during snippet expansion and buffered typing, without changing the global setting. Normal typing resumes automatic correction afterward.
|
||||||
|
- Restore the caret when snippet verification fails and record snippet failures in diagnostic logs.
|
||||||
|
|
||||||
|
Includes the earlier automatic-correction and log-viewer improvements. Existing modern releases update directly to this version without installing intermediate releases.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
220 native regression checks, 466 input/UI checks, screenshot editor and notification registration checks, 45 updater checks and updater progress checks passed. Native editor integration verified continuous typing during correction and complete selection/conversion of `hj,bnm` to `робить`. The correction was also confirmed in the user's editor.
|
223 native regression checks, 472 input/UI checks, screenshot editor and notification registration checks, 48 updater checks and updater progress checks. Targeted native integration checks cover continuous snippet typing, clipboard restoration, actual Enter key events, punctuation preservation, suppression of automatic correction and its resumption afterward. The updated development build was also confirmed by the user in Notepad. The broader integration suite showed intermittent failures during earlier interactive runs; it is not claimed as fully passing.
|
||||||
|
|
||||||
**Upgrade:** choose Update in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
**Upgrade:** choose Update in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
||||||
For manual installation, exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
|
For manual installation, exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
eae70031520882cc6a798e11a640fa101df01db12aeba4e6c96d8aa90037ae1e KSwitcher-win-x64.zip
|
65c5d4c41c0b062eab29e5c100fd5cbff3839d808907935160cf7f5b904af6b7 KSwitcher-win-x64.zip
|
||||||
|
|||||||
+3
-3
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
|
|||||||
1 RT_MANIFEST "app.manifest"
|
1 RT_MANIFEST "app.manifest"
|
||||||
201 RCDATA "update.ps1"
|
201 RCDATA "update.ps1"
|
||||||
1 VERSIONINFO
|
1 VERSIONINFO
|
||||||
FILEVERSION 0,6,13,0
|
FILEVERSION 0,6,16,0
|
||||||
PRODUCTVERSION 0,6,13,0
|
PRODUCTVERSION 0,6,16,0
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
FILETYPE VFT_APP
|
FILETYPE VFT_APP
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -20,7 +20,7 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
||||||
VALUE "ProductName", "KSwitcher"
|
VALUE "ProductName", "KSwitcher"
|
||||||
VALUE "FileVersion", "0.6.13"
|
VALUE "FileVersion", "0.6.16"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
+59
-18
@@ -12,6 +12,11 @@ namespace ks
|
|||||||
App *App::current = nullptr;
|
App *App::current = nullptr;
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
ULONG_PTR snippetReplayMarker()
|
||||||
|
{
|
||||||
|
static const char marker{};
|
||||||
|
return reinterpret_cast<ULONG_PTR>(&marker);
|
||||||
|
}
|
||||||
std::vector<std::pair<int, std::wstring>> shortcuts(const Settings &s)
|
std::vector<std::pair<int, std::wstring>> shortcuts(const Settings &s)
|
||||||
{
|
{
|
||||||
std::vector<std::pair<int, std::wstring>> out{{Toggle, s.toggle},
|
std::vector<std::pair<int, std::wstring>> out{{Toggle, s.toggle},
|
||||||
@@ -566,7 +571,8 @@ void App::keyboard(const KBDLLHOOKSTRUCT &k, UINT mods)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
run.record(ch, fg, layout, GetTickCount64());
|
run.record(ch, fg, layout, GetTickCount64());
|
||||||
if (settings.autoCorrect && mods == 0 && ch == L' ' && run.keys.size() >= (size_t)settings.autoMinLength + 1 &&
|
if (settings.autoCorrect && !snippetTyping && k.dwExtraInfo != snippetReplayMarker() &&
|
||||||
|
mods == 0 && ch == L' ' && run.keys.size() >= (size_t)settings.autoMinLength + 1 &&
|
||||||
run.keys.size() <= 41 && !busy && !run.overflow)
|
run.keys.size() <= 41 && !busy && !run.overflow)
|
||||||
{
|
{
|
||||||
for (int source = 0; source < (int)layouts.size(); ++source)
|
for (int source = 0; source < (int)layouts.size(); ++source)
|
||||||
@@ -619,7 +625,18 @@ void App::keyboard(const KBDLLHOOKSTRUCT &k, UINT mods)
|
|||||||
for (auto &trigger : variants)
|
for (auto &trigger : variants)
|
||||||
if (keys.size() >= trigger.size() && keys.ends_with(trigger))
|
if (keys.size() >= trigger.size() && keys.ends_with(trigger))
|
||||||
{
|
{
|
||||||
PostMessageW(window, SnippetMessage, (WPARAM)i, 0);
|
// Keep subsequent typing out of the selection/paste transaction.
|
||||||
|
typingTarget = fg;
|
||||||
|
snippetPending = (int)i;
|
||||||
|
snippetTyping = true;
|
||||||
|
busy = true;
|
||||||
|
if (!PostMessageW(window, SnippetMessage, (WPARAM)i, (LPARAM)inputVersion.load()))
|
||||||
|
{
|
||||||
|
typingTarget = nullptr;
|
||||||
|
snippetPending = -1;
|
||||||
|
snippetTyping = false;
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -693,12 +710,13 @@ bool App::handleKeyboard(const KBDLLHOOKSTRUCT &key, bool down, UINT mods)
|
|||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
bool App::beginTypingDeferral(HWND target, uint64_t version)
|
bool App::beginTypingDeferral(HWND target, uint64_t version, bool snippet)
|
||||||
{
|
{
|
||||||
std::lock_guard lock(stateMutex);
|
std::lock_guard lock(stateMutex);
|
||||||
if (inputVersion != version || GetForegroundWindow() != target || !modifiersReleased())
|
if (inputVersion != version || GetForegroundWindow() != target || !modifiersReleased())
|
||||||
return false;
|
return false;
|
||||||
typingTarget = target;
|
typingTarget = target;
|
||||||
|
snippetTyping = snippet;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
void App::replayTyping()
|
void App::replayTyping()
|
||||||
@@ -710,7 +728,12 @@ void App::replayTyping()
|
|||||||
target = typingTarget;
|
target = typingTarget;
|
||||||
typingTarget = nullptr;
|
typingTarget = nullptr;
|
||||||
autoPending = false;
|
autoPending = false;
|
||||||
|
snippetPending = -1;
|
||||||
inputs.swap(deferredTyping);
|
inputs.swap(deferredTyping);
|
||||||
|
if (snippetTyping)
|
||||||
|
for (auto &input : inputs)
|
||||||
|
input.ki.dwExtraInfo = snippetReplayMarker();
|
||||||
|
snippetTyping = false;
|
||||||
}
|
}
|
||||||
busy = false;
|
busy = false;
|
||||||
if (inputs.empty())
|
if (inputs.empty())
|
||||||
@@ -826,14 +849,19 @@ void App::transform(int command, int snippet)
|
|||||||
L" version=" + std::to_wstring(currentVersion) +
|
L" version=" + std::to_wstring(currentVersion) +
|
||||||
L" repeat=" + std::to_wstring(repeatAttempts));
|
L" repeat=" + std::to_wstring(repeatAttempts));
|
||||||
}
|
}
|
||||||
const bool preDeferred = command == AutoCorrect && autoPending;
|
const bool preDeferred = (command == AutoCorrect && autoPending) ||
|
||||||
|
(snippet >= 0 && snippetPending == snippet);
|
||||||
if (busy.exchange(true) && !preDeferred)
|
if (busy.exchange(true) && !preDeferred)
|
||||||
{
|
{
|
||||||
if (manual)
|
if (manual)
|
||||||
diagnosticEvent(L"Transform outcome id=" + std::to_wstring(attempt) + L" reason=busy", true);
|
diagnosticEvent(L"Transform outcome id=" + std::to_wstring(attempt) + L" reason=busy", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (preDeferred) autoPending = false;
|
if (preDeferred)
|
||||||
|
{
|
||||||
|
autoPending = false;
|
||||||
|
snippetPending = -1;
|
||||||
|
}
|
||||||
Run recent;
|
Run recent;
|
||||||
std::optional<LastTransform> previous;
|
std::optional<LastTransform> previous;
|
||||||
Settings config;
|
Settings config;
|
||||||
@@ -904,7 +932,7 @@ void App::transform(int command, int snippet)
|
|||||||
reason != L"paste-sent-unverified" && reason != L"layout-only" &&
|
reason != L"paste-sent-unverified" && reason != L"layout-only" &&
|
||||||
reason != L"unchanged-expected");
|
reason != L"unchanged-expected");
|
||||||
}
|
}
|
||||||
} audit{*this, target, version, attempt, requestedAt, manual, reason, text, result, expected, hint, to};
|
} audit{*this, target, version, attempt, requestedAt, manual || snippet >= 0, reason, text, result, expected, hint, to};
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
auto valid = [&]
|
auto valid = [&]
|
||||||
@@ -946,9 +974,9 @@ void App::transform(int command, int snippet)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reason = L"clipboard-snapshot";
|
reason = L"clipboard-snapshot";
|
||||||
if (command == AutoCorrect)
|
if (command == AutoCorrect || snippet >= 0)
|
||||||
{
|
{
|
||||||
if (!beginTypingDeferral(target, version))
|
if (!beginTypingDeferral(target, version, snippet >= 0))
|
||||||
return;
|
return;
|
||||||
reset.deferred = true;
|
reset.deferred = true;
|
||||||
}
|
}
|
||||||
@@ -1043,13 +1071,11 @@ void App::transform(int command, int snippet)
|
|||||||
tail.keys = tail.keys.substr(tail.keys.size() - sn.trigger.size());
|
tail.keys = tail.keys.substr(tail.keys.size() - sn.trigger.size());
|
||||||
if (!tx.select(sn.trigger.size()))
|
if (!tx.select(sn.trigger.size()))
|
||||||
return;
|
return;
|
||||||
|
autoSelected = true;
|
||||||
|
reason = L"snippet-verification-mismatch";
|
||||||
text = tx.copy();
|
text = tx.copy();
|
||||||
if (tail.match(text, available, physicalLayout()) < 0)
|
if (tail.match(text, available, physicalLayout()) < 0)
|
||||||
{
|
|
||||||
if (tx.valid())
|
|
||||||
chord(VK_RIGHT);
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
result = parseTemplate(sn.text).text;
|
result = parseTemplate(sn.text).text;
|
||||||
automatic = true;
|
automatic = true;
|
||||||
}
|
}
|
||||||
@@ -1162,12 +1188,22 @@ void App::transform(int command, int snippet)
|
|||||||
if (snippet >= 0 && sent && tx.valid())
|
if (snippet >= 0 && sent && tx.valid())
|
||||||
{
|
{
|
||||||
auto t = parseTemplate(config.snippets[snippet].text);
|
auto t = parseTemplate(config.snippets[snippet].text);
|
||||||
if (!t.suffix.empty() && tx.select(caretSteps(t.suffix)))
|
if (!t.suffix.empty())
|
||||||
{
|
{
|
||||||
|
if (!tx.select(caretSteps(t.suffix))) return;
|
||||||
auto suffix = tx.copy();
|
auto suffix = tx.copy();
|
||||||
if (tx.valid())
|
if (!tx.valid()) return;
|
||||||
chord(newlines(suffix) == newlines(t.suffix) ? VK_LEFT : VK_RIGHT);
|
if (newlines(suffix) != newlines(t.suffix))
|
||||||
|
{
|
||||||
|
chord(VK_RIGHT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!chord(VK_LEFT)) 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)
|
||||||
|
if (!chord(VK_RETURN)) break;
|
||||||
}
|
}
|
||||||
if (to >= 0 && config.switchLayout && tx.valid() && (sent || result == text))
|
if (to >= 0 && config.switchLayout && tx.valid() && (sent || result == text))
|
||||||
{
|
{
|
||||||
@@ -1517,15 +1553,20 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
case AutoCorrectMessage:
|
case AutoCorrectMessage:
|
||||||
if (app->autoPending)
|
if (app->autoPending)
|
||||||
{
|
{
|
||||||
if (!app->installingUpdate && app->settings.autoCorrect && app->inputVersion.load() == (uint64_t)wp)
|
if (!app->installingUpdate && !app->snippetTyping && app->settings.autoCorrect && app->inputVersion.load() == (uint64_t)wp)
|
||||||
app->transform(AutoCorrect);
|
app->transform(AutoCorrect);
|
||||||
else
|
else
|
||||||
app->replayTyping();
|
app->replayTyping();
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
case SnippetMessage:
|
case SnippetMessage:
|
||||||
if (app->installingUpdate) return 0;
|
if (app->snippetPending == (int)wp)
|
||||||
app->transform(0, (int)wp);
|
{
|
||||||
|
if (!app->installingUpdate && app->inputVersion.load() == (uint64_t)lp)
|
||||||
|
app->transform(0, (int)wp);
|
||||||
|
else
|
||||||
|
app->replayTyping();
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
case HistoryMessage:
|
case HistoryMessage:
|
||||||
if (app->historyWindow && IsWindowVisible(app->historyWindow))
|
if (app->historyWindow && IsWindowVisible(app->historyWindow))
|
||||||
|
|||||||
@@ -109,8 +109,10 @@ class App
|
|||||||
HotkeyHook hotkeyHook;
|
HotkeyHook hotkeyHook;
|
||||||
HWND typingTarget{};
|
HWND typingTarget{};
|
||||||
bool autoPending{};
|
bool autoPending{};
|
||||||
|
int snippetPending = -1;
|
||||||
|
bool snippetTyping{};
|
||||||
std::vector<INPUT> deferredTyping;
|
std::vector<INPUT> deferredTyping;
|
||||||
bool beginTypingDeferral(HWND, uint64_t);
|
bool beginTypingDeferral(HWND, uint64_t, bool snippet = false);
|
||||||
void replayTyping();
|
void replayTyping();
|
||||||
static LRESULT CALLBACK windowProc(HWND, UINT, WPARAM, LPARAM);
|
static LRESULT CALLBACK windowProc(HWND, UINT, WPARAM, LPARAM);
|
||||||
static LRESULT CALLBACK keyProc(int, WPARAM, LPARAM);
|
static LRESULT CALLBACK keyProc(int, WPARAM, LPARAM);
|
||||||
|
|||||||
+11
-1
@@ -261,7 +261,17 @@ Template parseTemplate(const std::wstring &text)
|
|||||||
size_t cursor = std::wstring::npos;
|
size_t cursor = std::wstring::npos;
|
||||||
for (size_t i = 0; i < text.size();)
|
for (size_t i = 0; i < text.size();)
|
||||||
{
|
{
|
||||||
if (text.compare(i, 10, L"{{cursor}}\0", 10) == 0)
|
if (text.compare(i, 9, L"{{enter}}") == 0)
|
||||||
|
{
|
||||||
|
t.text += L"{enter}";
|
||||||
|
i += 9;
|
||||||
|
}
|
||||||
|
else if (text.compare(i, 7, L"{enter}") == 0)
|
||||||
|
{
|
||||||
|
++t.enters;
|
||||||
|
i += 7;
|
||||||
|
}
|
||||||
|
else if (text.compare(i, 10, L"{{cursor}}\0", 10) == 0)
|
||||||
{
|
{
|
||||||
t.text += L"{cursor}";
|
t.text += L"{cursor}";
|
||||||
i += 10;
|
i += 10;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ struct Template
|
|||||||
{
|
{
|
||||||
std::wstring text, suffix;
|
std::wstring text, suffix;
|
||||||
int cursors = 0;
|
int cursors = 0;
|
||||||
|
int enters = 0;
|
||||||
};
|
};
|
||||||
Template parseTemplate(const std::wstring &);
|
Template parseTemplate(const std::wstring &);
|
||||||
struct Hotkey
|
struct Hotkey
|
||||||
|
|||||||
+35
-5
@@ -35,6 +35,7 @@ struct WindowLayout
|
|||||||
int wheelRemainder{}, horizontalWheelRemainder{};
|
int wheelRemainder{}, horizontalWheelRemainder{};
|
||||||
std::vector<std::pair<HWND, RECT>> controls; // Original 96-DPI geometry; never rescale rounded pixels.
|
std::vector<std::pair<HWND, RECT>> controls; // Original 96-DPI geometry; never rescale rounded pixels.
|
||||||
HWND stretch{}, bottomLeft{}, bottomRight{};
|
HWND stretch{}, bottomLeft{}, bottomRight{};
|
||||||
|
bool autoOptions{};
|
||||||
};
|
};
|
||||||
WindowLayout *windowLayoutState(HWND w)
|
WindowLayout *windowLayoutState(HWND w)
|
||||||
{
|
{
|
||||||
@@ -70,7 +71,7 @@ void arrangeWindow(HWND w)
|
|||||||
layout->y = SetScrollInfo(w, SB_VERT, &vertical, TRUE);
|
layout->y = SetScrollInfo(w, SB_VERT, &vertical, TRUE);
|
||||||
}
|
}
|
||||||
RECT client{}; GetClientRect(w, &client);
|
RECT client{}; GetClientRect(w, &client);
|
||||||
bool expandable = layout->history || layout->stretch;
|
bool expandable = layout->history || layout->stretch || layout->autoOptions;
|
||||||
int extraWidth = expandable ? std::max(0, int(client.right) - scale(w, layout->width)) : 0;
|
int extraWidth = expandable ? std::max(0, int(client.right) - scale(w, layout->width)) : 0;
|
||||||
int extraHeight = expandable ? std::max(0, int(client.bottom) - scale(w, layout->height)) : 0;
|
int extraHeight = expandable ? std::max(0, int(client.bottom) - scale(w, layout->height)) : 0;
|
||||||
struct Move { HWND child; RECT rect; };
|
struct Move { HWND child; RECT rect; };
|
||||||
@@ -89,6 +90,28 @@ void arrangeWindow(HWND w)
|
|||||||
}
|
}
|
||||||
if (child == layout->bottomLeft || child == layout->bottomRight)
|
if (child == layout->bottomLeft || child == layout->bottomRight)
|
||||||
OffsetRect(&next, child == layout->bottomRight ? extraWidth : 0, extraHeight);
|
OffsetRect(&next, child == layout->bottomRight ? extraWidth : 0, extraHeight);
|
||||||
|
if (layout->autoOptions)
|
||||||
|
{
|
||||||
|
if (r.top >= 396) OffsetRect(&next, 0, extraHeight);
|
||||||
|
if (r.top == 302 || r.top == 328 || (id >= 401 && id <= 403))
|
||||||
|
{
|
||||||
|
int column = id >= 401 && id <= 403 ? id - 401 : (r.left - 18) / 178;
|
||||||
|
int leftExtra = extraWidth * column / 3;
|
||||||
|
OffsetRect(&next, leftExtra, 0);
|
||||||
|
next.right += extraWidth * (column + 1) / 3 - leftExtra;
|
||||||
|
if (r.top == 328) next.bottom += extraHeight;
|
||||||
|
}
|
||||||
|
else if (r.top == 474 || r.top == 500)
|
||||||
|
{
|
||||||
|
bool rightColumn = r.left >= 290;
|
||||||
|
OffsetRect(&next, rightColumn ? extraWidth / 2 : 0, 0);
|
||||||
|
next.right += rightColumn ? extraWidth - extraWidth / 2 : extraWidth / 2;
|
||||||
|
}
|
||||||
|
else if (id == 406 || id == 410 || id == IDOK || id == IDCANCEL)
|
||||||
|
OffsetRect(&next, extraWidth, 0);
|
||||||
|
else if (r.right - r.left >= 500)
|
||||||
|
next.right += extraWidth;
|
||||||
|
}
|
||||||
RECT current{}; GetWindowRect(child, ¤t);
|
RECT current{}; GetWindowRect(child, ¤t);
|
||||||
MapWindowPoints(nullptr, w, reinterpret_cast<POINT *>(¤t), 2);
|
MapWindowPoints(nullptr, w, reinterpret_cast<POINT *>(¤t), 2);
|
||||||
if (!EqualRect(¤t, &next)) moves.push_back({child, next});
|
if (!EqualRect(¤t, &next)) moves.push_back({child, next});
|
||||||
@@ -1072,7 +1095,8 @@ LRESULT CALLBACK autoOptionsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
void showAutoOptions(HWND parent, SettingsView *v)
|
void showAutoOptions(HWND parent, SettingsView *v)
|
||||||
{
|
{
|
||||||
HWND w = makeWindow(*v->app, L"KSwitcher.AutoOptions", L"Automatic layout correction", autoOptionsProc,
|
HWND w = makeWindow(*v->app, L"KSwitcher.AutoOptions", L"Automatic layout correction", autoOptionsProc,
|
||||||
560, 680, 0, false);
|
560, 680);
|
||||||
|
windowLayoutState(w)->autoOptions = true;
|
||||||
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
|
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
|
||||||
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR) new AutoOptionsView{parent, v});
|
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR) new AutoOptionsView{parent, v});
|
||||||
label(w, L"Offline dictionaries (only layouts in your cycle are used)", 18, 16, 525);
|
label(w, L"Offline dictionaries (only layouts in your cycle are used)", 18, 16, 525);
|
||||||
@@ -1113,6 +1137,11 @@ void showAutoOptions(HWND parent, SettingsView *v)
|
|||||||
label(w, L"After editing files, click OK here, then Save in Settings to reload.", 18, 600, 525);
|
label(w, L"After editing files, click OK here, then Save in Settings to reload.", 18, 600, 525);
|
||||||
button(w, L"OK", IDOK, 326, 632, 100);
|
button(w, L"OK", IDOK, 326, 632, 100);
|
||||||
button(w, L"Cancel", IDCANCEL, 440, 632, 100);
|
button(w, L"Cancel", IDCANCEL, 440, 632, 100);
|
||||||
|
RECT initial{};
|
||||||
|
GetWindowRect(w, &initial);
|
||||||
|
initial.right += scale(w, 200);
|
||||||
|
initial.bottom += scale(w, 140);
|
||||||
|
fitToMonitor(w, initial);
|
||||||
EnableWindow(parent, FALSE);
|
EnableWindow(parent, FALSE);
|
||||||
showRequestedWindow(w);
|
showRequestedWindow(w);
|
||||||
SetForegroundWindow(w);
|
SetForegroundWindow(w);
|
||||||
@@ -1419,9 +1448,9 @@ LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (id == 312)
|
if (id == 312 || id == 313)
|
||||||
{
|
{
|
||||||
SendDlgItemMessageW(w, 303, EM_REPLACESEL, TRUE, (LPARAM)L"{cursor}");
|
SendDlgItemMessageW(w, 303, EM_REPLACESEL, TRUE, (LPARAM)(id == 312 ? L"{cursor}" : L"{enter}"));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (id == IDOK)
|
if (id == IDOK)
|
||||||
@@ -1871,8 +1900,9 @@ void showSnippets(App &app)
|
|||||||
edit(w, 302, 236, 44, 488);
|
edit(w, 302, 236, 44, 488);
|
||||||
label(w, L"Replacement text", 236, 82, 480);
|
label(w, L"Replacement text", 236, 82, 480);
|
||||||
edit(w, 303, 236, 110, 488, 270, ES_MULTILINE | ES_WANTRETURN | ES_AUTOVSCROLL | WS_VSCROLL);
|
edit(w, 303, 236, 110, 488, 270, ES_MULTILINE | ES_WANTRETURN | ES_AUTOVSCROLL | WS_VSCROLL);
|
||||||
label(w, L"Use {cursor} once; {{cursor}} inserts the literal marker.", 236, 392, 488);
|
control(w, L"STATIC", L"{cursor}: caret; {enter}: press Enter after insertion.\nUse {{cursor}} / {{enter}} for literal markers.", 0, 0, 236, 392, 488, 36);
|
||||||
button(w, L"Insert cursor marker", 312, 236, 430, 190);
|
button(w, L"Insert cursor marker", 312, 236, 430, 190);
|
||||||
|
button(w, L"Insert Enter marker", 313, 436, 430, 190);
|
||||||
button(w, L"Save", IDOK, 510, 478, 100);
|
button(w, L"Save", IDOK, 510, 478, 100);
|
||||||
button(w, L"Cancel", IDCANCEL, 624, 478, 100);
|
button(w, L"Cancel", IDCANCEL, 624, 478, 100);
|
||||||
fillSnippets(w, (SnippetView *)GetWindowLongPtrW(w, GWLP_USERDATA));
|
fillSnippets(w, (SnippetView *)GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.6.13";
|
inline constexpr wchar_t AppVersion[] = L"0.6.16";
|
||||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||||
|
|||||||
@@ -598,6 +598,35 @@ int main(int argc, char **argv) {
|
|||||||
auto autoOptions=FindWindowW(L"KSwitcher.AutoOptions",nullptr);
|
auto autoOptions=FindWindowW(L"KSwitcher.AutoOptions",nullptr);
|
||||||
require(autoOptions!=nullptr,"auto-correction options open");
|
require(autoOptions!=nullptr,"auto-correction options open");
|
||||||
ShowWindow(autoOptions,SW_HIDE);
|
ShowWindow(autoOptions,SW_HIDE);
|
||||||
|
{
|
||||||
|
require((GetWindowLongPtrW(autoOptions,GWL_STYLE)&WS_THICKFRAME)!=0,
|
||||||
|
"auto-correction dialog supports resizing");
|
||||||
|
RECT original{}, before[3]{}, after[3]{}, buttonBefore{}, buttonAfter{};
|
||||||
|
GetWindowRect(autoOptions,&original);
|
||||||
|
// Use a client size above the scrolling baseline at every DPI.
|
||||||
|
UINT dpi=GetDpiForWindow(autoOptions);
|
||||||
|
SetWindowPos(autoOptions,nullptr,0,0,MulDiv(800,dpi,96),MulDiv(900,dpi,96),
|
||||||
|
SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
|
||||||
|
RECT baseline{}; GetWindowRect(autoOptions,&baseline);
|
||||||
|
for(int i=0;i<3;++i) GetWindowRect(GetDlgItem(autoOptions,411+i),&before[i]);
|
||||||
|
GetWindowRect(GetDlgItem(autoOptions,IDOK),&buttonBefore);
|
||||||
|
SetWindowPos(autoOptions,nullptr,0,0,baseline.right-baseline.left+240,
|
||||||
|
baseline.bottom-baseline.top+120,SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
|
||||||
|
for(int i=0;i<3;++i) {
|
||||||
|
GetWindowRect(GetDlgItem(autoOptions,411+i),&after[i]);
|
||||||
|
require(after[i].right-after[i].left==before[i].right-before[i].left+80 &&
|
||||||
|
after[i].bottom-after[i].top==before[i].bottom-before[i].top+120,
|
||||||
|
"dictionary columns share extra width and receive extra height");
|
||||||
|
}
|
||||||
|
GetWindowRect(GetDlgItem(autoOptions,IDOK),&buttonAfter);
|
||||||
|
require(buttonAfter.left-buttonBefore.left==240 && buttonAfter.top-buttonBefore.top==120,
|
||||||
|
"auto-correction buttons follow bottom-right corner");
|
||||||
|
require(after[0].right<after[1].left && after[1].right<after[2].left &&
|
||||||
|
after[2].bottom<buttonAfter.top,
|
||||||
|
"resized dictionary fields and footer do not overlap");
|
||||||
|
SetWindowPos(autoOptions,nullptr,0,0,original.right-original.left,original.bottom-original.top,
|
||||||
|
SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
|
||||||
|
}
|
||||||
for(int id:{411,412,413}) {
|
for(int id:{411,412,413}) {
|
||||||
RECT control{},client{};
|
RECT control{},client{};
|
||||||
GetWindowRect(GetDlgItem(autoOptions,id),&control);
|
GetWindowRect(GetDlgItem(autoOptions,id),&control);
|
||||||
|
|||||||
+77
-2
@@ -21,6 +21,12 @@ ks::App *app{};
|
|||||||
HWND host{}, field{};
|
HWND host{}, field{};
|
||||||
ks::Clip saved;
|
ks::Clip saved;
|
||||||
bool captured = false, failed = false;
|
bool captured = false, failed = false;
|
||||||
|
int enterPresses = 0;
|
||||||
|
LRESULT CALLBACK observeEnter(HWND w, UINT message, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
|
||||||
|
{
|
||||||
|
if (message == WM_KEYDOWN && wp == VK_RETURN) ++enterPresses;
|
||||||
|
return DefSubclassProc(w, message, wp, lp);
|
||||||
|
}
|
||||||
int stage = 0, ru = -1, en = -1;
|
int stage = 0, ru = -1, en = -1;
|
||||||
size_t punctuationCase = 0;
|
size_t punctuationCase = 0;
|
||||||
bool typeDuringCorrection = false, typedDuringCorrection = false;
|
bool typeDuringCorrection = false, typedDuringCorrection = false;
|
||||||
@@ -281,7 +287,73 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
break;
|
break;
|
||||||
case 17:
|
case 17:
|
||||||
require(text() == punctuationCases.back().first, "undo preserves custom punctuation");
|
require(text() == punctuationCases.back().first, "undo preserves custom punctuation");
|
||||||
[[fallthrough]];
|
stage = 20;
|
||||||
|
break;
|
||||||
|
case 30:
|
||||||
|
case 32:
|
||||||
|
case 34:
|
||||||
|
if (!captured)
|
||||||
|
{
|
||||||
|
saved = ks::readClipboard(app->window, 64 * 1024 * 1024);
|
||||||
|
captured = true;
|
||||||
|
}
|
||||||
|
app->ownSequence = ks::writeClipboard(app->window, ks::textClip(sentinel));
|
||||||
|
app->settings.autoCorrect = stage == 35;
|
||||||
|
app->settings.autoLanguages = {L"en", L"ru"};
|
||||||
|
app->autoPredictor.configure(L"dictionaries", app->settings.autoLanguages);
|
||||||
|
app->settings.snippets = {{L";r", stage == 31 ? L"rider .{cursor}" : L"rider .{cursor}{enter}"}};
|
||||||
|
enterPresses = 0;
|
||||||
|
ActivateKeyboardLayout(app->layouts[en].handle, 0);
|
||||||
|
prepare(L"prefix ", false);
|
||||||
|
{
|
||||||
|
std::vector<INPUT> inputs;
|
||||||
|
const std::vector<WORD> keys = stage == 35
|
||||||
|
? std::vector<WORD>{VK_OEM_1, 'R', 'G', 'H', 'B', 'D', 'T', 'N', VK_SPACE}
|
||||||
|
: std::vector<WORD>{VK_OEM_1, 'R', VK_SPACE, 'X'};
|
||||||
|
for (WORD key : keys)
|
||||||
|
for (DWORD flags : {DWORD(0), DWORD(KEYEVENTF_KEYUP)})
|
||||||
|
{
|
||||||
|
auto input = ks::keyboardInput(key, flags);
|
||||||
|
input.ki.dwExtraInfo = 123;
|
||||||
|
inputs.push_back(input);
|
||||||
|
}
|
||||||
|
require(SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT)) == inputs.size(),
|
||||||
|
"snippet trigger and following text submitted without a pause");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 31:
|
||||||
|
require(text() == L"prefix rider . x", "snippet expands through keyboard hook and preserves following typing");
|
||||||
|
require(ks::readClipboard(app->window).text() == sentinel, "hook snippet restores clipboard");
|
||||||
|
require(enterPresses == 0, "snippet without Enter marker sends no Enter");
|
||||||
|
stage = 32;
|
||||||
|
break;
|
||||||
|
case 33:
|
||||||
|
require(text() == L"prefix rider .\r\n x", "Enter action precedes buffered typing");
|
||||||
|
require(enterPresses == 1, "Enter marker sends an actual Enter key press");
|
||||||
|
require(ks::readClipboard(app->window).text() == sentinel, "Enter snippet restores clipboard");
|
||||||
|
stage = 34;
|
||||||
|
break;
|
||||||
|
case 35:
|
||||||
|
require(text() == L"prefix rider .\r\nghbdtn ", "snippet replay does not trigger automatic correction");
|
||||||
|
require(app->settings.autoCorrect, "snippet preserves global auto-correction setting");
|
||||||
|
require(enterPresses == 1, "snippet punctuation and Enter survive with auto-correction enabled");
|
||||||
|
{
|
||||||
|
std::vector<INPUT> inputs;
|
||||||
|
for (WORD key : std::wstring(L"GHBDTN "))
|
||||||
|
for (DWORD flags : {DWORD(0), DWORD(KEYEVENTF_KEYUP)})
|
||||||
|
{
|
||||||
|
auto input = ks::keyboardInput(key == L' ' ? VK_SPACE : key, flags);
|
||||||
|
input.ki.dwExtraInfo = 123;
|
||||||
|
inputs.push_back(input);
|
||||||
|
}
|
||||||
|
require(SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT)) == inputs.size(),
|
||||||
|
"normal typing submitted after snippet completes");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 36:
|
||||||
|
require(text() == L"prefix rider .\r\nghbdtn привет ", "automatic correction resumes for subsequent normal typing");
|
||||||
|
finish();
|
||||||
|
break;
|
||||||
case 20:
|
case 20:
|
||||||
if (!captured)
|
if (!captured)
|
||||||
{
|
{
|
||||||
@@ -353,7 +425,7 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
case 22:
|
case 22:
|
||||||
require(text() == L"робить ", "automatic correction selects and converts the whole word across an internal comma");
|
require(text() == L"робить ", "automatic correction selects and converts the whole word across an internal comma");
|
||||||
finish();
|
stage = 30;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,8 +549,11 @@ int main(int argc, char **argv)
|
|||||||
CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_MULTILINE | ES_WANTRETURN,
|
CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_MULTILINE | ES_WANTRETURN,
|
||||||
20, 20, 590, 230, host, nullptr, cls.hInstance, nullptr);
|
20, 20, 590, 230, host, nullptr, cls.hInstance, nullptr);
|
||||||
deadline = GetTickCount64() + 30000;
|
deadline = GetTickCount64() + 30000;
|
||||||
|
SetWindowSubclass(field, observeEnter, 1, 0);
|
||||||
if (argc > 1 && std::string(argv[1]) == "--typing-only")
|
if (argc > 1 && std::string(argv[1]) == "--typing-only")
|
||||||
stage = 20;
|
stage = 20;
|
||||||
|
if (argc > 1 && std::string(argv[1]) == "--snippets-only")
|
||||||
|
stage = 30;
|
||||||
SetTimer(host, 1, 100, nullptr);
|
SetTimer(host, 1, 100, nullptr);
|
||||||
app->execute();
|
app->execute();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -301,6 +301,12 @@ int main(int argc, char **argv)
|
|||||||
require(ks::transliterate(L"Shchuka") == L"Щука", "reverse transliteration");
|
require(ks::transliterate(L"Shchuka") == L"Щука", "reverse transliteration");
|
||||||
require(ks::parseTemplate(L"a{cursor}b").suffix == L"b", "snippet suffix");
|
require(ks::parseTemplate(L"a{cursor}b").suffix == L"b", "snippet suffix");
|
||||||
require(ks::parseTemplate(L"{{cursor}}").text == L"{cursor}", "escaped cursor");
|
require(ks::parseTemplate(L"{{cursor}}").text == L"{cursor}", "escaped cursor");
|
||||||
|
require(ks::parseTemplate(L"rider .{enter}").text == L"rider ." &&
|
||||||
|
ks::parseTemplate(L"rider .{enter}").enters == 1, "Enter is an action, not inserted text");
|
||||||
|
require(ks::parseTemplate(L"a{cursor}b{enter}{enter}").suffix == L"b" &&
|
||||||
|
ks::parseTemplate(L"a{cursor}b{enter}{enter}").enters == 2, "Enter actions preserve cursor suffix");
|
||||||
|
require(ks::parseTemplate(L"{{enter}}").text == L"{enter}" &&
|
||||||
|
ks::parseTemplate(L"{{enter}}").enters == 0, "escaped Enter is literal");
|
||||||
require(ks::parseTemplate(L"{cursor}{cursor}").cursors == 2, "duplicate cursor detection");
|
require(ks::parseTemplate(L"{cursor}{cursor}").cursors == 2, "duplicate cursor detection");
|
||||||
require(ks::Hotkey::parse(L"Ctrl+Alt+Shift+Space")->key == VK_SPACE, "hotkey parse");
|
require(ks::Hotkey::parse(L"Ctrl+Alt+Shift+Space")->key == VK_SPACE, "hotkey parse");
|
||||||
require(!ks::Hotkey::parse(L"Ctrl+F25"), "invalid function key");
|
require(!ks::Hotkey::parse(L"Ctrl+F25"), "invalid function key");
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ try {
|
|||||||
$binaryInfo = [Diagnostics.FileVersionInfo]::GetVersionInfo((Resolve-Path -LiteralPath $Executable).Path)
|
$binaryInfo = [Diagnostics.FileVersionInfo]::GetVersionInfo((Resolve-Path -LiteralPath $Executable).Path)
|
||||||
$binaryVersion = Get-StableVersion $binaryInfo.FileVersion
|
$binaryVersion = Get-StableVersion $binaryInfo.FileVersion
|
||||||
Assert ($null -ne $binaryVersion) 'executable has a stable version'
|
Assert ($null -ne $binaryVersion) 'executable has a stable version'
|
||||||
|
$header = Get-Content -LiteralPath (Join-Path $PSScriptRoot '../src/update.h') -Raw
|
||||||
|
$match = [regex]::Match($header, 'AppVersion\[\]\s*=\s*L"([0-9.]+)"')
|
||||||
|
Assert ($match.Success -and (Get-StableVersion $match.Groups[1].Value) -eq $binaryVersion) 'updater runtime version matches packaged executable'
|
||||||
|
$cmake = Get-Content -LiteralPath (Join-Path $PSScriptRoot '../CMakeLists.txt') -Raw
|
||||||
|
$projectVersion = [regex]::Match($cmake, 'project\(KSwitcher VERSION ([0-9.]+)')
|
||||||
|
Assert ($projectVersion.Success -and (Get-StableVersion $projectVersion.Groups[1].Value) -eq $binaryVersion) 'CMake version matches packaged executable'
|
||||||
Assert ($binaryInfo.FileDescription -ceq ('KSwitcher ' + [char]0x2014 + ' native keyboard and clipboard utility')) 'executable description preserves Unicode em dash'
|
Assert ($binaryInfo.FileDescription -ceq ('KSwitcher ' + [char]0x2014 + ' native keyboard and clipboard utility')) 'executable description preserves Unicode em dash'
|
||||||
Assert ((Get-StableVersion 'v0.10.0') -gt (Get-StableVersion 'v0.2.9')) 'numeric versions'
|
Assert ((Get-StableVersion 'v0.10.0') -gt (Get-StableVersion 'v0.2.9')) 'numeric versions'
|
||||||
Assert ($null -eq (Get-StableVersion 'v1.0.0-beta')) 'prerelease rejected'
|
Assert ($null -eq (Get-StableVersion 'v1.0.0-beta')) 'prerelease rejected'
|
||||||
@@ -24,6 +30,8 @@ try {
|
|||||||
Assert ($null -eq (Select-Release @($release) ([version]'0.3.0'))) 'no downgrade'
|
Assert ($null -eq (Select-Release @($release) ([version]'0.3.0'))) 'no downgrade'
|
||||||
Assert ((Get-StableVersion 'release-v0.5.0') -eq [version]'0.5.0') 'modern stable release tag'
|
Assert ((Get-StableVersion 'release-v0.5.0') -eq [version]'0.5.0') 'modern stable release tag'
|
||||||
$modern=@{draft=$false;prerelease=$false;tag_name='release-v0.5.0';assets=$assets}
|
$modern=@{draft=$false;prerelease=$false;tag_name='release-v0.5.0';assets=$assets}
|
||||||
|
$installedRelease=@{draft=$false;prerelease=$false;tag_name=('release-v'+$binaryInfo.FileVersion);assets=$assets}
|
||||||
|
Assert ($null -eq (Select-Release @($installedRelease) (Get-StableVersion $match.Groups[1].Value))) 'runtime updater does not offer the installed modern release again'
|
||||||
Assert ((Select-Release @($release,$modern) ([version]'0.4.2')).version -eq 'release-v0.5.0') 'bridge discovers modern channel'
|
Assert ((Select-Release @($release,$modern) ([version]'0.4.2')).version -eq 'release-v0.5.0') 'bridge discovers modern channel'
|
||||||
$draft=@{draft=$true;prerelease=$false;tag_name='v9.0.0';assets=$assets}
|
$draft=@{draft=$true;prerelease=$false;tag_name='v9.0.0';assets=$assets}
|
||||||
Assert ((Select-Release @($draft,$release) ([version]'0.2.0')).version -eq 'v0.2.1') 'draft ignored'
|
Assert ((Select-Release @($draft,$release) ([version]'0.2.0')).version -eq 'v0.2.1') 'draft ignored'
|
||||||
|
|||||||
Reference in New Issue
Block a user