dsh-mini 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/app/HudWindow.cs +4 -1
- package/app/SelectionWatcher.cs +142 -24
- package/app/Service.cs +9 -2
- package/app/bin/DSH/345/260/217/347/252/227.exe +0 -0
- package/lib/client.js +2 -0
- package/lib/index.js +5 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -177,14 +177,17 @@ cd app
|
|
|
177
177
|
在**任何程序**里用鼠标选中一段文字,松开后光标右下方会弹出蓝色小药丸「问大肥鱼」;点它,
|
|
178
178
|
小窗自动弹到最前,选中的内容以引用块(每行 `> ` 前缀)插进输入框,直接接着提问即可。
|
|
179
179
|
|
|
180
|
-
* 实现在 `app/SelectionWatcher.cs
|
|
180
|
+
* 实现在 `app/SelectionWatcher.cs`:全局鼠标轮询(90ms)→ 松开时若拖动距离 ≥6 逻辑像素、
|
|
181
181
|
且不在小窗/药丸自己身上 → 后台线程等 130ms(双击选词等 320ms)→ 校验仍在前台 → 取选区。
|
|
182
|
-
*
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
*
|
|
186
|
-
|
|
187
|
-
|
|
182
|
+
* **取选区走系统辅助功能(UIA `TextPattern`),不碰键盘也不碰剪贴板**:拿到文本的同时还能拿到
|
|
183
|
+
选区最后一行/一个字的矩形,药丸才能精确贴在它右下角。绝大多数现代程序(Notepad、Chrome/Edge、
|
|
184
|
+
Word、WPS、VS Code、DSH 自己)都支持。
|
|
185
|
+
* 只有用户显式打开设置里的**兼容模式**,UIA 读不到时才补一次读剪贴板:先给焦点控件发 `WM_COPY`
|
|
186
|
+
(不合成按键),仍不行才真的发一次 `Ctrl+C`。**任何情况下都不再做「备份 → 复制 → 还原」**——
|
|
187
|
+
之前那套会把你自己的 `Ctrl+C` 覆盖掉,这正是"选中后复制不上"的元凶。
|
|
188
|
+
* 保护:按住 Ctrl/Alt/Shift 时的拖动直接跳过(那是复制/扩选,不是重新选词);终端
|
|
189
|
+
(console/cascadia/mintty/putty)不发按键;资源管理器/桌面/任务栏(Progman/WorkerW/CabinetWClass…)
|
|
190
|
+
一律跳过(否则会真的复制文件、污染 `Win+V` 历史);用户按着修饰键期间完全不介入。
|
|
188
191
|
* 药丸 9 秒无操作自动消失;托盘菜单「划词问大肥鱼」可随时关掉,状态存在
|
|
189
192
|
`%LOCALAPPDATA%\dsh-hud\ask.txt`。
|
|
190
193
|
* 因此**标题条 ✕ 只是收进托盘**(不是退出)——watcher 活在这个进程里,退出了就没法划词。
|
|
@@ -222,6 +225,8 @@ DSH小窗.exe --service --url <tokenized> # Mutex Local\DshHudService,单
|
|
|
222
225
|
主窗「设置 → DSH 小窗」:
|
|
223
226
|
|
|
224
227
|
* 截图自动复制到剪贴板(默认开,整屏/框选都复制);
|
|
228
|
+
* 兼容模式:读不到选区时模拟 `Ctrl+C`(**默认关**。关着时只用系统辅助功能读选区,
|
|
229
|
+
完全不碰剪贴板、不发按键;打开后老式控件才读得到,代价是可能和自己按的 `Ctrl+C` 抢剪贴板);
|
|
225
230
|
* 全屏截图快捷键、框选截图快捷键(失焦即保存)。
|
|
226
231
|
|
|
227
232
|
配置落在 `%LOCALAPPDATA%\dsh-hud\config.ini`(`key=value`,C# 壳、宿主插件、客户端三边都读),
|
package/app/HudWindow.cs
CHANGED
|
@@ -228,6 +228,7 @@ namespace DshHud
|
|
|
228
228
|
|
|
229
229
|
config = HudConfig.Load();
|
|
230
230
|
configStamp = HudConfig.Stamp();
|
|
231
|
+
SelectionReader.ClipboardFallback = config.TextFallback;
|
|
231
232
|
}
|
|
232
233
|
|
|
233
234
|
private Size Logical(int w, int h)
|
|
@@ -567,7 +568,9 @@ namespace DshHud
|
|
|
567
568
|
{
|
|
568
569
|
configStamp = stamp;
|
|
569
570
|
config = HudConfig.Load();
|
|
570
|
-
|
|
571
|
+
SelectionReader.ClipboardFallback = config.TextFallback;
|
|
572
|
+
Log("config reloaded copyShot=" + config.CopyShot +
|
|
573
|
+
" textFallback=" + (config.TextFallback ? "1" : "0"));
|
|
571
574
|
}
|
|
572
575
|
|
|
573
576
|
string quote = HudConfig.QuotePath;
|
package/app/SelectionWatcher.cs
CHANGED
|
@@ -52,15 +52,92 @@ namespace DshHud
|
|
|
52
52
|
|
|
53
53
|
internal const uint GA_ROOT = 2;
|
|
54
54
|
internal const int VK_LBUTTON = 0x01;
|
|
55
|
-
internal const
|
|
55
|
+
internal const int VK_SHIFT = 0x10;
|
|
56
|
+
internal const int VK_CONTROL = 0x11;
|
|
57
|
+
internal const int VK_MENU = 0x12;
|
|
56
58
|
internal const byte VK_C = 0x43;
|
|
57
59
|
internal const uint KEYEVENTF_KEYUP = 0x0002;
|
|
58
60
|
|
|
61
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
62
|
+
internal struct GUITHREADINFO
|
|
63
|
+
{
|
|
64
|
+
public int cbSize;
|
|
65
|
+
public uint flags;
|
|
66
|
+
public IntPtr hwndActive;
|
|
67
|
+
public IntPtr hwndFocus;
|
|
68
|
+
public IntPtr hwndCapture;
|
|
69
|
+
public IntPtr hwndMenuOwner;
|
|
70
|
+
public IntPtr hwndMoveSize;
|
|
71
|
+
public IntPtr hwndCaret;
|
|
72
|
+
public RECT rcCaret;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
76
|
+
internal struct RECT
|
|
77
|
+
{
|
|
78
|
+
public int left;
|
|
79
|
+
public int top;
|
|
80
|
+
public int right;
|
|
81
|
+
public int bottom;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
[DllImport("user32.dll")]
|
|
85
|
+
internal static extern bool GetGUIThreadInfo(uint idThread, ref GUITHREADINFO info);
|
|
86
|
+
|
|
87
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
|
88
|
+
internal static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint msg, IntPtr wParam,
|
|
89
|
+
IntPtr lParam, uint flags, uint timeout, out IntPtr result);
|
|
90
|
+
|
|
91
|
+
internal const uint WM_COPY = 0x0301;
|
|
92
|
+
internal const uint SMTO_ABORTIFHUNG = 0x0002;
|
|
93
|
+
|
|
59
94
|
internal static bool IsMouseDown()
|
|
60
95
|
{
|
|
61
96
|
return (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0;
|
|
62
97
|
}
|
|
63
98
|
|
|
99
|
+
internal static bool IsKeyDown(int vk)
|
|
100
|
+
{
|
|
101
|
+
return (GetAsyncKeyState(vk) & 0x8000) != 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// <summary>
|
|
105
|
+
/// True while the user holds Ctrl/Alt/Shift himself. Every probe is skipped then:
|
|
106
|
+
/// injecting our own Ctrl+C on top of a real one is what made the user's copies
|
|
107
|
+
/// land empty, and it can also cancel a drag-copy in Explorer.
|
|
108
|
+
/// </summary>
|
|
109
|
+
internal static bool IsModifierDown()
|
|
110
|
+
{
|
|
111
|
+
return IsKeyDown(VK_CONTROL) || IsKeyDown(VK_MENU) || IsKeyDown(VK_SHIFT);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// <summary>
|
|
115
|
+
/// The control that currently has the keyboard focus. WM_COPY has to be delivered to
|
|
116
|
+
/// the edit control, not the top-level window.
|
|
117
|
+
/// </summary>
|
|
118
|
+
internal static IntPtr FocusedControl()
|
|
119
|
+
{
|
|
120
|
+
try
|
|
121
|
+
{
|
|
122
|
+
IntPtr fg = GetForegroundWindow();
|
|
123
|
+
if (fg == IntPtr.Zero) return IntPtr.Zero;
|
|
124
|
+
uint pid;
|
|
125
|
+
uint tid = GetWindowThreadProcessId(fg, out pid);
|
|
126
|
+
GUITHREADINFO gti = new GUITHREADINFO();
|
|
127
|
+
gti.cbSize = Marshal.SizeOf(typeof(GUITHREADINFO));
|
|
128
|
+
if (GetGUIThreadInfo(tid, ref gti))
|
|
129
|
+
{
|
|
130
|
+
if (gti.hwndFocus != IntPtr.Zero) return gti.hwndFocus;
|
|
131
|
+
if (gti.hwndActive != IntPtr.Zero) return gti.hwndActive;
|
|
132
|
+
}
|
|
133
|
+
return fg;
|
|
134
|
+
}
|
|
135
|
+
catch
|
|
136
|
+
{
|
|
137
|
+
return IntPtr.Zero;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
64
141
|
/// <summary>
|
|
65
142
|
/// Terminals map Ctrl+C to an interrupt rather than to "copy", so the clipboard
|
|
66
143
|
/// fallback must never fire over one of them.
|
|
@@ -187,6 +264,28 @@ namespace DshHud
|
|
|
187
264
|
return false;
|
|
188
265
|
}
|
|
189
266
|
|
|
267
|
+
/// <summary>
|
|
268
|
+
/// Ask the focused control to copy through WM_COPY. This is the polite fallback:
|
|
269
|
+
/// no keystrokes are synthesised, so a real Ctrl+C pressed by the user is never
|
|
270
|
+
/// raced or cancelled, and nothing is written back over the clipboard.
|
|
271
|
+
/// </summary>
|
|
272
|
+
internal static bool SendCopyMessage()
|
|
273
|
+
{
|
|
274
|
+
try
|
|
275
|
+
{
|
|
276
|
+
IntPtr target = FocusedControl();
|
|
277
|
+
if (target == IntPtr.Zero) return false;
|
|
278
|
+
IntPtr result;
|
|
279
|
+
SendMessageTimeout(target, WM_COPY, IntPtr.Zero, IntPtr.Zero,
|
|
280
|
+
SMTO_ABORTIFHUNG, 300, out result);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
catch
|
|
284
|
+
{
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
190
289
|
internal static void SendCopy()
|
|
191
290
|
{
|
|
192
291
|
keybd_event(VK_CONTROL, 0, 0, UIntPtr.Zero);
|
|
@@ -218,6 +317,14 @@ namespace DshHud
|
|
|
218
317
|
|
|
219
318
|
internal static class SelectionReader
|
|
220
319
|
{
|
|
320
|
+
/// <summary>
|
|
321
|
+
/// Off by default. UIA answers for every modern text surface without touching the
|
|
322
|
+
/// keyboard or the clipboard; the copy-based path is only needed by legacy controls,
|
|
323
|
+
/// and it necessarily competes with the user's own Ctrl+C. Users can enable it from
|
|
324
|
+
/// the settings page.
|
|
325
|
+
/// </summary>
|
|
326
|
+
internal static bool ClipboardFallback;
|
|
327
|
+
|
|
221
328
|
/// <summary>
|
|
222
329
|
/// Read the current selection. UIA first: it is passive (no keystrokes, no clipboard
|
|
223
330
|
/// churn) and it also tells us where the selection ends, which is what the pill
|
|
@@ -275,7 +382,7 @@ namespace DshHud
|
|
|
275
382
|
{
|
|
276
383
|
}
|
|
277
384
|
|
|
278
|
-
hit.Text = Read();
|
|
385
|
+
if (ClipboardFallback) hit.Text = Read();
|
|
279
386
|
return hit;
|
|
280
387
|
}
|
|
281
388
|
|
|
@@ -338,16 +445,23 @@ namespace DshHud
|
|
|
338
445
|
|
|
339
446
|
private static string ReadOnStaThread()
|
|
340
447
|
{
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
{
|
|
348
|
-
}
|
|
448
|
+
// No clipboard backup/restore any more. Restoring was the actual damage: while
|
|
449
|
+
// we waited for our own copy to land, the user's real Ctrl+C could complete
|
|
450
|
+
// first, and our restore then threw that copy away ("复制不上"). Now the worst
|
|
451
|
+
// case is that the selection text stays on the clipboard — which is exactly
|
|
452
|
+
// what the user just asked for anyway.
|
|
453
|
+
if (ShellWin.IsModifierDown()) return "";
|
|
349
454
|
|
|
350
455
|
uint seq = ShellWin.GetClipboardSequenceNumber();
|
|
456
|
+
bool asked = ShellWin.SendCopyMessage();
|
|
457
|
+
if (!asked) return "";
|
|
458
|
+
|
|
459
|
+
string text = WaitForClipboardText(seq, 320);
|
|
460
|
+
if (!string.IsNullOrEmpty(text)) return Finish(text);
|
|
461
|
+
|
|
462
|
+
// WM_COPY is ignored by a few legacy controls; only then fall back to a real
|
|
463
|
+
// key chord, and only when the user has opted in.
|
|
464
|
+
if (!ClipboardFallback || ShellWin.IsModifierDown()) return "";
|
|
351
465
|
try
|
|
352
466
|
{
|
|
353
467
|
ShellWin.SendCopy();
|
|
@@ -356,32 +470,33 @@ namespace DshHud
|
|
|
356
470
|
{
|
|
357
471
|
return "";
|
|
358
472
|
}
|
|
473
|
+
text = WaitForClipboardText(seq, 700);
|
|
474
|
+
return string.IsNullOrEmpty(text) ? "" : Finish(text);
|
|
475
|
+
}
|
|
359
476
|
|
|
360
|
-
|
|
477
|
+
/// <summary>Wait until the clipboard sequence number moves, then read its text.</summary>
|
|
478
|
+
private static string WaitForClipboardText(uint seq, int budgetMs)
|
|
479
|
+
{
|
|
361
480
|
int waited = 0;
|
|
362
|
-
while (waited <
|
|
481
|
+
while (waited < budgetMs)
|
|
363
482
|
{
|
|
364
|
-
Thread.Sleep(
|
|
365
|
-
waited +=
|
|
483
|
+
Thread.Sleep(25);
|
|
484
|
+
waited += 25;
|
|
366
485
|
if (ShellWin.GetClipboardSequenceNumber() == seq) continue;
|
|
367
486
|
try
|
|
368
487
|
{
|
|
369
|
-
if (Clipboard.ContainsText())
|
|
488
|
+
if (Clipboard.ContainsText()) return Clipboard.GetText();
|
|
370
489
|
}
|
|
371
490
|
catch
|
|
372
491
|
{
|
|
373
492
|
}
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
try
|
|
378
|
-
{
|
|
379
|
-
if (backup != null) Clipboard.SetDataObject(backup, false);
|
|
380
|
-
}
|
|
381
|
-
catch
|
|
382
|
-
{
|
|
493
|
+
return "";
|
|
383
494
|
}
|
|
495
|
+
return "";
|
|
496
|
+
}
|
|
384
497
|
|
|
498
|
+
private static string Finish(string text)
|
|
499
|
+
{
|
|
385
500
|
if (string.IsNullOrEmpty(text)) return "";
|
|
386
501
|
text = text.Replace("\r\n", "\n").Replace('\r', '\n').Trim();
|
|
387
502
|
if (text.Length == 0) return "";
|
|
@@ -748,6 +863,9 @@ namespace DshHud
|
|
|
748
863
|
private void Kick(Point pt, int delay)
|
|
749
864
|
{
|
|
750
865
|
if (!enabled) { Log("skip: disabled"); return; }
|
|
866
|
+
// A drag with Ctrl/Shift/Alt held is a copy or an extension, not a fresh
|
|
867
|
+
// selection: probing it races the user's own shortcut.
|
|
868
|
+
if (ShellWin.IsModifierDown()) { Log("skip: modifier down"); return; }
|
|
751
869
|
|
|
752
870
|
if (ShellWin.IsShellSurfaceAt(pt)) { Log("skip: shell surface"); return; }
|
|
753
871
|
int id = ++probeId;
|
package/app/Service.cs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
using System;
|
|
2
2
|
using System.Diagnostics;
|
|
3
3
|
using System.Drawing;
|
|
4
4
|
using System.IO;
|
|
@@ -19,6 +19,7 @@ namespace DshHud
|
|
|
19
19
|
internal bool Ask;
|
|
20
20
|
internal string HotkeyFull;
|
|
21
21
|
internal string HotkeyRegion;
|
|
22
|
+
internal bool TextFallback;
|
|
22
23
|
|
|
23
24
|
internal HudConfig()
|
|
24
25
|
{
|
|
@@ -26,6 +27,7 @@ namespace DshHud
|
|
|
26
27
|
Ask = true;
|
|
27
28
|
HotkeyFull = "Ctrl+Shift+1";
|
|
28
29
|
HotkeyRegion = "Ctrl+Shift+2";
|
|
30
|
+
TextFallback = false;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
internal static string Dir
|
|
@@ -61,6 +63,7 @@ namespace DshHud
|
|
|
61
63
|
else if (key == "ask") cfg.Ask = val != "0";
|
|
62
64
|
else if (key == "hotkeyFull") cfg.HotkeyFull = val;
|
|
63
65
|
else if (key == "hotkeyRegion") cfg.HotkeyRegion = val;
|
|
66
|
+
else if (key == "textFallback") cfg.TextFallback = val != "0";
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
catch
|
|
@@ -79,6 +82,7 @@ namespace DshHud
|
|
|
79
82
|
sb.AppendLine("ask=" + (Ask ? "1" : "0"));
|
|
80
83
|
sb.AppendLine("hotkeyFull=" + HotkeyFull);
|
|
81
84
|
sb.AppendLine("hotkeyRegion=" + HotkeyRegion);
|
|
85
|
+
sb.AppendLine("textFallback=" + (TextFallback ? "1" : "0"));
|
|
82
86
|
File.WriteAllText(ConfigPath, sb.ToString(), Encoding.UTF8);
|
|
83
87
|
}
|
|
84
88
|
catch
|
|
@@ -250,6 +254,7 @@ namespace DshHud
|
|
|
250
254
|
watcher = new SelectionWatcher(host);
|
|
251
255
|
watcher.QuoteRequested += DeliverQuote;
|
|
252
256
|
watcher.Enabled = config.Ask;
|
|
257
|
+
SelectionReader.ClipboardFallback = config.TextFallback;
|
|
253
258
|
|
|
254
259
|
tray = new NotifyIcon();
|
|
255
260
|
tray.Icon = SystemIcons.Application;
|
|
@@ -313,9 +318,11 @@ namespace DshHud
|
|
|
313
318
|
config = HudConfig.Load();
|
|
314
319
|
watcher.Enabled = config.Ask;
|
|
315
320
|
askItem.Checked = config.Ask;
|
|
321
|
+
SelectionReader.ClipboardFallback = config.TextFallback;
|
|
316
322
|
host.Bind(config);
|
|
317
323
|
SelectionWatcher.Log("config reloaded: hotkeyFull=" + config.HotkeyFull +
|
|
318
|
-
" hotkeyRegion=" + config.HotkeyRegion
|
|
324
|
+
" hotkeyRegion=" + config.HotkeyRegion +
|
|
325
|
+
" textFallback=" + (config.TextFallback ? "1" : "0"));
|
|
319
326
|
}
|
|
320
327
|
|
|
321
328
|
private void OnHotkey(string kind)
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -551,6 +551,8 @@ window.__ModuleLoader__.load({
|
|
|
551
551
|
react.createElement('h3', { style: { fontSize: '14px', margin: '0 0 4px' } }, '小窗与截图'),
|
|
552
552
|
react.createElement('p', { style: hintStyle }, '设置对原生小窗和后台划词服务同时生效,保存后约 3 秒内应用。'),
|
|
553
553
|
checkbox('copyShot', '截图自动复制到剪贴板', '全屏、框选两种截图都会复制'),
|
|
554
|
+
checkbox('textFallback', '兼容模式:读不到选区时模拟 Ctrl+C',
|
|
555
|
+
'默认关闭。关闭时只走系统辅助功能读选区,完全不碰剪贴板、不发按键;打开后老式控件才读得到,但可能和你自己的 Ctrl+C 抢剪贴板。'),
|
|
554
556
|
hotkey('hotkeyFull', '全屏截图快捷键'),
|
|
555
557
|
hotkey('hotkeyRegion', '框选截图快捷键'),
|
|
556
558
|
react.createElement('p', { style: hintStyle }, '写法:Ctrl+Shift+1、Ctrl+Shift+S、Alt+PrintScreen、F8 等。组合里必须带 Ctrl/Alt/Shift/Win 之一。'),
|
package/lib/index.js
CHANGED
|
@@ -202,7 +202,7 @@ function advertise(ctx) {
|
|
|
202
202
|
|
|
203
203
|
const CONFIG_PATH = join(WORK_DIR, 'config.ini')
|
|
204
204
|
|
|
205
|
-
const CONFIG_DEFAULTS = { copyShot: true, ask: true, hotkeyFull: 'Ctrl+Shift+1', hotkeyRegion: 'Ctrl+Shift+2' }
|
|
205
|
+
const CONFIG_DEFAULTS = { copyShot: true, ask: true, textFallback: false, hotkeyFull: 'Ctrl+Shift+1', hotkeyRegion: 'Ctrl+Shift+2' }
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
208
|
* Settings live in a flat `key=value` file because the window shell (C# 5) and this
|
|
@@ -219,7 +219,7 @@ function readConfig() {
|
|
|
219
219
|
if (eq <= 0) continue
|
|
220
220
|
const key = line.slice(0, eq).trim()
|
|
221
221
|
const value = line.slice(eq + 1).trim()
|
|
222
|
-
if (key === 'copyShot' || key === 'ask') config[key] = value !== '0'
|
|
222
|
+
if (key === 'copyShot' || key === 'ask' || key === 'textFallback') config[key] = value !== '0'
|
|
223
223
|
else if (key === 'hotkeyFull' || key === 'hotkeyRegion') config[key] = value
|
|
224
224
|
}
|
|
225
225
|
} catch {
|
|
@@ -235,6 +235,7 @@ function writeConfig(config) {
|
|
|
235
235
|
`copyShot=${config.copyShot ? '1' : '0'}`,
|
|
236
236
|
`ask=${config.ask ? '1' : '0'}`,
|
|
237
237
|
`hotkeyFull=${config.hotkeyFull}`,
|
|
238
|
+
`textFallback=${config.textFallback ? '1' : '0'}`,
|
|
238
239
|
`hotkeyRegion=${config.hotkeyRegion}`,
|
|
239
240
|
]
|
|
240
241
|
writeFileSync(CONFIG_PATH, lines.join('\n') + '\n', 'utf8')
|
|
@@ -326,12 +327,13 @@ export function apply(ctx, config = {}) {
|
|
|
326
327
|
let body
|
|
327
328
|
try { body = await readJson(req) } catch (error) { sendJson(res, 400, { error: error.message }); return }
|
|
328
329
|
const next = readConfig()
|
|
329
|
-
for (const key of ['copyShot', 'ask', 'hotkeyFull', 'hotkeyRegion']) {
|
|
330
|
+
for (const key of ['copyShot', 'ask', 'textFallback', 'hotkeyFull', 'hotkeyRegion']) {
|
|
330
331
|
if (Object.prototype.hasOwnProperty.call(body, key)) next[key] = body[key]
|
|
331
332
|
}
|
|
332
333
|
// Normalize before writing: the shell matches these strings exactly when it
|
|
333
334
|
// registers the global hotkeys.
|
|
334
335
|
next.copyShot = Boolean(next.copyShot)
|
|
336
|
+
next.textFallback = Boolean(next.textFallback)
|
|
335
337
|
next.ask = Boolean(next.ask)
|
|
336
338
|
next.hotkeyFull = String(next.hotkeyFull ?? '').trim() || CONFIG_DEFAULTS.hotkeyFull
|
|
337
339
|
next.hotkeyRegion = String(next.hotkeyRegion ?? '').trim() || CONFIG_DEFAULTS.hotkeyRegion
|