Skip to content

Commit 5c18b92

Browse files
committed
feat: automatically launch at startup
1 parent aae6fd8 commit 5c18b92

File tree

12 files changed

+562
-85
lines changed

12 files changed

+562
-85
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System.Diagnostics;
2+
3+
namespace Everywhere.Windows.Interop;
4+
5+
public static class TaskSchedulerHelper
6+
{
7+
public static bool IsTaskScheduled(string taskName)
8+
{
9+
using var process = Process.Start(
10+
new ProcessStartInfo("schtasks.exe", $"/Query /TN \"{taskName}\"")
11+
{
12+
CreateNoWindow = true,
13+
RedirectStandardOutput = true,
14+
RedirectStandardError = true
15+
});
16+
if (process is null) return false;
17+
process.WaitForExit();
18+
return process.ExitCode == 0;
19+
}
20+
21+
public static void CreateScheduledTask(string taskName, string appPath)
22+
{
23+
Process.Start(
24+
new ProcessStartInfo("schtasks.exe", $"/Create /TN \"{taskName}\" /TR \"{appPath.Replace("\"", "\\\"")}\" /SC ONLOGON /RL HIGHEST /F")
25+
{
26+
CreateNoWindow = true,
27+
RedirectStandardOutput = true,
28+
RedirectStandardError = true
29+
})?.WaitForExit();
30+
}
31+
32+
public static void DeleteScheduledTask(string taskName)
33+
{
34+
Process.Start(
35+
new ProcessStartInfo("schtasks.exe", $"/Delete /TN \"{taskName}\" /F")
36+
{
37+
CreateNoWindow = true,
38+
RedirectStandardOutput = true,
39+
RedirectStandardError = true
40+
})?.WaitForExit();
41+
}
42+
}

src/Everywhere.Windows/Services/Win32NativeHelper.cs

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
using System.Numerics;
1+
using System.Diagnostics;
2+
using System.Numerics;
23
using System.Reflection;
34
using System.Runtime.InteropServices;
5+
using System.Security.Principal;
46
using Windows.Data.Xml.Dom;
57
using Windows.UI.Composition;
68
using Windows.UI.Composition.Desktop;
@@ -16,6 +18,7 @@
1618
using Avalonia.Media.Imaging;
1719
using Avalonia.Platform;
1820
using Avalonia.Threading;
21+
using Everywhere.Extensions;
1922
using Everywhere.Interfaces;
2023
using Everywhere.Windows.Interop;
2124
using MicroCom.Runtime;
@@ -27,13 +30,111 @@ namespace Everywhere.Windows.Services;
2730

2831
public class Win32NativeHelper : INativeHelper
2932
{
33+
private const string AppName = nameof(Everywhere);
34+
private const string RegistryRunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
35+
private static string AppPath => $"\"{Environment.ProcessPath}\" --autorun";
36+
3037
// ReSharper disable InconsistentNaming
3138
// ReSharper disable IdentifierTypo
3239
private const uint EVENT_SYSTEM_FOREGROUND = 0x0003;
3340
private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
3441
// ReSharper restore InconsistentNaming
3542
// ReSharper restore IdentifierTypo
3643

44+
public bool IsAdministrator
45+
{
46+
get
47+
{
48+
var identity = WindowsIdentity.GetCurrent();
49+
var principal = new WindowsPrincipal(identity);
50+
return principal.IsInRole(WindowsBuiltInRole.Administrator);
51+
}
52+
}
53+
54+
public bool IsUserStartupEnabled
55+
{
56+
get
57+
{
58+
try
59+
{
60+
using var key = Registry.CurrentUser.OpenSubKey(RegistryRunKey);
61+
return key?.GetValue(AppName) != null;
62+
}
63+
catch
64+
{
65+
// If the registry key cannot be accessed, assume it is not enabled.
66+
return false;
67+
}
68+
}
69+
set
70+
{
71+
if (value)
72+
{
73+
using var key = Registry.CurrentUser.OpenSubKey(RegistryRunKey, true);
74+
key?.SetValue(AppName, AppPath);
75+
}
76+
else
77+
{
78+
using var key = Registry.CurrentUser.OpenSubKey(RegistryRunKey, true);
79+
key?.DeleteValue(AppName, false);
80+
}
81+
}
82+
}
83+
84+
public bool IsAdministratorStartupEnabled
85+
{
86+
get
87+
{
88+
try
89+
{
90+
return TaskSchedulerHelper.IsTaskScheduled(AppName);
91+
}
92+
catch
93+
{
94+
return false;
95+
}
96+
}
97+
set
98+
{
99+
if (!IsAdministrator) throw new UnauthorizedAccessException("The current user is not an administrator.");
100+
101+
if (value)
102+
{
103+
TaskSchedulerHelper.CreateScheduledTask(AppName, AppPath);
104+
}
105+
else
106+
{
107+
TaskSchedulerHelper.DeleteScheduledTask(AppName);
108+
}
109+
}
110+
}
111+
112+
public void RestartAsAdministrator()
113+
{
114+
if (IsAdministrator)
115+
{
116+
throw new InvalidOperationException("The application is already running as an administrator.");
117+
}
118+
119+
var startInfo = new ProcessStartInfo
120+
{
121+
FileName = Environment.ProcessPath.NotNull(),
122+
Arguments = "--autorun",
123+
UseShellExecute = true,
124+
Verb = "runas" // This will prompt for elevation
125+
};
126+
127+
try
128+
{
129+
Process.Start(startInfo);
130+
Environment.Exit(0); // Exit the current process
131+
}
132+
catch (Exception ex)
133+
{
134+
throw new InvalidOperationException("Failed to restart as administrator.", ex);
135+
}
136+
}
137+
37138
public void SetWindowNoFocus(Window window)
38139
{
39140
Win32Properties.AddWindowStylesCallback(window, WindowStylesCallback);
@@ -129,6 +230,7 @@ void WinEventProc(
129230
public void SetWindowHitTestInvisible(Window window)
130231
{
131232
Win32Properties.AddWindowStylesCallback(window, WindowStylesCallback);
233+
132234
static (uint style, uint exStyle) WindowStylesCallback(uint style, uint exStyle)
133235
{
134236
return (style, exStyle |
@@ -166,8 +268,10 @@ public void SetWindowCornerRadius(Window window, CornerRadius cornerRadius)
166268
if (!_compositionContexts.TryGetValue(hWnd, out var compositionContext))
167269
{
168270
// we will need lots of hacks, let's go
169-
if (window.PlatformImpl?.GetType().GetField("_glSurface", BindingFlags.Instance | BindingFlags.NonPublic) is not { } glSurfaceField) return;
170-
if (glSurfaceField.GetValue(window.PlatformImpl) is not { } glSurface) return; // Avalonia.Win32.WinRT.Composition.WinUiCompositedWindowSurface
271+
if (window.PlatformImpl?.GetType().GetField("_glSurface", BindingFlags.Instance | BindingFlags.NonPublic) is not
272+
{ } glSurfaceField) return;
273+
if (glSurfaceField.GetValue(window.PlatformImpl) is not { } glSurface)
274+
return; // Avalonia.Win32.WinRT.Composition.WinUiCompositedWindowSurface
171275
if (glSurface.GetType().GetField("_window", BindingFlags.Instance | BindingFlags.NonPublic) is not { } windowField) return;
172276
if (windowField.GetValue(glSurface) is not { } compositedWindow) return; // Avalonia.Win32.WinRT.Composition.WinUiCompositedWindow
173277
if (glSurface.GetType().GetField("_shared", BindingFlags.Instance | BindingFlags.NonPublic) is not { } sharedField) return;
@@ -377,4 +481,4 @@ private record CompositionContext(Compositor Compositor, Visual RootVisual)
377481
{
378482
public CompositionClip? Clip { get; set; }
379483
}
380-
}
484+
}

src/Everywhere/App.axaml.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ private void HandleExitMenuItemClicked(object? sender, EventArgs e)
192192

193193
file class DesignTimeNativeHelper : INativeHelper
194194
{
195+
public bool IsAdministrator => false;
196+
public bool IsUserStartupEnabled { get; set; }
197+
public bool IsAdministratorStartupEnabled { get; set; }
198+
public void RestartAsAdministrator() { }
195199
public void SetWindowNoFocus(Window window) { }
196200
public void SetWindowAutoHide(Window window) { }
197201
public void SetWindowHitTestInvisible(Window window) { }

src/Everywhere/Attributes/SettingsItemAttributes.cs renamed to src/Everywhere/Attributes/SettingsAttributes.cs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,42 @@
1-
namespace Everywhere.Attributes;
1+
using Lucide.Avalonia;
2+
3+
namespace Everywhere.Attributes;
4+
5+
/// <summary>
6+
/// Represents an attribute that defines a settings category.
7+
/// </summary>
8+
[AttributeUsage(AttributeTargets.Property)]
9+
public class SettingsCategoryAttribute : Attribute
10+
{
11+
/// <summary>
12+
/// The display name of the settings category.
13+
/// </summary>
14+
public required string Header { get; set; }
15+
16+
/// <summary>
17+
/// The Icon of the settings category.
18+
/// </summary>
19+
public required LucideIconKind Icon { get; set; }
20+
}
221

322
/// <summary>
423
/// This attribute is used to mark properties that should not be serialized or displayed in the UI.
524
/// </summary>
625
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)]
7-
public class HiddenSettingsItemAttribute : Attribute
26+
public class HiddenSettingsItemAttribute : Attribute;
27+
28+
[AttributeUsage(AttributeTargets.Property)]
29+
public class SettingsItemAttribute : Attribute
830
{
9-
public string? Condition { get; set; }
31+
/// <summary>
32+
/// Sets a binding path that will be used to determine if this item is visible in the UI.
33+
/// </summary>
34+
public string? IsVisibleBindingPath { get; set; }
35+
36+
/// <summary>
37+
/// Sets a binding path that will be used to determine if this item is enabled in the UI.
38+
/// </summary>
39+
public string? IsEnabledBindingPath { get; set; }
1040
}
1141

1242
[AttributeUsage(AttributeTargets.Property)]
@@ -39,9 +69,9 @@ public class SettingsDoubleItemAttribute : Attribute
3969
public class SettingsSelectionItemAttribute : Attribute
4070
{
4171
/// <summary>
42-
/// this can be a binding path
72+
/// A binding path to the property that contains the items to select from.
4373
/// </summary>
44-
public required string ItemsSource { get; set; }
74+
public required string ItemsSourceBindingPath { get; set; }
4575

4676
/// <summary>
4777
/// Should look for i18n keys in the items source

src/Everywhere/Entrance.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,25 @@ public static class Entrance
1414

1515
public static void Initialize(string[] args)
1616
{
17-
InitializeApplicationMutex();
17+
InitializeApplicationMutex(args);
1818
InitializeErrorHandling();
1919
}
2020

21-
private static void InitializeApplicationMutex()
21+
private static void InitializeApplicationMutex(string[] args)
2222
{
2323
#if !DEBUG // axaml designer may launch this code, so we need to set it to null.
2424
AppMutex = new Mutex(true, "EverywhereAppMutex", out var createdNew);
2525
if (!createdNew)
2626
{
27-
NativeMessageBox.Show(
28-
"Info",
29-
"Everywhere is already running. Please check your system tray for the application window.",
30-
NativeMessageBoxButtons.Ok,
31-
NativeMessageBoxIcon.Information);
27+
if (!args.Contains("--autorun"))
28+
{
29+
NativeMessageBox.Show(
30+
"Info",
31+
"Everywhere is already running. Please check your system tray for the application window.",
32+
NativeMessageBoxButtons.Ok,
33+
NativeMessageBoxIcon.Information);
34+
}
35+
3236
Environment.Exit(0);
3337
}
3438
#endif

src/Everywhere/I18N/Strings.resx

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
<data name="Settings_Common_Language_Header" xml:space="preserve">
3232
<value>Language</value>
3333
</data>
34-
<data name="SettingsCategory_Appearance_Header" xml:space="preserve">
35-
<value>Appearance Settings</value>
34+
<data name="SettingsCategory_Common_Header" xml:space="preserve">
35+
<value>Common Settings</value>
3636
</data>
3737
<data name="Settings_Common_Language_Description" xml:space="preserve">
3838
<value>Select the display language for the software.</value>
@@ -73,18 +73,6 @@
7373
<data name="Settings_Model_ModelName_Description" xml:space="preserve">
7474
<value>Specify the name of the model the assistant will use.</value>
7575
</data>
76-
<data name="Settings_Model_Endpoint_Header" xml:space="preserve">
77-
<value>Model API Endpoint</value>
78-
</data>
79-
<data name="Settings_Model_Endpoint_Description" xml:space="preserve">
80-
<value>Specify the base API endpoint URL for the assistant; supply only the root path without including specific endpoint routes (e.g., https://openai.com/api/v1).</value>
81-
</data>
82-
<data name="Settings_Model_ApiKey_Header" xml:space="preserve">
83-
<value>Model API Key</value>
84-
</data>
85-
<data name="Settings_Model_ApiKey_Description" xml:space="preserve">
86-
<value>Specify the API access key for the model, obtainable from the provider’s API site; leave blank for local models (e.g., Ollama or LM Studio) that do not require authentication.</value>
87-
</data>
8876
<data name="Settings_Model_Temperature_Header" xml:space="preserve">
8977
<value>Creativity (temperature)</value>
9078
</data>
@@ -617,4 +605,25 @@ Tip: You can reopen this message by clicking “About - Welcome Page”.</value>
617605
<data name="Settings_Model_SelectedModelProvider_Schema_Description" xml:space="preserve">
618606
<value>Select API schema. Leaving this to default if you dont't know what to do.</value>
619607
</data>
608+
<data name="Settings_Common_IsStartupEnabled_Header" xml:space="preserve">
609+
<value>Run at Startup</value>
610+
</data>
611+
<data name="Settings_Common_IsStartupEnabled_Description" xml:space="preserve">
612+
<value>Everywhere will run automatically after startup. If “Startup as administrator” is enabled, the current option can only be disabled in administrator mode.</value>
613+
</data>
614+
<data name="Settings_Common_IsAdministratorStartupEnabled_Header" xml:space="preserve">
615+
<value>Startup as Administrator</value>
616+
</data>
617+
<data name="Settings_Common_IsAdministratorStartupEnabled_Description" xml:space="preserve">
618+
<value>Everywhere will start automatically as administrator. You can only turn this option on or off in administrator mode.</value>
619+
</data>
620+
<data name="Settings_Common_RestartAsAdministrator_Description" xml:space="preserve">
621+
<value>Due to operating system restrictions, when running any application as an administrator (also referred to as elevated permissions), Everywhere may not work correctly when the elevated applications are in focus or trying to interact with a Everywhere feature like "Pick Element". This can be addressed by also running Everywhere as administrator.</value>
622+
</data>
623+
<data name="Settings_Common_RestartAsAdministrator_Header" xml:space="preserve">
624+
<value>Restart as Administrator</value>
625+
</data>
626+
<data name="Settings_Common_RestartAsAdministrator_Button_Content" xml:space="preserve">
627+
<value>Restart</value>
628+
</data>
620629
</root>

src/Everywhere/I18N/Strings.zh-hans.resx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
<resheader name="writer">
1212
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
1313
</resheader>
14-
<data name="SettingsCategory_Appearance_Header" xml:space="preserve">
15-
<value>外观设置</value>
14+
<data name="SettingsCategory_Common_Header" xml:space="preserve">
15+
<value>通用设置</value>
1616
</data>
1717
<data name="SettingsPage_Saved_Toast_Title" xml:space="preserve">
1818
<value>设置已保存</value>
@@ -585,4 +585,25 @@
585585
<data name="Settings_Model_SelectedModelProvider_Schema_Description" xml:space="preserve">
586586
<value>选择 API 的模式。如果你不确定这是什么,请保持默认值。</value>
587587
</data>
588+
<data name="Settings_Common_IsStartupEnabled_Header" xml:space="preserve">
589+
<value>开机启动</value>
590+
</data>
591+
<data name="Settings_Common_IsStartupEnabled_Description" xml:space="preserve">
592+
<value>Everywhere 会在开机后自动运行。如果开启了“以管理员方式启动”,则只有在管理员模式下才能关闭当前选项。</value>
593+
</data>
594+
<data name="Settings_Common_IsAdministratorStartupEnabled_Header" xml:space="preserve">
595+
<value>以管理员方式启动</value>
596+
</data>
597+
<data name="Settings_Common_IsAdministratorStartupEnabled_Description" xml:space="preserve">
598+
<value>Everywhere 会以管理员方式自启动。只有在管理员模式下才能开关当前选项。</value>
599+
</data>
600+
<data name="Settings_Common_RestartAsAdministrator_Header" xml:space="preserve">
601+
<value>以管理员身份重启</value>
602+
</data>
603+
<data name="Settings_Common_RestartAsAdministrator_Description" xml:space="preserve">
604+
<value>由于操作系统限制,当以管理员身份运行任何应用程序(也称为提升权限)时,如果提升权限的应用程序处于活动状态或试图与 Everywhere 的某个功能(如“选择元素”)交互,Everywhere 可能无法正常工作。可以通过同时以管理员身份运行 Everywhere 来解决此问题。</value>
605+
</data>
606+
<data name="Settings_Common_RestartAsAdministrator_Button_Content" xml:space="preserve">
607+
<value>重启</value>
608+
</data>
588609
</root>

0 commit comments

Comments
 (0)