Release v0.3.0: move settings into an on-demand single-instance configurator

This commit is contained in:
2026-10-28 19:51:18 +02:00
parent eb30701e07
commit e5ff9495e4
22 changed files with 620 additions and 45 deletions
+37 -2
View File
@@ -1,6 +1,7 @@
#include "app.h"
#include "../resources/resource.h"
#include "ocr.h"
#include "configuration.h"
#include <algorithm>
#include <commctrl.h>
#include <set>
@@ -398,6 +399,18 @@ void App::reloadLayouts()
}
registerHotkeys(settings);
}
void App::clearHistory()
{
if (busy) throw std::runtime_error("Wait for the current operation to finish.");
if (history) history->clear();
if (historyWindow) refreshHistory(*this);
}
void App::setHotkeyCapture(HWND field)
{
hotkeyCapture = field;
hotkeyCaptureProcess = 0;
if (field) GetWindowThreadProcessId(field, &hotkeyCaptureProcess);
}
bool App::isHotkey(DWORD key, UINT mods)
{
for (auto &[id, text] : shortcuts(settings))
@@ -496,6 +509,10 @@ bool App::handleKeyboard(const KBDLLHOOKSTRUCT &key, bool down, UINT mods)
// down AND up events use exactly the same path as physical keyboard input.
if (ownKeyboardInput(key) || installingUpdate)
return false;
DWORD captureProcess{};
if (hotkeyCapture) GetWindowThreadProcessId(hotkeyCapture, &captureProcess);
if (hotkeyCapture && (!captureProcess || captureProcess != hotkeyCaptureProcess))
setHotkeyCapture(nullptr);
if (hotkeyCapture && GetForegroundWindow() == GetAncestor(hotkeyCapture, GA_ROOT))
{
bool modifier = key.vkCode == VK_CONTROL || key.vkCode == VK_SHIFT || key.vkCode == VK_MENU ||
@@ -579,6 +596,11 @@ void App::dispatch(int command)
showSettings(*this);
return;
}
if (command == 22)
{
showLogs(*this);
return;
}
if (command == 21)
{
showSnippets(*this);
@@ -1129,9 +1151,9 @@ void App::trayMenu()
break;
case 16:
MessageBoxW(window,
L"KSwitcher 0.2.2\nNative C++ / Win32 keyboard and clipboard utility\n\nBased on "
(std::wstring(L"KSwitcher ") + AppVersion + L"\nNative C++ / Win32 keyboard and clipboard utility\n\nBased on "
L"Typlo by bitcodepro. MIT license.\nHistory is encrypted for your Windows "
L"account.\nNo .NET, WebView or graphics framework.",
L"account.\nNo .NET, WebView or graphics framework.").c_str(),
L"About KSwitcher", MB_OK);
break;
case 17:
@@ -1165,6 +1187,11 @@ void App::trayMenu()
void App::checkUpdate(bool interactive, bool install, const std::wstring &version)
{
if (updateRunning) return;
if (install && FindWindowW(L"KSwitcher.Config", nullptr))
{
report(L"Close the configurator before installing an update.");
return;
}
if (install && busy)
{
report(L"Wait for the current text operation to finish, then select Update again.");
@@ -1284,6 +1311,11 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
case WM_COPYDATA:
{
auto *data = (COPYDATASTRUCT *)lp;
if (data && data->dwData >= ConfigRead && data->dwData < ConfigReply)
{
if (app->installingUpdate) return FALSE;
return configurationRequest(*app, (HWND)wp, *data);
}
if (data && data->dwData == 1 && data->cbData == sizeof(int))
{
int cmd;
@@ -1340,6 +1372,9 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
if (auto config = FindWindowW(L"KSwitcher.Config", nullptr);
config && configurationPeer(config, L"KSwitcher.Config.exe"))
PostMessageW(config, WM_CLOSE, 0, 0);
PostQuitMessage(0);
return 0;
default:
+3
View File
@@ -43,6 +43,7 @@ class App
std::atomic<ULONGLONG> pauseUntil{0};
HWND pasteTarget{};
HWND hotkeyCapture{};
DWORD hotkeyCaptureProcess{};
HWND logWindow{};
std::mutex stateMutex;
Run run;
@@ -66,6 +67,8 @@ class App
bool registerHotkeys(const Settings &, bool rollback = false);
void applySettings(const Settings &);
void reloadLayouts();
void clearHistory();
void setHotkeyCapture(HWND);
bool isHotkey(DWORD, UINT);
void keyboard(const KBDLLHOOKSTRUCT &, UINT modifiers);
bool handleKeyboard(const KBDLLHOOKSTRUCT &, bool down, UINT modifiers);
+205
View File
@@ -0,0 +1,205 @@
#include "configuration.h"
#include <commctrl.h>
#include <cstring>
namespace ks
{
App *App::current = nullptr;
namespace
{
HWND service{};
Bytes response;
bool replied{};
Bytes request(App &app, ULONG_PTR command, const Bytes &payload = {})
{
if (!service || !configurationPeer(service, L"KSwitcher.exe"))
throw std::runtime_error("KSwitcher has stopped. Close and reopen the configurator.");
if (payload.size() > ConfigLimit) throw std::runtime_error("Settings are too large.");
response.clear(); replied = false;
COPYDATASTRUCT message{command, (DWORD)payload.size(), const_cast<unsigned char *>(payload.data())};
DWORD_PTR result{};
SetLastError(ERROR_SUCCESS);
if (!SendMessageTimeoutW(service, WM_COPYDATA, (WPARAM)app.window, (LPARAM)&message,
SMTO_ABORTIFHUNG | SMTO_ERRORONEXIT, 15000, &result) || !replied)
{
if (GetLastError() == ERROR_ACCESS_DENIED)
throw std::runtime_error("Open Settings from the KSwitcher tray menu so both processes use the same privileges.");
throw std::runtime_error("KSwitcher did not respond. The operation may still finish; reopen Settings to check.");
}
if (result != 1)
throw std::runtime_error(response.empty() ? "KSwitcher rejected the request." :
std::string(response.begin(), response.end()));
return response;
}
LRESULT CALLBACK controller(HWND window, UINT message, WPARAM wp, LPARAM lp)
{
auto *app = App::current;
if (!app) return DefWindowProcW(window, message, wp, lp);
if (message == WM_COPYDATA)
{
auto *data = reinterpret_cast<COPYDATASTRUCT *>(lp);
if ((HWND)wp == service && data && data->dwData == ConfigReply && data->cbData <= ConfigLimit &&
(!data->cbData || data->lpData))
{
response.clear();
if (data->cbData)
{
auto first = static_cast<const unsigned char *>(data->lpData);
response.assign(first, first + data->cbData);
}
replied = true;
return TRUE;
}
return FALSE;
}
if (message == WM_APP + 20)
{
try
{
if (wp == 21) showSnippets(*app);
else showSettings(*app);
}
catch (const std::exception &e)
{
MessageBoxW(app->settingsWindow, wide(e.what()).c_str(), L"KSwitcher configurator", MB_OK | MB_ICONERROR);
}
return 0;
}
if (message == WM_CLOSE)
{
if (app->settingsWindow) DestroyWindow(app->settingsWindow);
if (app->snippetsWindow) DestroyWindow(app->snippetsWindow);
PostQuitMessage(0);
return 0;
}
return DefWindowProcW(window, message, wp, lp);
}
}
// The configurator owns no history store, dictionaries, worker or input hooks.
// All mutations are validated and committed by the resident process.
App::App(HINSTANCE h, bool, const std::filesystem::path &) : instance(h)
{
current = this;
layouts = readLayouts();
}
App::~App()
{
if (settingsWindow) DestroyWindow(settingsWindow);
if (snippetsWindow) DestroyWindow(snippetsWindow);
if (window) DestroyWindow(window);
current = nullptr;
}
void App::applySettings(const Settings &next)
{
validateSettings(next);
auto before = encodeSettings(settings), after = encodeSettings(next);
DWORD length = (DWORD)before.size();
Bytes payload(sizeof(length));
memcpy(payload.data(), &length, sizeof(length));
payload.insert(payload.end(), before.begin(), before.end());
payload.insert(payload.end(), after.begin(), after.end());
settings = decodeSettings(request(*this, ConfigSave, payload), layouts);
}
void App::reloadLayouts()
{
auto bytes = request(*this, ConfigLayouts);
layouts = readLayouts();
settings = decodeSettings(bytes, layouts);
}
void App::clearHistory() { request(*this, ConfigClearHistory); }
void App::setHotkeyCapture(HWND field)
{
hotkeyCapture = field;
Bytes payload(sizeof(field));
memcpy(payload.data(), &field, sizeof(field));
// Focus notifications must not let an exception escape a window callback.
try { request(*this, ConfigCapture, payload); }
catch (const std::exception &e)
{
hotkeyCapture = nullptr;
if (field) MessageBoxW(settingsWindow, wide(e.what()).c_str(), L"Shortcut capture unavailable", MB_OK | MB_ICONWARNING);
}
}
void showLogs(App &app) { request(app, ConfigLogs); }
int runConfigurator(HINSTANCE instance, bool snippets)
{
HANDLE singleton = CreateMutexW(nullptr, TRUE, L"Local\\KSwitcher.Config.Singleton");
if (!singleton) throw std::runtime_error("Cannot create configurator instance lock.");
const bool existing = GetLastError() == ERROR_ALREADY_EXISTS;
struct Close { HANDLE handle; ~Close() { CloseHandle(handle); } } close{singleton};
if (existing)
{
HWND window{};
for (int i = 0; i < 100 && !window; ++i)
{
window = FindWindowW(L"KSwitcher.Config", nullptr);
if (!window) Sleep(50);
}
if (!window || !configurationPeer(window, L"KSwitcher.Config.exe"))
throw std::runtime_error("The configurator is starting or belongs to another installation. Try again shortly.");
DWORD pid{}; GetWindowThreadProcessId(window, &pid);
AllowSetForegroundWindow(pid);
PostMessageW(window, WM_APP + 20, snippets ? 21 : 20, 0);
return 0;
}
service = FindWindowW(L"KSwitcher.Main", L"KSwitcher");
if (!service)
{
auto path = std::filesystem::path(executable()).parent_path() / L"KSwitcher.exe";
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("Cannot start KSwitcher beside the configurator.");
CloseHandle(process.hThread); CloseHandle(process.hProcess);
for (int i = 0; i < 100 && !service; ++i)
{
Sleep(50); service = FindWindowW(L"KSwitcher.Main", L"KSwitcher");
}
}
if (!configurationPeer(service, L"KSwitcher.exe"))
throw std::runtime_error("Run the configurator from the active KSwitcher installation.");
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_STANDARD_CLASSES};
InitCommonControlsEx(&controls);
App app(instance);
WNDCLASSW cls{}; cls.hInstance = instance; cls.lpfnWndProc = controller; cls.lpszClassName = L"KSwitcher.Config";
RegisterClassW(&cls);
app.window = CreateWindowExW(WS_EX_TOOLWINDOW, cls.lpszClassName, L"KSwitcher configurator", WS_POPUP,
0, 0, 0, 0, nullptr, nullptr, instance, nullptr);
if (!app.window) throw std::runtime_error("Cannot create configurator window.");
app.settings = decodeSettings(request(app, ConfigRead), app.layouts);
if (snippets) showSnippets(app); else showSettings(app);
MSG message;
while ((app.settingsWindow || app.snippetsWindow) && GetMessageW(&message, nullptr, 0, 0) > 0)
{
if (app.settingsWindow && !IsWindowEnabled(app.settingsWindow))
{
HWND popup = GetLastActivePopup(app.settingsWindow);
if (popup != app.settingsWindow && IsDialogMessageW(popup, &message)) continue;
}
if (app.settingsWindow && IsDialogMessageW(app.settingsWindow, &message)) continue;
if (app.snippetsWindow && IsDialogMessageW(app.snippetsWindow, &message)) continue;
TranslateMessage(&message); DispatchMessageW(&message);
}
return 0;
}
}
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int)
{
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
int result = 1;
try
{
int count{}; auto args = CommandLineToArgvW(GetCommandLineW(), &count);
bool snippets = count > 1 && std::wstring(args[1]) == L"--snippets";
LocalFree(args);
result = ks::runConfigurator(instance, snippets);
}
catch (const std::exception &e)
{
MessageBoxW(nullptr, ks::wide(e.what()).c_str(), L"KSwitcher configurator", MB_OK | MB_ICONERROR);
}
CoUninitialize();
return result;
}
+98
View File
@@ -0,0 +1,98 @@
#include "configuration.h"
#include <cstring>
namespace ks
{
bool configurationPeer(HWND window, const wchar_t *filename)
{
DWORD pid{};
GetWindowThreadProcessId(window, &pid);
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!process) return false;
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;
}
void launchConfiguration(App &app, bool snippets)
{
auto path = std::filesystem::path(executable()).parent_path() / L"KSwitcher.Config.exe";
if (!std::filesystem::exists(path))
throw std::runtime_error("KSwitcher.Config.exe is missing. Extract the complete application package.");
auto command = L"\"" + path.wstring() + L"\"" + (snippets ? L" --snippets" : 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 configurator")));
AllowSetForegroundWindow(process.dwProcessId);
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
(void)app;
}
LRESULT configurationRequest(App &app, HWND sender, const COPYDATASTRUCT &message)
{
if (!configurationPeer(sender, L"KSwitcher.Config.exe") || message.cbData > ConfigLimit ||
(message.cbData && !message.lpData)) return FALSE;
Bytes reply;
bool ok = false;
try
{
switch (message.dwData)
{
case ConfigSave:
{
if (app.busy) throw std::runtime_error("Wait for the current text operation to finish.");
if (message.cbData < sizeof(DWORD)) throw std::runtime_error("Invalid settings request.");
DWORD length{};
memcpy(&length, message.lpData, sizeof(length));
if (length > message.cbData - sizeof(length)) throw std::runtime_error("Invalid settings snapshot.");
auto begin = static_cast<const unsigned char *>(message.lpData) + sizeof(length);
if (Bytes(begin, begin + length) != encodeSettings(app.settings))
throw std::runtime_error("Settings changed in KSwitcher. Close and reopen the configurator before saving.");
Bytes next(begin + length, static_cast<const unsigned char *>(message.lpData) + message.cbData);
if (next.empty()) throw std::runtime_error("Empty settings request.");
app.applySettings(decodeSettings(next, app.layouts));
reply = encodeSettings(app.settings);
break;
}
case ConfigLayouts:
if (app.busy) throw std::runtime_error("Wait for the current text operation to finish.");
app.reloadLayouts();
[[fallthrough]];
case ConfigRead:
reply = encodeSettings(app.settings);
break;
case ConfigClearHistory:
app.clearHistory();
break;
case ConfigCapture:
{
if (message.cbData != sizeof(HWND)) throw std::runtime_error("Invalid shortcut field.");
HWND field{}; memcpy(&field, message.lpData, sizeof(field));
DWORD fieldPid{}, senderPid{};
GetWindowThreadProcessId(field, &fieldPid);
GetWindowThreadProcessId(sender, &senderPid);
if (field && fieldPid != senderPid) throw std::runtime_error("Invalid shortcut field owner.");
app.setHotkeyCapture(field);
break;
}
case ConfigLogs:
PostMessageW(app.window, WM_HOTKEY, 22, 0);
break;
default: return FALSE;
}
ok = true;
}
catch (const std::exception &error)
{
std::string text = error.what();
reply.assign(text.begin(), text.end());
}
COPYDATASTRUCT response{ConfigReply, (DWORD)reply.size(), reply.data()};
DWORD_PTR received{};
if (!SendMessageTimeoutW(sender, WM_COPYDATA, (WPARAM)app.window, (LPARAM)&response,
SMTO_ABORTIFHUNG, 2000, &received)) return FALSE;
return ok ? 1 : 2;
}
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include "app.h"
namespace ks
{
inline constexpr ULONG_PTR ConfigRead = 0x4b530100, ConfigSave = ConfigRead + 1,
ConfigLayouts = ConfigRead + 2, ConfigClearHistory = ConfigRead + 3,
ConfigCapture = ConfigRead + 4, ConfigLogs = ConfigRead + 5,
ConfigReply = ConfigRead + 6;
inline constexpr size_t ConfigLimit = 8 * 1024 * 1024;
bool configurationPeer(HWND, const wchar_t *filename);
void launchConfiguration(App &, bool snippets = false);
LRESULT configurationRequest(App &, HWND sender, const COPYDATASTRUCT &);
}
+11 -3
View File
@@ -102,6 +102,10 @@ void validateSettings(const Settings &s)
}
}
Settings loadSettings(const std::filesystem::path &path, const std::vector<Layout> &layouts)
{
return decodeSettings(std::filesystem::exists(path) ? readFile(path, 4 * 1024 * 1024) : Bytes{}, layouts);
}
Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
{
Settings s;
for (size_t i = 0; i < layouts.size(); ++i)
@@ -111,9 +115,9 @@ Settings loadSettings(const std::filesystem::path &path, const std::vector<Layou
s.direct.push_back(i < 3 ? L"Ctrl+Alt+Shift+" + std::to_wstring(i + 1) : L"");
s.directIds.push_back(layouts[i].id);
}
if (!std::filesystem::exists(path))
if (bytes.empty())
return s;
auto j = parse(readFile(path, 4 * 1024 * 1024));
auto j = parse(bytes);
auto format = j.value("format", std::string());
if (format != "kswitcher-settings" && format != "typlo-settings")
throw std::runtime_error("Not a KSwitcher or Typlo settings file");
@@ -177,6 +181,10 @@ Settings loadSettings(const std::filesystem::path &path, const std::vector<Layou
return s;
}
void saveSettings(const std::filesystem::path &path, const Settings &s)
{
atomicWrite(path, encodeSettings(s));
}
Bytes encodeSettings(const Settings &s)
{
validateSettings(s);
Json j = {{"format", "kswitcher-settings"},
@@ -221,7 +229,7 @@ void saveSettings(const std::filesystem::path &path, const Settings &s)
j["excludedApps"] = Json::array();
for (auto &name : s.excluded)
j["excludedApps"].push_back(utf8(name));
atomicWrite(path, jsonBytes(j));
return jsonBytes(j);
}
Bytes encodeClip(const Clip &clip)
{
+2
View File
@@ -4,6 +4,8 @@
namespace ks
{
Settings loadSettings(const std::filesystem::path &, const std::vector<Layout> &);
Settings decodeSettings(const Bytes &, const std::vector<Layout> &);
Bytes encodeSettings(const Settings &);
void saveSettings(const std::filesystem::path &, const Settings &);
void validateSettings(const Settings &);
struct Entry
+26 -10
View File
@@ -1,5 +1,6 @@
#include "../resources/resource.h"
#include "app.h"
#include "configuration.h"
#include "ocr.h"
#include "richtext.h"
#include "ui_geometry.h"
@@ -185,13 +186,13 @@ LRESULT CALLBACK hotkeyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DW
auto *app = (App *)data;
if (msg == WM_SETFOCUS)
{
app->hotkeyCapture = w;
app->setHotkeyCapture(w);
SendMessageW(w, EM_SETSEL, 0, -1);
}
if (msg == WM_KILLFOCUS || msg == WM_NCDESTROY)
{
if (app->hotkeyCapture == w)
app->hotkeyCapture = nullptr;
app->setHotkeyCapture(nullptr);
}
if (msg == CaptureHotkeyMessage)
{
@@ -218,13 +219,13 @@ LRESULT CALLBACK hotkeyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DW
return 0;
return DefSubclassProc(w, msg, wp, lp);
}
void hotkeyEdit(App &app, HWND w, int id, int x, int y, int width)
[[maybe_unused]] void hotkeyEdit(App &app, HWND w, int id, int x, int y, int width)
{
HWND field = edit(w, id, x, y, width, 25, ES_AUTOHSCROLL | ES_READONLY);
SendMessageW(field, EM_SETCUEBANNER, FALSE, (LPARAM)L"Click, then press a shortcut");
SetWindowSubclass(field, hotkeyProc, 1, (DWORD_PTR)&app);
}
void checkbox(HWND w, const wchar_t *text, int id, int x, int y, int width = 300)
[[maybe_unused]] void checkbox(HWND w, const wchar_t *text, int id, int x, int y, int width = 300)
{
control(w, L"BUTTON", text, WS_TABSTOP | BS_AUTOCHECKBOX, id, x, y, width, 24);
}
@@ -241,11 +242,11 @@ void value(HWND w, int id, const std::wstring &text)
{
SetDlgItemTextW(w, id, text.c_str());
}
bool checked(HWND w, int id)
[[maybe_unused]] bool checked(HWND w, int id)
{
return IsDlgButtonChecked(w, id) == BST_CHECKED;
}
void check(HWND w, int id, bool yes)
[[maybe_unused]] void check(HWND w, int id, bool yes)
{
CheckDlgButton(w, id, yes ? BST_CHECKED : BST_UNCHECKED);
}
@@ -352,7 +353,7 @@ void resizeForDpi(HWND w, WPARAM wp, LPARAM lp)
fitToMonitor(w, *(RECT *)lp);
arrangeWindow(w);
}
std::filesystem::path pickFile(HWND owner, bool save)
[[maybe_unused]] std::filesystem::path pickFile(HWND owner, bool save)
{
wchar_t path[32768]{};
OPENFILENAMEW ofn{sizeof(ofn)};
@@ -372,6 +373,7 @@ void disposeFont(HWND w)
DeleteObject(h);
}
#ifndef KS_CONFIG_APP
struct SearchResult
{
uint64_t generation;
@@ -755,6 +757,8 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
return DefWindowProcW(w, msg, wp, lp);
}
#endif
#ifndef KS_RESIDENT_APP
struct SettingsView
{
App *app{};
@@ -1146,11 +1150,11 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
fillSettings(w, v);
return 0;
}
if (id == 254 && v->app->history)
if (id == 254)
{
if (MessageBoxW(w, L"Delete all clipboard history, including pinned items?", L"Clear history",
MB_YESNO | MB_ICONWARNING) == IDYES)
v->app->history->clear();
v->app->clearHistory();
return 0;
}
break;
@@ -1294,6 +1298,8 @@ LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
}
return DefWindowProcW(w, msg, wp, lp);
}
#endif
#ifndef KS_CONFIG_APP
LRESULT CALLBACK entryProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
if (msg == WM_SIZE)
@@ -1305,7 +1311,9 @@ LRESULT CALLBACK entryProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
disposeFont(w);
return DefWindowProcW(w, msg, wp, lp);
}
#endif
} // namespace
#ifndef KS_CONFIG_APP
void refreshHistory(App &app)
{
if (app.historyWindow)
@@ -1498,7 +1506,7 @@ void refreshLogView(HWND w, App &app)
InvalidateRect(list, nullptr, TRUE);
if (row)
ListView_EnsureVisible(list, row - 1, FALSE);
value(w, LogStatus, app.settings.writeLog ? L"Logging enabled: problems and suspicious repeats only."
value(w, LogStatus, app.settings.writeLog ? L"Logging enabled: failed operations only."
: L"Logging disabled. Showing previously saved entries.");
}
LRESULT CALLBACK logProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
@@ -1609,6 +1617,8 @@ void showLogs(App &app)
showRequestedWindow(w);
activateWindow(w);
}
#endif
#ifndef KS_RESIDENT_APP
void showSettings(App &app, bool visible)
{
if (app.settingsWindow)
@@ -1699,6 +1709,11 @@ void showSnippets(App &app)
showRequestedWindow(w);
SetForegroundWindow(w);
}
#else
void showSettings(App &app, bool) { launchConfiguration(app); }
void showSnippets(App &app) { launchConfiguration(app, true); }
#endif
#ifndef KS_CONFIG_APP
void showShortcuts(App &app)
{
auto &s = app.settings;
@@ -1736,4 +1751,5 @@ void showEntry(App &app, const std::wstring &id)
error(app.historyWindow, e);
}
}
#endif
} // namespace ks
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.2.3";
inline constexpr wchar_t AppVersion[] = L"0.3.0";
inline constexpr UINT UpdateMessage = WM_APP + 7;
struct UpdateResult
{