dsh-mini 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,1278 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Diagnostics;
4
+ using System.Drawing;
5
+ using System.Drawing.Imaging;
6
+ using System.IO;
7
+ using System.Runtime.InteropServices;
8
+ using System.Text;
9
+ using System.Threading;
10
+ using System.Windows.Forms;
11
+ using Microsoft.Web.WebView2.Core;
12
+ using Microsoft.Web.WebView2.WinForms;
13
+
14
+ namespace DshHud
15
+ {
16
+ internal static class Native
17
+ {
18
+ internal const int WM_DPICHANGED = 0x02E0;
19
+
20
+ [DllImport("user32.dll", SetLastError = true)]
21
+ internal static extern bool SetProcessDpiAwarenessContext(IntPtr value);
22
+
23
+ [DllImport("user32.dll")]
24
+ internal static extern uint GetDpiForSystem();
25
+
26
+ [DllImport("user32.dll")]
27
+ internal static extern IntPtr MonitorFromPoint(POINT pt, uint flags);
28
+
29
+ [DllImport("shcore.dll")]
30
+ internal static extern int GetDpiForMonitor(IntPtr monitor, int dpiType, out uint dpiX, out uint dpiY);
31
+
32
+ [DllImport("user32.dll")]
33
+ internal static extern bool GetMonitorInfo(IntPtr monitor, ref MONITORINFO info);
34
+
35
+ [StructLayout(LayoutKind.Sequential)]
36
+ internal struct RECTW
37
+ {
38
+ public int Left;
39
+ public int Top;
40
+ public int Right;
41
+ public int Bottom;
42
+ }
43
+
44
+ [StructLayout(LayoutKind.Sequential)]
45
+ internal struct MONITORINFO
46
+ {
47
+ public int cbSize;
48
+ public RECTW rcMonitor;
49
+ public RECTW rcWork;
50
+ public uint dwFlags;
51
+ }
52
+
53
+ internal const uint MONITOR_DEFAULTTOPRIMARY = 2;
54
+
55
+ /// <summary>Primary monitor work area in physical pixels.</summary>
56
+ internal static Rectangle WorkArea()
57
+ {
58
+ try
59
+ {
60
+ POINT pt;
61
+ pt.X = 0;
62
+ pt.Y = 0;
63
+ IntPtr mon = MonitorFromPoint(pt, MONITOR_DEFAULTTOPRIMARY);
64
+ MONITORINFO mi = new MONITORINFO();
65
+ mi.cbSize = Marshal.SizeOf(typeof(MONITORINFO));
66
+ if (GetMonitorInfo(mon, ref mi))
67
+ {
68
+ return Rectangle.FromLTRB(mi.rcWork.Left, mi.rcWork.Top, mi.rcWork.Right, mi.rcWork.Bottom);
69
+ }
70
+ }
71
+ catch
72
+ {
73
+ }
74
+ return Screen.PrimaryScreen.WorkingArea;
75
+ }
76
+
77
+ [StructLayout(LayoutKind.Sequential)]
78
+ internal struct POINT
79
+ {
80
+ public int X;
81
+ public int Y;
82
+ }
83
+
84
+ /// <summary>Effective DPI of the monitor that owns the given point (0,0 = primary).</summary>
85
+ internal static float EffectiveScale()
86
+ {
87
+ try
88
+ {
89
+ POINT pt;
90
+ pt.X = 0;
91
+ pt.Y = 0;
92
+ IntPtr mon = MonitorFromPoint(pt, 2 /*MONITOR_DEFAULTTOPRIMARY*/);
93
+ uint dx, dy;
94
+ if (GetDpiForMonitor(mon, 0 /*MDT_EFFECTIVE_DPI*/, out dx, out dy) == 0 && dx >= 96)
95
+ {
96
+ return dx / 96f;
97
+ }
98
+ }
99
+ catch
100
+ {
101
+ }
102
+
103
+ uint dpi = GetDpiForSystem();
104
+ if (dpi >= 96)
105
+ {
106
+ return dpi / 96f;
107
+ }
108
+ return 1f;
109
+ }
110
+
111
+ [DllImport("user32.dll")]
112
+ internal static extern bool ReleaseCapture();
113
+
114
+ [DllImport("user32.dll")]
115
+ internal static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
116
+
117
+ [DllImport("user32.dll")]
118
+ internal static extern bool SetForegroundWindow(IntPtr hWnd);
119
+
120
+ [DllImport("user32.dll")]
121
+ internal static extern bool ShowWindow(IntPtr hWnd, int cmd);
122
+
123
+ internal const int SW_RESTORE = 9;
124
+ [DllImport("dwmapi.dll")]
125
+ internal static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int value, int size);
126
+
127
+ internal const int WM_NCHITTEST = 0x0084;
128
+ internal const int WM_NCLBUTTONDOWN = 0x00A1;
129
+ internal const int HTCLIENT = 1;
130
+ internal const int HTCAPTION = 2;
131
+ internal const int HTLEFT = 10;
132
+ internal const int HTRIGHT = 11;
133
+ internal const int HTTOP = 12;
134
+ internal const int HTTOPLEFT = 13;
135
+ internal const int HTTOPRIGHT = 14;
136
+ internal const int HTBOTTOM = 15;
137
+ internal const int HTBOTTOMLEFT = 16;
138
+ internal const int HTBOTTOMRIGHT = 17;
139
+
140
+ internal const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
141
+ internal const int DWMWA_WINDOW_CORNER_PREFERENCE = 33;
142
+ internal const int DWMWCP_ROUND = 2;
143
+ }
144
+
145
+ internal sealed class HudForm : Form
146
+ {
147
+ private readonly string startUrl;
148
+ private readonly string pendingPath;
149
+ private readonly string statePath;
150
+ private readonly Mutex mutex;
151
+
152
+ private WebView2 web;
153
+ private NotifyIcon tray;
154
+ private System.Windows.Forms.Timer pendingTimer;
155
+ private SelectionWatcher watcher;
156
+ private ToolStripMenuItem askMenuItem;
157
+ private string pendingQuote;
158
+ private string pendingCmd;
159
+ private HudConfig config;
160
+ private long configStamp;
161
+ private string askFlagPath;
162
+ private Panel titleBar;
163
+ private Label titleLabel;
164
+ private Label pinButton;
165
+ private readonly List<Label> iconButtons = new List<Label>();
166
+ private readonly List<int> iconWidths = new List<int>();
167
+ private Panel gapPanel;
168
+ private System.Windows.Forms.Timer themeTimer;
169
+ private Color chromeBg = Color.FromArgb(247, 247, 248);
170
+ private Color chromeFg = Color.FromArgb(32, 32, 36);
171
+ private bool pinned = true;
172
+ private bool ready;
173
+ private float scale = 1f;
174
+
175
+ internal HudForm(string url, string title, Mutex mutex)
176
+ {
177
+ this.startUrl = url;
178
+ this.mutex = mutex;
179
+
180
+ string dir = Path.Combine(
181
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "dsh-hud");
182
+ Directory.CreateDirectory(dir);
183
+ pendingPath = Path.Combine(dir, "pending.txt");
184
+ statePath = Path.Combine(dir, "window.txt");
185
+ askFlagPath = Path.Combine(dir, "ask.txt");
186
+
187
+ scale = Native.EffectiveScale();
188
+
189
+ SuspendLayout();
190
+ FormBorderStyle = FormBorderStyle.None;
191
+ StartPosition = FormStartPosition.Manual;
192
+ AutoScaleMode = AutoScaleMode.None;
193
+ Text = title;
194
+ TopMost = true;
195
+ ShowInTaskbar = true;
196
+ BackColor = Color.FromArgb(247, 247, 248);
197
+ DoubleBuffered = true;
198
+ KeyPreview = true;
199
+ Padding = new Padding(Unit(5));
200
+ MinimumSize = Logical(300, 340);
201
+ Size = Logical(460, 560);
202
+ RestoreSavedBounds();
203
+
204
+ BuildTitleBar(title);
205
+ BuildWebView();
206
+ BuildTray();
207
+ ResumeLayout(false);
208
+
209
+ pendingTimer = new System.Windows.Forms.Timer();
210
+ pendingTimer.Interval = 700;
211
+ pendingTimer.Tick += OnPendingTick;
212
+ pendingTimer.Start();
213
+
214
+ // The page can repaint itself long after load (theme switch, session switch,
215
+ // moving between monitors with different DPI). Keep the chrome in step.
216
+ themeTimer = new System.Windows.Forms.Timer();
217
+ themeTimer.Interval = 6000;
218
+ themeTimer.Tick += delegate
219
+ {
220
+ if (!ready || !Visible) return;
221
+ SyncChromeWithPageAsync(0);
222
+ };
223
+ themeTimer.Start();
224
+
225
+ watcher = new SelectionWatcher(this);
226
+ watcher.QuoteRequested += AskAboutSelection;
227
+ watcher.Enabled = ReadAskFlag();
228
+
229
+ config = HudConfig.Load();
230
+ configStamp = HudConfig.Stamp();
231
+ }
232
+
233
+ private Size Logical(int w, int h)
234
+ {
235
+ return new Size((int)Math.Round(w * scale), (int)Math.Round(h * scale));
236
+ }
237
+
238
+ private int Unit(int v)
239
+ {
240
+ return (int)Math.Round(v * scale);
241
+ }
242
+
243
+ // ---------- chrome ----------
244
+
245
+ private void BuildTitleBar(string title)
246
+ {
247
+ titleBar = new Panel();
248
+ titleBar.Dock = DockStyle.Top;
249
+ titleBar.Height = Unit(36);
250
+ titleBar.BackColor = Blend(chromeBg, chromeFg, 0.04);
251
+ Controls.Add(titleBar);
252
+
253
+ titleLabel = new Label();
254
+ titleLabel.AutoSize = false;
255
+ titleLabel.Dock = DockStyle.Fill;
256
+ titleLabel.TextAlign = ContentAlignment.MiddleLeft;
257
+ titleLabel.Padding = new Padding(Unit(12), 0, 0, 0);
258
+ titleLabel.Font = new Font("Microsoft YaHei UI", 9.5f, FontStyle.Regular);
259
+ titleLabel.ForeColor = chromeFg;
260
+ titleLabel.Text = title;
261
+ titleLabel.MouseDown += OnDragAreaMouseDown;
262
+ titleLabel.MouseDoubleClick += OnTitleDoubleClick;
263
+ titleBar.Controls.Add(titleLabel);
264
+
265
+ Label pin = MakeIcon("\uE718", "\u7F6E\u9876", 44);
266
+ pin.Click += delegate { TogglePin(); };
267
+ pinButton = pin;
268
+
269
+ Label back = MakeIcon("\uE72B", "\u4E3B", 40);
270
+ back.Click += delegate { BackToMain(); };
271
+
272
+ Label shot = MakeIcon("\uE722", "\u622A", 40);
273
+ shot.Click += delegate { DoScreenshot(); };
274
+
275
+ // Region / window picker. Segoe MDL2 "Crop" = E7A8.
276
+ Label crop = MakeIcon("\uE7A8", "\u6846", 40);
277
+ crop.Click += delegate { DoRegionCapture(); };
278
+
279
+ // The close glyph is drawn noticeably larger than the others at the same
280
+ // point size, so it gets its own smaller face and a hover highlight.
281
+ Label close = MakeIcon("\uE8BB", "\u00D7", 40);
282
+ close.Font = new Font("Segoe MDL2 Assets", 8.5f, FontStyle.Regular);
283
+ close.ForeColor = IconTint();
284
+ close.MouseEnter += delegate { close.BackColor = Color.FromArgb(232, 17, 35); close.ForeColor = Color.White; };
285
+ close.MouseLeave += delegate { close.BackColor = Color.Transparent; close.ForeColor = IconTint(); };
286
+ close.Click += delegate { HideToTray(); };
287
+
288
+ // Small spacer so the last key does not sit on the window border. Right
289
+ // docking stacks leftwards, so the spacer is added last to stay rightmost.
290
+ Panel gap = new Panel();
291
+ gap.Dock = DockStyle.Right;
292
+ gap.Width = Unit(6);
293
+ gapPanel = gap;
294
+
295
+ titleBar.Controls.Add(close);
296
+ titleBar.Controls.Add(pin);
297
+ titleBar.Controls.Add(back);
298
+ titleBar.Controls.Add(shot);
299
+ titleBar.Controls.Add(crop);
300
+ titleBar.Controls.Add(gap);
301
+
302
+ titleBar.MouseDown += OnDragAreaMouseDown;
303
+ titleBar.MouseDoubleClick += OnTitleDoubleClick;
304
+ }
305
+
306
+ private Label MakeIcon(string glyph, string fallback, int width)
307
+ {
308
+ Label b = new Label();
309
+ b.Dock = DockStyle.Right;
310
+ b.Width = Unit(width);
311
+ iconWidths.Add(width);
312
+ b.TextAlign = ContentAlignment.MiddleCenter;
313
+ b.Font = new Font("Segoe MDL2 Assets", 10f, FontStyle.Regular);
314
+ b.ForeColor = IconTint();
315
+ b.Cursor = Cursors.Hand;
316
+ b.Text = glyph;
317
+ b.MouseEnter += delegate { b.BackColor = HoverTint(); };
318
+ b.MouseLeave += delegate { b.BackColor = Color.Transparent; };
319
+ b.Tag = fallback;
320
+ iconButtons.Add(b);
321
+ return b;
322
+ }
323
+
324
+ /// <summary>
325
+ /// The window frame and title bar follow the page's own colours. Before the
326
+ /// page has reported in, a neutral light grey stands in for it.
327
+ /// </summary>
328
+ private Color Blend(Color a, Color b, double amount)
329
+ {
330
+ int r = (int)Math.Round(a.R + (b.R - a.R) * amount);
331
+ int g = (int)Math.Round(a.G + (b.G - a.G) * amount);
332
+ int bl = (int)Math.Round(a.B + (b.B - a.B) * amount);
333
+ return Color.FromArgb(r, g, bl);
334
+ }
335
+
336
+ private Color IconTint()
337
+ {
338
+ return Blend(chromeFg, chromeBg, 0.25);
339
+ }
340
+
341
+ private Color HoverTint()
342
+ {
343
+ return Blend(chromeBg, chromeFg, 0.12);
344
+ }
345
+
346
+ private void OnDragAreaMouseDown(object sender, MouseEventArgs e)
347
+ {
348
+ if (e.Button != MouseButtons.Left) return;
349
+ Native.ReleaseCapture();
350
+ Native.SendMessage(Handle, Native.WM_NCLBUTTONDOWN, (IntPtr)Native.HTCAPTION, IntPtr.Zero);
351
+ }
352
+
353
+ private void OnTitleDoubleClick(object sender, MouseEventArgs e)
354
+ {
355
+ Size = (WindowState == FormWindowState.Maximized) ? Logical(460, 920) : Screen.FromControl(this).WorkingArea.Size;
356
+ }
357
+
358
+ private void TogglePin()
359
+ {
360
+ pinned = !pinned;
361
+ TopMost = pinned;
362
+ pinButton.ForeColor = pinned ? Color.FromArgb(110, 190, 255) : Color.FromArgb(140, 140, 148);
363
+ }
364
+
365
+ private void BuildWebView()
366
+ {
367
+ web = new WebView2();
368
+ web.Dock = DockStyle.Fill;
369
+ web.DefaultBackgroundColor = Color.FromArgb(24, 24, 27);
370
+ web.AllowExternalDrop = false;
371
+ Controls.Add(web);
372
+ web.BringToFront();
373
+ }
374
+
375
+ private void BuildTray()
376
+ {
377
+ tray = new NotifyIcon();
378
+ tray.Icon = SystemIcons.Application;
379
+ tray.Text = "DSH \u5C0F\u7A97";
380
+ tray.Visible = true;
381
+ tray.DoubleClick += delegate { RestoreFromTray(); };
382
+
383
+ ContextMenuStrip menu = new ContextMenuStrip();
384
+ menu.Items.Add("\u663E\u793A\u5C0F\u7A97", null, delegate { RestoreFromTray(); });
385
+
386
+ askMenuItem = new ToolStripMenuItem("\u5212\u8BCD\u95EE\u5927\u80A5\u9C7C");
387
+ askMenuItem.CheckOnClick = true;
388
+ askMenuItem.Checked = true;
389
+ askMenuItem.CheckedChanged += delegate
390
+ {
391
+ if (watcher == null) return;
392
+ watcher.Enabled = askMenuItem.Checked;
393
+ WriteAskFlag(askMenuItem.Checked);
394
+ };
395
+ menu.Items.Add(askMenuItem);
396
+
397
+ menu.Items.Add(new ToolStripSeparator());
398
+ menu.Items.Add("\u9000\u51FA", null, delegate { Close(); });
399
+ tray.ContextMenuStrip = menu;
400
+ }
401
+
402
+ private void RestoreFromTray()
403
+ {
404
+ Show();
405
+ WindowState = FormWindowState.Normal;
406
+ Activate();
407
+ }
408
+
409
+ /// <summary>
410
+ /// The close key parks the window in the tray instead of exiting: the selection
411
+ /// watcher lives in this process, so the "问大肥鱼" pill would die with it.
412
+ /// </summary>
413
+ private void HideToTray()
414
+ {
415
+ Hide();
416
+ if (tray != null && !tray.Visible) tray.Visible = true;
417
+ }
418
+
419
+ private bool ReadAskFlag()
420
+ {
421
+ try
422
+ {
423
+ if (!File.Exists(askFlagPath)) return true;
424
+ return File.ReadAllText(askFlagPath).Trim() != "0";
425
+ }
426
+ catch
427
+ {
428
+ return true;
429
+ }
430
+ }
431
+
432
+ private void WriteAskFlag(bool on)
433
+ {
434
+ try
435
+ {
436
+ File.WriteAllText(askFlagPath, on ? "1" : "0", Encoding.UTF8);
437
+ }
438
+ catch
439
+ {
440
+ }
441
+ }
442
+
443
+ // ---------- lifecycle ----------
444
+
445
+ protected override void OnHandleCreated(EventArgs e)
446
+ {
447
+ base.OnHandleCreated(e);
448
+ try
449
+ {
450
+ int round = Native.DWMWCP_ROUND;
451
+ Native.DwmSetWindowAttribute(Handle, Native.DWMWA_WINDOW_CORNER_PREFERENCE, ref round, sizeof(int));
452
+ int dark = 1;
453
+ Native.DwmSetWindowAttribute(Handle, Native.DWMWA_USE_IMMERSIVE_DARK_MODE, ref dark, sizeof(int));
454
+ }
455
+ catch
456
+ {
457
+ }
458
+ }
459
+
460
+ protected override void OnShown(EventArgs e)
461
+ {
462
+ base.OnShown(e);
463
+ InitWebViewAsync();
464
+ }
465
+
466
+ private async void InitWebViewAsync()
467
+ {
468
+ try
469
+ {
470
+ string udf = Path.Combine(
471
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "dsh-hud", "webview2");
472
+ Directory.CreateDirectory(udf);
473
+ CoreWebView2Environment env = await CoreWebView2Environment.CreateAsync(null, udf, null);
474
+ await web.EnsureCoreWebView2Async(env);
475
+
476
+ web.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
477
+ web.CoreWebView2.Settings.IsStatusBarEnabled = false;
478
+ web.CoreWebView2.Settings.AreDevToolsEnabled = false;
479
+ web.CoreWebView2.Settings.IsZoomControlEnabled = false;
480
+ web.CoreWebView2.WebMessageReceived += OnWebMessage;
481
+ web.CoreWebView2.NavigationCompleted += OnNavigationCompleted;
482
+
483
+ // A real browser zoom: the layout viewport becomes 460/0.9 CSS px, so
484
+ // the document still fills the window edge to edge. CSS `zoom` on the
485
+ // body only shrank the used height, which left blank bands.
486
+ web.ZoomFactor = 0.9;
487
+
488
+ try
489
+ {
490
+ SyncHintCookies(startUrl);
491
+ }
492
+ catch
493
+ {
494
+ }
495
+
496
+ web.CoreWebView2.Navigate(startUrl);
497
+ ready = true;
498
+ }
499
+ catch (Exception ex)
500
+ {
501
+ MessageBox.Show("WebView2 \u521D\u59CB\u5316\u5931\u8D25\uFF1A" + ex.Message,
502
+ "DSH \u5C0F\u7A97", MessageBoxButtons.OK, MessageBoxIcon.Error);
503
+ }
504
+ }
505
+
506
+ private void OnPendingTick(object sender, EventArgs e)
507
+ {
508
+ try
509
+ {
510
+ if (File.Exists(pendingPath))
511
+ {
512
+ string url = File.ReadAllText(pendingPath, Encoding.UTF8).Trim();
513
+ File.Delete(pendingPath);
514
+ if (url.Length > 0 && ready)
515
+ {
516
+ SyncHintCookies(url);
517
+ web.CoreWebView2.Navigate(url);
518
+ RestoreFromTray();
519
+ }
520
+ }
521
+ }
522
+ catch
523
+ {
524
+ }
525
+ PumpEval();
526
+ PumpRequests();
527
+
528
+ try
529
+ {
530
+ string pillPath = Path.Combine(Path.GetDirectoryName(pendingPath), "showpill.txt");
531
+ if (File.Exists(pillPath))
532
+ {
533
+ File.Delete(pillPath);
534
+ if (watcher != null) watcher.DebugPill();
535
+ }
536
+
537
+ string regionPath = Path.Combine(Path.GetDirectoryName(pendingPath), "regionshot.txt");
538
+ if (File.Exists(regionPath))
539
+ {
540
+ File.Delete(regionPath);
541
+ DoRegionCapture();
542
+ }
543
+
544
+ string testPath = Path.Combine(Path.GetDirectoryName(pendingPath), "seltest.txt");
545
+ if (File.Exists(testPath))
546
+ {
547
+ File.Delete(testPath);
548
+ if (watcher != null) watcher.TestProbe();
549
+ }
550
+ }
551
+ catch
552
+ {
553
+ }
554
+ }
555
+
556
+ /// <summary>
557
+ /// Work handed over by the background service: a quote to insert, or a screenshot
558
+ /// command from a global hotkey. Both arrive as files so the two processes never
559
+ /// need to agree on IPC.
560
+ /// </summary>
561
+ private void PumpRequests()
562
+ {
563
+ try
564
+ {
565
+ long stamp = HudConfig.Stamp();
566
+ if (stamp != configStamp)
567
+ {
568
+ configStamp = stamp;
569
+ config = HudConfig.Load();
570
+ Log("config reloaded copyShot=" + config.CopyShot);
571
+ }
572
+
573
+ string quote = HudConfig.QuotePath;
574
+ if (File.Exists(quote))
575
+ {
576
+ string text = File.ReadAllText(quote, Encoding.UTF8);
577
+ File.Delete(quote);
578
+ if (!string.IsNullOrEmpty(text))
579
+ {
580
+ Log("quote from service " + text.Length + " chars");
581
+ pendingQuote = text;
582
+ RestoreFromTray();
583
+ if (ready && web != null && web.CoreWebView2 != null) FlushPendingQuote();
584
+ }
585
+ }
586
+
587
+ string cmd = HudConfig.CmdPath;
588
+ if (File.Exists(cmd))
589
+ {
590
+ string kind = File.ReadAllText(cmd, Encoding.UTF8).Trim();
591
+ File.Delete(cmd);
592
+ if (kind.Length > 0)
593
+ {
594
+ Log("cmd from service: " + kind);
595
+ if (!ready) pendingCmd = kind;
596
+ else RunCmd(kind);
597
+ }
598
+ }
599
+ else if (pendingCmd != null && ready)
600
+ {
601
+ string kind = pendingCmd;
602
+ pendingCmd = null;
603
+ RunCmd(kind);
604
+ }
605
+ }
606
+ catch
607
+ {
608
+ }
609
+ }
610
+
611
+ private void RunCmd(string kind)
612
+ {
613
+ if (kind == "region") DoRegionCapture();
614
+ else DoScreenshot();
615
+ }
616
+
617
+ /// <summary>
618
+ /// The GUI authenticates a fresh profile by redirecting to a cleaned URL, which
619
+ /// drops our query flags. Cookies survive that redirect, so the hand-off values
620
+ /// live there where the page can still read them.
621
+ /// </summary>
622
+ private void SyncHintCookies(string url)
623
+ {
624
+ CoreWebView2Cookie hud = web.CoreWebView2.CookieManager.CreateCookie("dsh_hud", "1", "127.0.0.1", "/");
625
+ hud.Expires = DateTime.Now.AddHours(12);
626
+ web.CoreWebView2.CookieManager.AddOrUpdateCookie(hud);
627
+
628
+ string session = QueryValue(url, "session");
629
+ CoreWebView2Cookie handoff = web.CoreWebView2.CookieManager.CreateCookie(
630
+ "dsh_hud_session", session, "127.0.0.1", "/");
631
+ handoff.Expires = session.Length == 0 ? DateTime.Now.AddSeconds(-1) : DateTime.Now.AddHours(12);
632
+ web.CoreWebView2.CookieManager.AddOrUpdateCookie(handoff);
633
+ }
634
+
635
+ private static string QueryValue(string url, string key)
636
+ {
637
+ int question = url.IndexOf('?');
638
+ if (question < 0) return "";
639
+ string query = url.Substring(question + 1);
640
+ int hash = query.IndexOf('#');
641
+ if (hash >= 0) query = query.Substring(0, hash);
642
+ string[] parts = query.Split('&');
643
+ for (int i = 0; i < parts.Length; i++)
644
+ {
645
+ int equals = parts[i].IndexOf('=');
646
+ if (equals <= 0) continue;
647
+ if (parts[i].Substring(0, equals) == key)
648
+ {
649
+ return Uri.UnescapeDataString(parts[i].Substring(equals + 1));
650
+ }
651
+ }
652
+ return "";
653
+ }
654
+
655
+ /// <summary>Dev loop: drop JS into %LOCALAPPDATA%\dsh-hud\eval.js, result lands in hud.log.</summary>
656
+ private void PumpEval()
657
+ {
658
+ try
659
+ {
660
+ if (!ready) return;
661
+ string dir = Path.GetDirectoryName(pendingPath);
662
+ string evalPath = Path.Combine(dir, "eval.js");
663
+ if (!File.Exists(evalPath)) return;
664
+ string script = File.ReadAllText(evalPath, Encoding.UTF8);
665
+ File.Delete(evalPath);
666
+ RunEval(script);
667
+ }
668
+ catch
669
+ {
670
+ }
671
+ }
672
+
673
+ private async void RunEval(string script)
674
+ {
675
+ string dir = Path.GetDirectoryName(pendingPath);
676
+ string logPath = Path.Combine(dir, "hud.log");
677
+ try
678
+ {
679
+ string result = await web.CoreWebView2.ExecuteScriptAsync(script);
680
+ File.WriteAllText(logPath, result, Encoding.UTF8);
681
+ }
682
+ catch (Exception ex)
683
+ {
684
+ try { File.WriteAllText(logPath, "eval failed: " + ex.Message, Encoding.UTF8); }
685
+ catch { }
686
+ }
687
+ }
688
+
689
+ // ---------- page bridge ----------
690
+
691
+ private void OnNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
692
+ {
693
+ if (!e.IsSuccess) return;
694
+ try
695
+ {
696
+ web.CoreWebView2.ExecuteScriptAsync(HudPage.Script);
697
+ }
698
+ catch
699
+ {
700
+ }
701
+ // The app paints itself a moment after load, so sample the theme twice.
702
+ SyncChromeWithPageAsync(400);
703
+ SyncChromeWithPageAsync(1800);
704
+ if (Environment.GetEnvironmentVariable("DSH_HUD_DEBUG") == "1")
705
+ {
706
+ RunProbe();
707
+ }
708
+ if (pendingQuote != null && web != null && web.CoreWebView2 != null)
709
+ {
710
+ QuoteAsync(pendingQuote);
711
+ pendingQuote = null;
712
+ }
713
+ }
714
+
715
+ /// <summary>
716
+ /// Paint the frame and title bar in the page's own colours, so a light document
717
+ /// does not sit inside a black border. The page reports the two computed values
718
+ /// (`background-color|color`) and the shell recolours its chrome from them.
719
+ /// </summary>
720
+ private async void SyncChromeWithPageAsync(int delay)
721
+ {
722
+ if (delay > 0)
723
+ {
724
+ try { await System.Threading.Tasks.Task.Delay(delay); }
725
+ catch { return; }
726
+ }
727
+ try
728
+ {
729
+ if (!ready || web.CoreWebView2 == null) return;
730
+ string raw = await web.CoreWebView2.ExecuteScriptAsync(
731
+ "(function(){var s=getComputedStyle(document.body);" +
732
+ "return s.backgroundColor+'|'+s.color;})()");
733
+ string text = Unquote(raw);
734
+ string[] parts = text.Split('|');
735
+ if (parts.Length < 2) return;
736
+ Color bg = ParseCssColor(parts[0], Color.Empty);
737
+ Color fg = ParseCssColor(parts[1], Color.Empty);
738
+ if (bg.IsEmpty || bg.A == 0) return;
739
+ if (fg.IsEmpty) fg = Luminance(bg) > 0.55 ? Color.FromArgb(32, 32, 36) : Color.FromArgb(235, 235, 240);
740
+ ApplyChrome(bg, fg);
741
+ }
742
+ catch
743
+ {
744
+ }
745
+ }
746
+
747
+ private void ApplyChrome(Color bg, Color fg)
748
+ {
749
+ chromeBg = bg;
750
+ chromeFg = fg;
751
+ try
752
+ {
753
+ BackColor = bg;
754
+ if (titleBar != null) titleBar.BackColor = Blend(bg, fg, 0.04);
755
+ if (titleLabel != null) titleLabel.ForeColor = fg;
756
+ for (int i = 0; i < iconButtons.Count; i++) iconButtons[i].ForeColor = IconTint();
757
+ if (pinButton != null) pinButton.ForeColor = IconTint();
758
+
759
+ int dark = Luminance(bg) < 0.45 ? 1 : 0;
760
+ if (IsHandleCreated)
761
+ {
762
+ Native.DwmSetWindowAttribute(Handle, Native.DWMWA_USE_IMMERSIVE_DARK_MODE, ref dark, sizeof(int));
763
+ }
764
+ }
765
+ catch
766
+ {
767
+ }
768
+ }
769
+
770
+ private static double Luminance(Color c)
771
+ {
772
+ return (0.2126 * c.R + 0.7152 * c.G + 0.0722 * c.B) / 255.0;
773
+ }
774
+
775
+ /// <summary>ExecuteScriptAsync answers with a JSON literal; unwrap it.</summary>
776
+ private static string Unquote(string raw)
777
+ {
778
+ if (raw == null) return "";
779
+ string text = raw.Trim();
780
+ if (text.Length >= 2 && text[0] == '"' && text[text.Length - 1] == '"')
781
+ {
782
+ text = text.Substring(1, text.Length - 2);
783
+ }
784
+ return text.Replace("\\\"", "\"").Replace("\\u0026", "&").Replace("\\/", "/");
785
+ }
786
+
787
+ private static Color ParseCssColor(string value, Color fallback)
788
+ {
789
+ if (value == null) return fallback;
790
+ string text = value.Trim();
791
+ if (text.Length == 0) return fallback;
792
+ int open = text.IndexOf('(');
793
+ if (open < 0 || text.IndexOf("rgb", StringComparison.OrdinalIgnoreCase) != 0) return fallback;
794
+ int close = text.IndexOf(')', open);
795
+ if (close < 0) return fallback;
796
+ string[] parts = text.Substring(open + 1, close - open - 1).Split(',');
797
+ if (parts.Length < 3) return fallback;
798
+ int r, g, b;
799
+ if (!int.TryParse(parts[0].Trim(), out r)) return fallback;
800
+ if (!int.TryParse(parts[1].Trim(), out g)) return fallback;
801
+ if (!int.TryParse(parts[2].Trim(), out b)) return fallback;
802
+ int a = 255;
803
+ if (parts.Length >= 4)
804
+ {
805
+ double alpha;
806
+ if (double.TryParse(parts[3].Trim(), System.Globalization.NumberStyles.Float,
807
+ System.Globalization.CultureInfo.InvariantCulture, out alpha))
808
+ {
809
+ a = (int)Math.Round(alpha * 255);
810
+ }
811
+ }
812
+ return Color.FromArgb(a, Math.Min(255, r), Math.Min(255, g), Math.Min(255, b));
813
+ }
814
+
815
+ private async void RunProbe()
816
+ {
817
+ string logPath = Path.Combine(Path.GetDirectoryName(pendingPath), "hud.log");
818
+ try
819
+ {
820
+ await System.Threading.Tasks.Task.Delay(3000);
821
+ string raw = await web.CoreWebView2.ExecuteScriptAsync(
822
+ "JSON.stringify(window.__dshHud ? window.__dshHud.probe() : {err:'bridge missing'})");
823
+ File.WriteAllText(logPath, raw, Encoding.UTF8);
824
+ }
825
+ catch (Exception ex)
826
+ {
827
+ try
828
+ {
829
+ File.WriteAllText(logPath, "probe failed: " + ex.Message, Encoding.UTF8);
830
+ }
831
+ catch
832
+ {
833
+ }
834
+ }
835
+ }
836
+
837
+ private void OnWebMessage(object sender, CoreWebView2WebMessageReceivedEventArgs e)
838
+ {
839
+ string text;
840
+ try
841
+ {
842
+ text = e.TryGetWebMessageAsString();
843
+ }
844
+ catch
845
+ {
846
+ return;
847
+ }
848
+ if (text == null) return;
849
+
850
+ if (text.IndexOf("\"shot\"", StringComparison.Ordinal) >= 0) { DoScreenshot(); return; }
851
+ if (text.IndexOf("\"shot-region\"", StringComparison.Ordinal) >= 0) { DoRegionCapture(); return; }
852
+ if (text.IndexOf("\"pin\"", StringComparison.Ordinal) >= 0) { TogglePin(); return; }
853
+ if (text.IndexOf("\"close\"", StringComparison.Ordinal) >= 0) { Close(); return; }
854
+ if (text.IndexOf("\"hide\"", StringComparison.Ordinal) >= 0) { Hide(); return; }
855
+ }
856
+
857
+ private void BackToMain()
858
+ {
859
+ try
860
+ {
861
+ Process self = Process.GetCurrentProcess();
862
+ foreach (Process p in Process.GetProcessesByName("DeepSeek Harness"))
863
+ {
864
+ if (p.Id == self.Id) continue;
865
+ IntPtr h = p.MainWindowHandle;
866
+ if (h == IntPtr.Zero) continue;
867
+ Native.ShowWindow(h, Native.SW_RESTORE);
868
+ Native.SetForegroundWindow(h);
869
+ break;
870
+ }
871
+ }
872
+ catch
873
+ {
874
+ }
875
+ Hide();
876
+ }
877
+ private void SendToPage(string json)
878
+ {
879
+ if (!ready) return;
880
+ try
881
+ {
882
+ web.CoreWebView2.PostWebMessageAsJson(json);
883
+ }
884
+ catch
885
+ {
886
+ }
887
+ }
888
+
889
+ // ---------- 划词问大肥鱼 ----------
890
+
891
+ /// <summary>
892
+ /// Called when the user clicks the pill next to a selection. Brings the window up
893
+ /// and quotes the captured text into the composer.
894
+ /// </summary>
895
+ private void AskAboutSelection(string text)
896
+ {
897
+ if (string.IsNullOrEmpty(text)) return;
898
+ pendingQuote = text;
899
+ RestoreFromTray();
900
+ if (ready && web != null && web.CoreWebView2 != null) FlushPendingQuote();
901
+ }
902
+
903
+ private void FlushPendingQuote()
904
+ {
905
+ if (pendingQuote == null) return;
906
+ string text = pendingQuote;
907
+ pendingQuote = null;
908
+ QuoteAsync(text);
909
+ }
910
+
911
+ private async void QuoteAsync(string text)
912
+ {
913
+ try
914
+ {
915
+ string script = "(function(){try{return window.__dshHud && window.__dshHud.quote("
916
+ + "\"" + Escape(text) + "\") ? 'ok' : 'fail';}catch(e){return 'err:' + e.message;}})()";
917
+ string result = await web.CoreWebView2.ExecuteScriptAsync(script);
918
+ Log("quote " + text.Length + " chars -> " + result);
919
+ }
920
+ catch (Exception ex)
921
+ {
922
+ Log("quote failed: " + ex.Message);
923
+ }
924
+ }
925
+
926
+ private void Log(string line)
927
+ {
928
+ try
929
+ {
930
+ string logPath = Path.Combine(Path.GetDirectoryName(pendingPath), "hud.log");
931
+ File.AppendAllText(logPath, DateTime.Now.ToString("HH:mm:ss") + " " + line + "\r\n", Encoding.UTF8);
932
+ }
933
+ catch
934
+ {
935
+ }
936
+ }
937
+
938
+ private static string Escape(string s)
939
+ {
940
+ if (s == null) return "";
941
+ StringBuilder sb = new StringBuilder(s.Length + 8);
942
+ foreach (char c in s)
943
+ {
944
+ if (c == '"' || c == '\\') sb.Append('\\').Append(c);
945
+ else if (c == '\n') sb.Append("\\n");
946
+ else if (c == '\r') sb.Append("\\r");
947
+ else if (c < ' ') sb.Append(' ');
948
+ else sb.Append(c);
949
+ }
950
+ return sb.ToString();
951
+ }
952
+
953
+ private void CopyToClipboard(Image img)
954
+ {
955
+ try
956
+ {
957
+ Clipboard.SetImage(img);
958
+ Log("copied to clipboard " + img.Width + "x" + img.Height);
959
+ }
960
+ catch (Exception ex)
961
+ {
962
+ Log("clipboard failed: " + ex.Message);
963
+ }
964
+ }
965
+
966
+ private void DoScreenshot()
967
+ {
968
+ if (!ready) return;
969
+ Hide();
970
+ Application.DoEvents();
971
+ Thread.Sleep(320);
972
+ try
973
+ {
974
+ Rectangle vs = RegionCaptureForm.PhysicalVirtualScreen();
975
+ using (Bitmap bmp = new Bitmap(vs.Width, vs.Height))
976
+ {
977
+ using (Graphics g = Graphics.FromImage(bmp))
978
+ {
979
+ g.CopyFromScreen(vs.X, vs.Y, 0, 0, new Size(vs.Width, vs.Height), CopyPixelOperation.SourceCopy);
980
+ }
981
+ byte[] bytes;
982
+ using (MemoryStream ms = new MemoryStream())
983
+ {
984
+ bmp.Save(ms, ImageFormat.Png);
985
+ bytes = ms.ToArray();
986
+ }
987
+ string b64 = Convert.ToBase64String(bytes);
988
+ if (config != null && config.CopyShot) CopyToClipboard(bmp);
989
+ SendToPage("{\"type\":\"shot-result\",\"dataUrl\":\"data:image/png;base64," + b64 +
990
+ "\",\"width\":" + vs.Width + ",\"height\":" + vs.Height + "}");
991
+ }
992
+ }
993
+ catch (Exception ex)
994
+ {
995
+ SendToPage("{\"type\":\"shot-error\",\"error\":\"" + Escape(ex.Message) + "\"}");
996
+ }
997
+ finally
998
+ {
999
+ Show();
1000
+ Activate();
1001
+ }
1002
+ }
1003
+
1004
+ /// <summary>
1005
+ /// Freeze the desktop, let the user pick a window (click) or a rectangle (drag),
1006
+ /// then crop that out of the frozen frame. Cropping the frozen bitmap instead of
1007
+ /// re-capturing means the overlay can never end up in its own picture.
1008
+ /// </summary>
1009
+ private void DoRegionCapture()
1010
+ {
1011
+ if (!ready) return;
1012
+
1013
+ Rectangle vs = RegionCaptureForm.PhysicalVirtualScreen();
1014
+ Bitmap frozen = null;
1015
+ Bitmap crop = null;
1016
+ try
1017
+ {
1018
+ Hide();
1019
+ Application.DoEvents();
1020
+ Thread.Sleep(240);
1021
+
1022
+ List<CaptureTarget> targets = RegionCaptureForm.CollectTargets();
1023
+ frozen = RegionCaptureForm.GrabDesktop(vs);
1024
+ bool picked = false;
1025
+ Rectangle local = Rectangle.Empty;
1026
+ using (RegionCaptureForm picker = new RegionCaptureForm(frozen, vs, targets))
1027
+ {
1028
+ if (picker.ShowDialog() == DialogResult.OK)
1029
+ {
1030
+ Rectangle r = picker.SelectedRect;
1031
+ local = new Rectangle(r.X - vs.X, r.Y - vs.Y, r.Width, r.Height);
1032
+ picked = local.Width > 1 && local.Height > 1;
1033
+ if (picked)
1034
+ {
1035
+ // Clone before the picker disposes the frozen backdrop.
1036
+ crop = frozen.Clone(local, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
1037
+ }
1038
+ }
1039
+ frozen = null; // owned and disposed by the picker
1040
+ }
1041
+
1042
+ Log("region pick " + (picked ? (local.Width + "x" + local.Height) : "none") +
1043
+ " at " + local.X + "," + local.Y);
1044
+ if (!picked || crop == null) return;
1045
+
1046
+ byte[] bytes;
1047
+ using (MemoryStream ms = new MemoryStream())
1048
+ {
1049
+ crop.Save(ms, ImageFormat.Png);
1050
+ bytes = ms.ToArray();
1051
+ }
1052
+ string b64 = Convert.ToBase64String(bytes);
1053
+ if (config != null && config.CopyShot) CopyToClipboard(crop);
1054
+ SendToPage("{\"type\":\"shot-result\",\"dataUrl\":\"data:image/png;base64," + b64 +
1055
+ "\",\"width\":" + crop.Width + ",\"height\":" + crop.Height + "}");
1056
+ }
1057
+ catch (Exception ex)
1058
+ {
1059
+ SendToPage("{\"type\":\"shot-error\",\"error\":\"" + Escape(ex.Message) + "\"}");
1060
+ }
1061
+ finally
1062
+ {
1063
+ if (crop != null) crop.Dispose();
1064
+ Show();
1065
+ Activate();
1066
+ }
1067
+ }
1068
+
1069
+ // ---------- window plumbing ----------
1070
+
1071
+ /// <summary>
1072
+ /// Re-derive every hard-coded pixel size from a new DPI factor. Called when the
1073
+ /// window is dragged to a monitor with a different scale (WM_DPICHANGED), which
1074
+ /// is why the sizes are kept in logical units in the first place.
1075
+ /// </summary>
1076
+ private void ApplyScale(float next)
1077
+ {
1078
+ if (next <= 0f) return;
1079
+ Size logical = new Size(
1080
+ (int)Math.Round(Width / (double)scale),
1081
+ (int)Math.Round(Height / (double)scale));
1082
+ scale = next;
1083
+ Padding = new Padding(Unit(5));
1084
+ MinimumSize = Logical(300, 340);
1085
+ if (titleBar != null) titleBar.Height = Unit(36);
1086
+ if (titleLabel != null) titleLabel.Padding = new Padding(Unit(12), 0, 0, 0);
1087
+ if (gapPanel != null) gapPanel.Width = Unit(6);
1088
+ for (int i = 0; i < iconButtons.Count && i < iconWidths.Count; i++)
1089
+ {
1090
+ iconButtons[i].Width = Unit(iconWidths[i]);
1091
+ }
1092
+ Size = Logical(logical.Width, logical.Height);
1093
+ Log("dpi scale " + next.ToString("0.##"));
1094
+ }
1095
+
1096
+ protected override void WndProc(ref Message m)
1097
+ {
1098
+ if (m.Msg == Native.WM_DPICHANGED)
1099
+ {
1100
+ int dpi = (int)(m.WParam.ToInt64() & 0xFFFF);
1101
+ if (dpi > 0)
1102
+ {
1103
+ float next = dpi / 96f;
1104
+ if (Math.Abs(next - scale) > 0.01f) ApplyScale(next);
1105
+ }
1106
+ SyncChromeWithPageAsync(0);
1107
+ }
1108
+
1109
+ if (m.Msg == Native.WM_NCHITTEST)
1110
+ {
1111
+ int edge = Unit(7);
1112
+ int screenX = (short)(m.LParam.ToInt64() & 0xFFFF);
1113
+ int screenY = (short)((m.LParam.ToInt64() >> 16) & 0xFFFF);
1114
+ Point p = PointToClient(new Point(screenX, screenY));
1115
+ bool left = p.X <= edge;
1116
+ bool right = p.X >= ClientSize.Width - edge;
1117
+ bool top = p.Y <= edge;
1118
+ bool bottom = p.Y >= ClientSize.Height - edge;
1119
+ if (left && top) { m.Result = (IntPtr)Native.HTTOPLEFT; return; }
1120
+ if (right && top) { m.Result = (IntPtr)Native.HTTOPRIGHT; return; }
1121
+ if (left && bottom) { m.Result = (IntPtr)Native.HTBOTTOMLEFT; return; }
1122
+ if (right && bottom) { m.Result = (IntPtr)Native.HTBOTTOMRIGHT; return; }
1123
+ if (left) { m.Result = (IntPtr)Native.HTLEFT; return; }
1124
+ if (right) { m.Result = (IntPtr)Native.HTRIGHT; return; }
1125
+ if (top) { m.Result = (IntPtr)Native.HTTOP; return; }
1126
+ if (bottom) { m.Result = (IntPtr)Native.HTBOTTOM; return; }
1127
+ }
1128
+ base.WndProc(ref m);
1129
+ }
1130
+
1131
+ private void RestoreSavedBounds()
1132
+ {
1133
+ Rectangle wa = Native.WorkArea();
1134
+ int x = wa.Right - Width - Unit(16);
1135
+ int y = wa.Top + Unit(16);
1136
+ if (File.Exists(statePath))
1137
+ {
1138
+ try
1139
+ {
1140
+ string[] parts = File.ReadAllText(statePath).Split(',');
1141
+ if (parts.Length == 4)
1142
+ {
1143
+ int sx, sy, sw, sh;
1144
+ if (int.TryParse(parts[0], out sx) && int.TryParse(parts[1], out sy) &&
1145
+ int.TryParse(parts[2], out sw) && int.TryParse(parts[3], out sh) && sw > 200 && sh > 200)
1146
+ {
1147
+ Rectangle saved = new Rectangle(sx, sy, sw, sh);
1148
+ bool visible = false;
1149
+ foreach (Screen s in Screen.AllScreens)
1150
+ {
1151
+ if (Rectangle.Intersect(s.WorkingArea, saved).Width > 80) { visible = true; break; }
1152
+ }
1153
+ if (visible) { Location = new Point(sx, sy); Size = new Size(sw, sh); return; }
1154
+ }
1155
+ }
1156
+ }
1157
+ catch
1158
+ {
1159
+ }
1160
+ }
1161
+ Location = new Point(x, y);
1162
+ }
1163
+
1164
+ protected override void OnFormClosing(FormClosingEventArgs e)
1165
+ {
1166
+ base.OnFormClosing(e);
1167
+ try
1168
+ {
1169
+ Rectangle r = WindowState == FormWindowState.Normal ? Bounds : RestoreBounds;
1170
+ File.WriteAllText(statePath, r.X + "," + r.Y + "," + r.Width + "," + r.Height);
1171
+ }
1172
+ catch
1173
+ {
1174
+ }
1175
+ if (tray != null) { tray.Visible = false; tray.Dispose(); tray = null; }
1176
+ if (watcher != null) { watcher.Dispose(); watcher = null; }
1177
+ if (pendingTimer != null) { pendingTimer.Stop(); pendingTimer.Dispose(); pendingTimer = null; }
1178
+ if (mutex != null)
1179
+ {
1180
+ try { mutex.ReleaseMutex(); } catch { }
1181
+ mutex.Dispose();
1182
+ }
1183
+ }
1184
+
1185
+ protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
1186
+ {
1187
+ if (keyData == Keys.Escape) { Hide(); return true; }
1188
+ return base.ProcessCmdKey(ref msg, keyData);
1189
+ }
1190
+ }
1191
+
1192
+ internal static class Program
1193
+ {
1194
+ [STAThread]
1195
+ private static void Main(string[] argv)
1196
+ {
1197
+ try { Native.SetProcessDpiAwarenessContext(new IntPtr(-4)); }
1198
+ catch { }
1199
+
1200
+ Application.EnableVisualStyles();
1201
+ Application.SetCompatibleTextRenderingDefault(false);
1202
+
1203
+ string url = null;
1204
+ string title = "DSH \u5C0F\u7A97";
1205
+ bool service = false;
1206
+ bool probe = false;
1207
+ long probeHwnd = 0;
1208
+ for (int i = 0; i < argv.Length; i++)
1209
+ {
1210
+ if (argv[i] == "--url" && i + 1 < argv.Length) url = argv[i + 1];
1211
+ else if (argv[i] == "--title" && i + 1 < argv.Length) title = argv[i + 1];
1212
+ else if (argv[i] == "--service") service = true;
1213
+ else if (argv[i] == "--probe") probe = true;
1214
+ else if (argv[i] == "--hwnd" && i + 1 < argv.Length)
1215
+ {
1216
+ long parsed;
1217
+ if (long.TryParse(argv[i + 1], out parsed)) probeHwnd = parsed;
1218
+ }
1219
+ else if (argv[i] == "--debug")
1220
+ {
1221
+ // expose the WebView2 page over CDP so we can inspect/patch the DOM from outside
1222
+ Environment.SetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS",
1223
+ "--remote-debugging-port=9333");
1224
+ Environment.SetEnvironmentVariable("DSH_HUD_DEBUG", "1");
1225
+ }
1226
+ }
1227
+
1228
+ if (probe)
1229
+ {
1230
+ // Headless self-test: read the selection once and report it, no window at all.
1231
+ SelectionHit hit = SelectionReader.ReadHit(new IntPtr(probeHwnd));
1232
+ string head = hit.Text ?? "";
1233
+ if (head.Length > 60) head = head.Substring(0, 60);
1234
+ SelectionWatcher.Log("probe " + (hit.Text == null ? 0 : hit.Text.Length) + " chars" +
1235
+ (hit.HasRect ? " rect=" + hit.Rect.ToString() : " rect=none") +
1236
+ " head=" + head.Replace("\n", " ").Replace("\r", " "));
1237
+ return;
1238
+ }
1239
+ if (service)
1240
+ {
1241
+ // One background helper: it owns the selection pill and the global hotkeys,
1242
+ // so "问大肥鱼" works whether or not this window has ever been opened.
1243
+ bool serviceCreated;
1244
+ Mutex serviceMutex = new Mutex(true, "Local\\DshHudService", out serviceCreated);
1245
+ if (!serviceCreated)
1246
+ {
1247
+ serviceMutex.Dispose();
1248
+ return;
1249
+ }
1250
+ Application.Run(new SelectionService(url));
1251
+ return;
1252
+ }
1253
+
1254
+ if (string.IsNullOrEmpty(url))
1255
+ {
1256
+ MessageBox.Show("\u7F3A\u5C11 --url \u53C2\u6570\u3002", "DSH \u5C0F\u7A97",
1257
+ MessageBoxButtons.OK, MessageBoxIcon.Warning);
1258
+ return;
1259
+ }
1260
+
1261
+ string dir = Path.Combine(
1262
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "dsh-hud");
1263
+ Directory.CreateDirectory(dir);
1264
+ string pending = Path.Combine(dir, "pending.txt");
1265
+
1266
+ bool created;
1267
+ Mutex mutex = new Mutex(true, "Local\\DshHudWindow", out created);
1268
+ if (!created)
1269
+ {
1270
+ try { File.WriteAllText(pending, url, Encoding.UTF8); }
1271
+ catch { }
1272
+ return;
1273
+ }
1274
+
1275
+ Application.Run(new HudForm(url, title, mutex));
1276
+ }
1277
+ }
1278
+ }