positron.js 1.0.4 → 1.0.6

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/core/win/main.cs CHANGED
@@ -10,13 +10,50 @@ using System.Threading;
10
10
  using System.Threading.Tasks;
11
11
  using System.Windows;
12
12
  using System.Windows.Controls;
13
- using System.Windows.Input;
14
13
  using Microsoft.Web.WebView2.Core;
15
14
  using Microsoft.Web.WebView2.Wpf;
16
15
  using System.Text.Json.Serialization;
17
16
  using System.Net;
18
17
  using System.Net.Sockets;
19
18
  using Microsoft.VisualBasic;
19
+ using System.Runtime.InteropServices;
20
+ using Microsoft.Win32;
21
+ using System.Linq;
22
+
23
+ class PowerSaveBlocker
24
+ {
25
+ // Import the Win32 API
26
+ [DllImport("kernel32.dll")]
27
+ private static extern uint SetThreadExecutionState(uint esFlags);
28
+
29
+ // Flags
30
+ private const uint ES_CONTINUOUS = 0x80000000;
31
+ private const uint ES_SYSTEM_REQUIRED = 0x00000001;
32
+ private const uint ES_DISPLAY_REQUIRED = 0x00000002;
33
+
34
+ private static uint currentState = 0;
35
+
36
+ public static void BlockPowerSave(bool keepDisplayOn = false)
37
+ {
38
+ uint flags = ES_CONTINUOUS | ES_SYSTEM_REQUIRED;
39
+ if (keepDisplayOn)
40
+ {
41
+ flags |= ES_DISPLAY_REQUIRED;
42
+ }
43
+
44
+ currentState = SetThreadExecutionState(flags);
45
+ if (currentState == 0)
46
+ {
47
+ Console.WriteLine("Failed to set execution state!");
48
+ }
49
+ }
50
+
51
+ public static void UnblockPowerSave()
52
+ {
53
+ SetThreadExecutionState(ES_CONTINUOUS);
54
+ currentState = 0;
55
+ }
56
+ }
20
57
 
21
58
 
22
59
  namespace PositronWindows
@@ -137,10 +174,18 @@ namespace PositronWindows
137
174
  ? Path.Combine(basePath, "resources")
138
175
  : basePath;
139
176
 
140
- if (File.Exists(Path.Combine(targetDir, "positron-backend.exe")))
177
+ string backendExeName = "positron-backend.exe";
178
+ if (Directory.Exists(targetDir)) {
179
+ string[] files = Directory.GetFiles(targetDir, "*-backend.exe");
180
+ if (files.Length > 0) {
181
+ backendExeName = Path.GetFileName(files[0]);
182
+ }
183
+ }
184
+
185
+ if (File.Exists(Path.Combine(targetDir, backendExeName)))
141
186
  {
142
187
  // PACKAGED MODE — C# is the entry point; launch the Node backend
143
- StartNodeProcess(targetDir);
188
+ StartNodeProcess(targetDir, backendExeName);
144
189
  }
145
190
  else
146
191
  {
@@ -153,7 +198,7 @@ namespace PositronWindows
153
198
  }
154
199
  else
155
200
  {
156
- error("No positron-backend.exe found and POSITRON_IPC_PORT not set. Cannot start.");
201
+ error($"No {backendExeName} found and POSITRON_IPC_PORT not set. Cannot start.");
157
202
  Shutdown();
158
203
  return;
159
204
  }
@@ -189,13 +234,13 @@ namespace PositronWindows
189
234
 
190
235
  private static int _ipcPort = 9000;
191
236
 
192
- private void StartNodeProcess(string workingDirectory)
237
+ private void StartNodeProcess(string workingDirectory, string backendExeName)
193
238
  {
194
239
  IsPackaged = true;
195
240
 
196
241
  _ipcPort = GetRandomOpenPort();
197
242
 
198
- string backendExe = Path.Combine(workingDirectory, "positron-backend.exe");
243
+ string backendExe = Path.Combine(workingDirectory, backendExeName);
199
244
 
200
245
  _nodeProcess = new Process
201
246
  {
@@ -335,7 +380,8 @@ private void StartNodeProcess(string workingDirectory)
335
380
  _ipcClient.Send(new IPCResponse
336
381
  {
337
382
  windowId = windowId,
338
- @event = eventName
383
+ @event = eventName,
384
+ data = new() { { "url", webView.Source?.ToString() ?? "" }, { "title", webView.CoreWebView2.DocumentTitle }, { "canGoBack", webView.CoreWebView2.CanGoBack.ToString().ToLower() }, { "canGoForward", webView.CoreWebView2.CanGoForward.ToString().ToLower() } }
339
385
  });
340
386
  };
341
387
 
@@ -359,6 +405,7 @@ private void StartNodeProcess(string workingDirectory)
359
405
  break;
360
406
  }
361
407
 
408
+
362
409
  case "setContextMenu":
363
410
  if (!LayoutMap.TryGetValue(windowId, out var layout)) break;
364
411
  if (args.Count == 0)
@@ -394,12 +441,41 @@ private void StartNodeProcess(string workingDirectory)
394
441
  GetIPCClient().Send(new IPCResponse
395
442
  {
396
443
  windowId = windowId,
397
- @event = "setSwipeNav-reply-" + windowId,
444
+ @event = args[^1] ?? "setSwipeNav-reply-" + windowId,
398
445
  data = new() { { "enabled", (wvSwipeNav?.CoreWebView2.Settings.IsSwipeNavigationEnabled ?? false).ToString().ToLower() } }
399
446
  });
400
447
 
401
448
  break;
402
449
 
450
+ case "blockPowerSave":
451
+ PowerSaveBlocker.BlockPowerSave();
452
+ break;
453
+
454
+ case "unblockPowerSave":
455
+ PowerSaveBlocker.UnblockPowerSave();
456
+ break;
457
+
458
+ case "isDarkMode":
459
+ bool isLightTheme = true;
460
+ using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"))
461
+ {
462
+ if (key != null)
463
+ {
464
+ object? value = key?.GetValue("AppsUseLightTheme");
465
+ if (value != null && (int)value == 0)
466
+ {
467
+ isLightTheme = false; // Dark Mode
468
+ }
469
+ }
470
+ }
471
+ GetIPCClient().Send(new IPCResponse
472
+ {
473
+ windowId = windowId,
474
+ @event = args[^1] ?? "isDarkMode-reply-" + windowId,
475
+ data = new() { { "isDarkMode", (!isLightTheme).ToString().ToLower() } }
476
+ });
477
+ break;
478
+
403
479
  case "isSwipeNavEnabled":
404
480
  if (!WindowsMap.TryGetValue(windowId, out var winCheckSwipe)) break;
405
481
  var wvCheckSwipe = GetWebView(windowId);
@@ -408,19 +484,75 @@ private void StartNodeProcess(string workingDirectory)
408
484
  GetIPCClient().Send(new IPCResponse
409
485
  {
410
486
  windowId = windowId,
411
- @event = "isSwipeNavEnabled-reply-" + windowId,
487
+ @event = args[^1] ?? "isSwipeNavEnabled-reply-" + windowId,
412
488
  data = new() { { "enabled", isEnabled.ToString().ToLower() } }
413
489
  }
414
490
  );
415
491
  }
416
492
  break;
417
493
 
494
+ case "showFileOpenDialog":
495
+ {
496
+ var dialog = new Microsoft.Win32.OpenFileDialog
497
+ {
498
+ Multiselect = args.Count > 0 && args[0].ToLower() == "true"
499
+ };
500
+ bool? result = dialog.ShowDialog();
501
+ if (result == true)
502
+ {
503
+ string[] files = dialog.FileNames;
504
+ GetIPCClient().Send(new IPCResponse
505
+ {
506
+ windowId = windowId,
507
+ @event = args[^1] ?? "showFileOpenDialog-reply-" + windowId,
508
+ data = new() { { "files", JsonSerializer.Serialize(files) } }
509
+ });
510
+ }
511
+ }
512
+ break;
513
+
514
+ case "readFromClipboard":
515
+ string clipboardText = "";
516
+ Current.Dispatcher.Invoke(() =>
517
+ {
518
+ try
519
+ {
520
+ clipboardText = Clipboard.GetText();
521
+ }
522
+ catch (Exception ex)
523
+ {
524
+ error($"readFromClipboard failed: {ex.Message}");
525
+ }
526
+ });
527
+ GetIPCClient().Send(new IPCResponse
528
+ {
529
+ windowId = windowId,
530
+ @event = args[^1] ?? "readFromClipboard-reply-" + windowId,
531
+ data = new() { { "text", clipboardText } }
532
+ });
533
+ break;
534
+
535
+ case "writeToClipboard":
536
+ if (args.Count == 0) break;
537
+ Current.Dispatcher.Invoke(() =>
538
+ {
539
+ try
540
+ {
541
+ Clipboard.SetText(args[0]);
542
+ }
543
+ catch (Exception ex)
544
+ {
545
+ error($"writeToClipboard failed: {ex.Message}");
546
+ }
547
+ });
548
+ break;
549
+
418
550
  case "isVisible":
419
551
  if (!WindowsMap.TryGetValue(windowId, out var winVisible)) break;
420
552
  bool isVisible = winVisible.IsVisible;
421
553
  GetIPCClient().Send(new IPCResponse
422
554
  { windowId = windowId,
423
- @event = "isVisible-reply-" + windowId,
555
+ @event = args[^1] ?? "isVisible-reply-" + windowId,
424
556
  data = new() { { "isVisible", isVisible.ToString().ToLower() } }
425
557
  });
426
558
  break;
@@ -430,14 +562,17 @@ private void StartNodeProcess(string workingDirectory)
430
562
  bool isFullscreen = winFullscreen.WindowState == WindowState.Maximized;
431
563
  GetIPCClient().Send(new IPCResponse
432
564
  { windowId = windowId,
433
- @event = "isFullscreen-reply-" + windowId,
565
+ @event = args[^1] ?? "isFullscreen-reply-" + windowId,
434
566
  data = new() { { "isFullscreen", isFullscreen.ToString().ToLower() } }
435
567
  });
436
568
  break;
437
569
 
438
570
  case "closeWindow":
439
571
  if (WindowsMap.TryGetValue(windowId, out var winToClose))
572
+ {
573
+ _forceClosing.Add(windowId);
440
574
  winToClose.Close(); // Triggers Closed → cleanup above
575
+ }
441
576
  else
442
577
  error($"closeWindow — no window found with ID {windowId}");
443
578
  break;
@@ -500,6 +635,15 @@ case "forceCloseWindow":
500
635
  }
501
636
  break;
502
637
 
638
+ case "addToContentBlocker":
639
+ _ipcClient.Send(new IPCResponse
640
+ {
641
+ windowId = windowId,
642
+ @event = args[^1] ?? "addToContentBlocker-reply-" + windowId,
643
+ data = new() { { "status", "success" }, { "warning", "Content blocker not supported on Windows." } }
644
+ });
645
+ break;
646
+
503
647
  case "loadFile":
504
648
  if (!WindowsMap.TryGetValue(windowId, out _)) break;
505
649
  if (args.Count == 0)
@@ -550,7 +694,7 @@ case "forceCloseWindow":
550
694
  _ipcClient.Send(new IPCResponse
551
695
  {
552
696
  windowId = windowId,
553
- @event = "evaluateJS-reply-" + windowId,
697
+ @event = args[^1] ?? "evaluateJS-reply-" + windowId,
554
698
  data = new() { { "result", result ?? "null" } }
555
699
  });
556
700
  }
@@ -560,7 +704,7 @@ case "forceCloseWindow":
560
704
  _ipcClient.Send(new IPCResponse
561
705
  {
562
706
  windowId = windowId,
563
- @event = "evaluateJS-reply-" + windowId,
707
+ @event = args[^1] ?? "evaluateJS-reply-" + windowId,
564
708
  data = new() { { "error", ex.Message } }
565
709
  });
566
710
  }
@@ -574,11 +718,21 @@ case "forceCloseWindow":
574
718
  _ipcClient.Send(new IPCResponse
575
719
  {
576
720
  windowId = windowId,
577
- @event = "isFocused-reply-" + windowId,
721
+ @event = args[^1] ?? "isFocused-reply-" + windowId,
578
722
  data = new() { { "isFocused", isFocused.ToString().ToLower() } }
579
723
  });
580
724
  break;
581
725
 
726
+ case "getFocusedWindowId":
727
+ int focusedWindowId = WindowsMap.FirstOrDefault(kv => kv.Value.IsActive).Key;
728
+ _ipcClient.Send(new IPCResponse
729
+ {
730
+ windowId = windowId,
731
+ @event = args[^1] ?? "getFocusedWindowId-reply-" + windowId,
732
+ data = new() { { "focusedWindowId", focusedWindowId.ToString() } }
733
+ });
734
+ break;
735
+
582
736
  case "showNotification":
583
737
  if (args.Count < 2)
584
738
  {
@@ -666,7 +820,7 @@ case "setBounds":
666
820
  _ipcClient.Send(new IPCResponse
667
821
  {
668
822
  windowId = windowId,
669
- @event = "prompt-reply-" + windowId,
823
+ @event = args[^1] ?? "prompt-reply-" + windowId,
670
824
  data = new() { { "input", result } }
671
825
  });
672
826
  }
@@ -684,7 +838,7 @@ case "setBounds":
684
838
  _ipcClient.Send(new IPCResponse
685
839
  {
686
840
  windowId = windowId,
687
- @event = "confirm-reply-" + windowId,
841
+ @event = args[^1] ?? "confirm-reply-" + windowId,
688
842
  data = new() { { "confirmed", result.ToString().ToLower() } }
689
843
  });
690
844
  }
@@ -810,7 +964,7 @@ case "setBounds":
810
964
  _ipcClient.Send(new IPCResponse
811
965
  {
812
966
  windowId = windowId,
813
- @event = "capture-page-result-" + windowId,
967
+ @event = args[^1] ?? "capture-page-result-" + windowId,
814
968
  data = new() { { "imageData", base64 } }
815
969
  });
816
970
  }
@@ -831,7 +985,7 @@ case "setBounds":
831
985
  _ipcClient.Send(new IPCResponse
832
986
  {
833
987
  windowId = windowId,
834
- @event = "canGoBack-reply-" + windowId,
988
+ @event = args[^1] ?? "canGoBack-reply-" + windowId,
835
989
  data = new() { { "canGoBack", canGoBack.ToString().ToLower() } }
836
990
  });
837
991
  }
@@ -847,7 +1001,7 @@ case "setBounds":
847
1001
  _ipcClient.Send(new IPCResponse
848
1002
  {
849
1003
  windowId = windowId,
850
- @event = "canGoForward-reply-" + windowId,
1004
+ @event = args[^1] ?? "canGoForward-reply-" + windowId,
851
1005
  data = new() { { "canGoForward", canGoForward.ToString().ToLower() } }
852
1006
  });
853
1007
  }
@@ -863,7 +1017,7 @@ case "setBounds":
863
1017
  _ipcClient.Send(new IPCResponse
864
1018
  {
865
1019
  windowId = windowId,
866
- @event = "getURL-reply-" + windowId,
1020
+ @event = args[^1] ?? "getURL-reply-" + windowId,
867
1021
  data = new() { { "url", url } }
868
1022
  });
869
1023
  }
@@ -879,7 +1033,7 @@ case "setBounds":
879
1033
  _ipcClient.Send(new IPCResponse
880
1034
  {
881
1035
  windowId = windowId,
882
- @event = "getTitle-reply-" + windowId,
1036
+ @event = args[^1] ?? "getTitle-reply-" + windowId,
883
1037
  data = new() { { "title", title } }
884
1038
  });
885
1039
  }
@@ -951,7 +1105,7 @@ case "setBounds":
951
1105
  MenuMap[windowId] = menu;
952
1106
  }
953
1107
 
954
- private static void PopulateMenu(ItemCollection parentItems, JsonArray items, int windowId, string eventType = "menu-action")
1108
+ internal static void PopulateMenu(ItemCollection parentItems, JsonArray items, int windowId, string eventType = "menu-action")
955
1109
  {
956
1110
  foreach (var item in items)
957
1111
  {
@@ -0,0 +1,142 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Windows.Controls;
4
+ using System.Text.Json;
5
+ using System.Text.Json.Nodes;
6
+ using System.Windows;
7
+ using System.Drawing;
8
+
9
+ namespace PositronWindows
10
+ {
11
+ public class TrayManager
12
+ {
13
+ private static TrayManager? _shared;
14
+ public static TrayManager Shared => _shared ??= new TrayManager();
15
+
16
+ public System.Windows.Forms.NotifyIcon? NotifyIcon { get; private set; }
17
+ private ContextMenu? _wpfContextMenu;
18
+
19
+ public void SetupTray()
20
+ {
21
+ if (NotifyIcon == null)
22
+ {
23
+ NotifyIcon = new System.Windows.Forms.NotifyIcon();
24
+ NotifyIcon.Visible = true;
25
+ NotifyIcon.Text = "App";
26
+ NotifyIcon.Icon = SystemIcons.Application;
27
+
28
+ NotifyIcon.MouseUp += (s, e) =>
29
+ {
30
+ if (e.Button == System.Windows.Forms.MouseButtons.Right || e.Button == System.Windows.Forms.MouseButtons.Left)
31
+ {
32
+ if (_wpfContextMenu != null)
33
+ {
34
+ _wpfContextMenu.IsOpen = true;
35
+ if (Application.Current.MainWindow != null)
36
+ {
37
+ Application.Current.MainWindow.Activate();
38
+ }
39
+ }
40
+ }
41
+ };
42
+ }
43
+ }
44
+
45
+ public void SetMenu(ContextMenu menu)
46
+ {
47
+ _wpfContextMenu = menu;
48
+ }
49
+
50
+ public void SetTitle(string title)
51
+ {
52
+ if (NotifyIcon != null && !string.IsNullOrEmpty(title))
53
+ {
54
+ NotifyIcon.Text = title.Length > 63 ? title.Substring(0, 63) : title;
55
+ }
56
+ }
57
+
58
+ public void SetIcon(string iconPath)
59
+ {
60
+ if (NotifyIcon != null && !string.IsNullOrEmpty(iconPath))
61
+ {
62
+ try
63
+ {
64
+ NotifyIcon.Icon = new Icon(iconPath);
65
+ }
66
+ catch (Exception ex)
67
+ {
68
+ Console.WriteLine($"Failed to set tray icon: {ex.Message}");
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ public static class TrayExtension
75
+ {
76
+ public static void Handle(int windowId, List<string> args)
77
+ {
78
+ if (args.Count == 0)
79
+ {
80
+ Console.WriteLine("tray:setMenu — missing JSON descriptor");
81
+ return;
82
+ }
83
+
84
+ if (args[^1] == "setTitle")
85
+ {
86
+ string title = args[0];
87
+ Application.Current.Dispatcher.Invoke(() =>
88
+ {
89
+ TrayManager.Shared.SetTitle(title);
90
+ });
91
+ return;
92
+ }
93
+
94
+ if (args[^1] == "setIcon")
95
+ {
96
+ string iconPath = args[0];
97
+ Application.Current.Dispatcher.Invoke(() =>
98
+ {
99
+ TrayManager.Shared.SetIcon(iconPath);
100
+ });
101
+ return;
102
+ }
103
+
104
+ var descString = args[0];
105
+ var ctxDescriptor = JsonSerializer.Deserialize<JsonArray>(descString);
106
+ if (ctxDescriptor == null)
107
+ {
108
+ Console.WriteLine("tray:setMenu — invalid JSON descriptor");
109
+ return;
110
+ }
111
+
112
+ if(args[^1] == "setMenu")
113
+ {
114
+ Application.Current.Dispatcher.Invoke(() =>
115
+ {
116
+ var contextMenu = new ContextMenu();
117
+ App.PopulateMenu(contextMenu.Items, ctxDescriptor, windowId, "context-menu-action");
118
+ TrayManager.Shared.SetMenu(contextMenu);
119
+ });
120
+ return;
121
+ }
122
+
123
+ Application.Current.Dispatcher.Invoke(() =>
124
+ {
125
+ TrayManager.Shared.SetupTray();
126
+
127
+ string? title = args.Count > 1 ? args[1] : "";
128
+ TrayManager.Shared.SetTitle(title);
129
+
130
+ string? imagePath = args.Count > 2 ? args[2] : null;
131
+ if (imagePath != null)
132
+ {
133
+ TrayManager.Shared.SetIcon(imagePath);
134
+ }
135
+
136
+ var contextMenu = new ContextMenu();
137
+ App.PopulateMenu(contextMenu.Items, ctxDescriptor, windowId, "context-menu-action");
138
+ TrayManager.Shared.SetMenu(contextMenu);
139
+ });
140
+ }
141
+ }
142
+ }