Skip to content

Commit 9e41911

Browse files
committed
Compat
1 parent f71ad0e commit 9e41911

51 files changed

Lines changed: 4315 additions & 3353 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Amethyst.Contract/Amethyst.Contract.csproj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Nullable>enable</Nullable>
77
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
88
<Title>Amethyst Plugin API Contract</Title>
9-
<Version>1.0.0</Version>
9+
<Version>2.0.0</Version>
1010
<Platforms>x64</Platforms>
1111
</PropertyGroup>
1212

@@ -26,6 +26,14 @@
2626
<PlatformTarget>AnyCPU</PlatformTarget>
2727
<AllowUnsafeBlocks>False</AllowUnsafeBlocks>
2828
</PropertyGroup>
29+
30+
<ItemGroup>
31+
<PackageReference Include="Avalonia" Version="11.3.2"/>
32+
<PackageReference Include="Avalonia.Labs.Controls" Version="11.3.1" />
33+
<PackageReference Include="Xaml.Behaviors.Avalonia" Version="11.3.2"/>
34+
<PackageReference Include="Newtonsoft.Json" Version="13.0.1"/>
35+
<PackageReference Include="Splat" Version="16.2.1"/>
36+
</ItemGroup>
2937

3038
<ItemGroup>
3139
<None Include="Assets\ktvr.png" Pack="true" PackagePath="\"/>

Amethyst.Contract/Contract.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.ComponentModel;
33
using System.Numerics;
44
using System.Runtime.CompilerServices;
5+
using Avalonia.Platform.Storage;
56

67
namespace Amethyst.Contract;
78

@@ -337,6 +338,11 @@ public Task ProcessKeyInput(IKeyInputAction action, object? data,
337338
/// </summary>
338339
public interface IAmethystHost
339340
{
341+
/// <summary>
342+
/// Get the app storage handler
343+
/// </summary>
344+
IStorageProvider StorageProvider { get; }
345+
340346
/// <summary>
341347
/// Helper to get all joints' positions from the app, which are added in Amethyst.
342348
/// Note: if joint's off, its trackingState will be ITrackedJointState::State_NotTracked
@@ -537,6 +543,11 @@ public interface ILocalizationHost
537543
/// </summary>
538544
string DocsLanguageCode { get; }
539545

546+
/// <summary>
547+
/// Get the app storage handler
548+
/// </summary>
549+
IStorageProvider StorageProvider { get; }
550+
540551
/// <summary>
541552
/// Log a message to Amethyst logs : handler
542553
/// </summary>

Amethyst.Desktop/Program.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ public static class AppBuilderExtensions
3434

3535
public static AppBuilder SetupDesktopNotifications(this AppBuilder builder, out INotificationManager manager)
3636
{
37-
if (OperatingSystem.IsWindows()) Shell = new SystemShell();
37+
if (OperatingSystem.IsWindows()) Shell = new SystemShellWindows();
38+
else if (OperatingSystem.IsMacCatalyst() || OperatingSystem.IsMacOS()) Shell = new SystemShellMac();
39+
else if (OperatingSystem.IsLinux()) Shell = new SystemShellLinux();
3840

3941
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
4042
switch (Environment.OSVersion.Platform)
@@ -66,7 +68,7 @@ public static AppBuilder SetupDesktopNotifications(this AppBuilder builder, out
6668
Locator.CurrentMutable.Register(() => Shell);
6769
if (b.Instance?.ApplicationLifetime is IControlledApplicationLifetime lifetime)
6870
{
69-
lifetime.Exit += (s, e) => { notificationManager?.Dispose(); };
71+
lifetime.Exit += (_, _) => { notificationManager?.Dispose(); };
7072
}
7173
});
7274

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Runtime.InteropServices;
7+
using Avalonia;
8+
using Avalonia.Media;
9+
using Avalonia.Media.Imaging;
10+
11+
namespace Amethyst.Desktop;
12+
13+
public class SystemShellLinux : ISystemShell
14+
{
15+
public uint TimeBeginPeriod(uint uMilliseconds)
16+
{
17+
return 0; // No-op
18+
}
19+
20+
public uint TimeEndPeriod(uint uMilliseconds)
21+
{
22+
return 0; // No-op
23+
}
24+
25+
public void OpenFolderAndSelectItem(string path)
26+
{
27+
if (string.IsNullOrWhiteSpace(path))
28+
return;
29+
30+
path = Path.GetFullPath(path);
31+
string folder = File.Exists(path) ? Path.GetDirectoryName(path)! : path;
32+
33+
// Try Nautilus
34+
if (CommandExists("nautilus"))
35+
{
36+
TryStartProcess("nautilus", $"--select \"{path}\"");
37+
return;
38+
}
39+
40+
// Try Dolphin
41+
if (CommandExists("dolphin"))
42+
{
43+
TryStartProcess("dolphin", $"--select \"{path}\"");
44+
return;
45+
}
46+
47+
// Fallback: open the folder only
48+
TryStartProcess("xdg-open", $"\"{folder}\"");
49+
}
50+
51+
public void OpenFolderAndSelectItem(Uri path)
52+
{
53+
if (path == null) return;
54+
OpenFolderAndSelectItem(path.LocalPath);
55+
}
56+
57+
public Bitmap GetFileIcon(string path)
58+
{
59+
const int size = 32;
60+
61+
if (string.IsNullOrEmpty(path))
62+
throw new ArgumentNullException(nameof(path));
63+
64+
path = Path.GetFullPath(path);
65+
66+
// If it's an image file, load it directly
67+
if (IsImage(path))
68+
{
69+
try
70+
{
71+
using var stream = File.OpenRead(path);
72+
return new Bitmap(stream);
73+
}
74+
catch
75+
{
76+
// fallback to placeholder
77+
}
78+
}
79+
80+
// Otherwise, generate a simple placeholder icon
81+
var bitmap = new RenderTargetBitmap(new PixelSize(size, size), new Vector(96, 96));
82+
using var ctx = bitmap.CreateDrawingContext();
83+
84+
// Draw background
85+
ctx.FillRectangle(Brushes.LightGray, new Rect(0, 0, size, size));
86+
87+
// Draw border
88+
ctx.DrawRectangle(new Pen(Brushes.Black), new Rect(0, 0, size, size));
89+
90+
// Draw X
91+
ctx.DrawLine(new Pen(Brushes.Black), new Point(0, 0), new Point(size, size));
92+
ctx.DrawLine(new Pen(Brushes.Black), new Point(size, 0), new Point(0, size));
93+
94+
return bitmap;
95+
}
96+
97+
/// <summary>
98+
/// Returns a list of processes that currently have <paramref name="path"/> open.
99+
/// Uses the 'lsof' command which is standard on most Linux distributions.
100+
/// </summary>
101+
public List<Process> WhoIsLocking(string path)
102+
{
103+
var results = new List<Process>();
104+
105+
if (!File.Exists(path))
106+
{
107+
if (!Directory.Exists(path))
108+
return results;
109+
110+
// Recursively check all files in a directory
111+
foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
112+
results.AddRange(WhoIsLocking(file));
113+
114+
return results.GroupBy(p => p.Id).Select(g => g.First()).ToList();
115+
}
116+
117+
try
118+
{
119+
var psi = new ProcessStartInfo
120+
{
121+
FileName = "/usr/bin/lsof",
122+
Arguments = $"-F p -- \"{path}\"",
123+
RedirectStandardOutput = true,
124+
RedirectStandardError = true,
125+
UseShellExecute = false
126+
};
127+
128+
using var proc = Process.Start(psi);
129+
if (proc == null)
130+
return results;
131+
132+
var output = proc.StandardOutput.ReadToEnd();
133+
proc.WaitForExit();
134+
135+
foreach (var line in output.Split('\n', StringSplitOptions.RemoveEmptyEntries))
136+
{
137+
if (line.StartsWith('p') && int.TryParse(line.AsSpan(1), out var pid))
138+
{
139+
try
140+
{
141+
results.Add(Process.GetProcessById(pid));
142+
}
143+
catch
144+
{
145+
// Process may have exited in the meantime
146+
}
147+
}
148+
}
149+
}
150+
catch
151+
{
152+
// lsof missing or other failure – return what we have
153+
}
154+
155+
return results;
156+
}
157+
158+
/// <summary>
159+
/// Checks whether the current process is running as root (effective UID 0).
160+
/// </summary>
161+
public bool IsCurrentProcessElevated()
162+
{
163+
return GetEuid() == 0;
164+
}
165+
166+
/// <summary>
167+
/// Checks whether a specific process is running as root.
168+
/// </summary>
169+
public bool IsProcessElevated(Process process)
170+
{
171+
try
172+
{
173+
// Use /proc/<pid>/status to read Uid line
174+
var statusFile = $"/proc/{process.Id}/status";
175+
if (!File.Exists(statusFile)) return false;
176+
177+
foreach (var line in File.ReadLines(statusFile))
178+
{
179+
if (line.StartsWith("Uid:"))
180+
{
181+
// Format: Uid: real effective saved fs
182+
var parts = line.Split('\t', StringSplitOptions.RemoveEmptyEntries);
183+
if (parts.Length >= 3 && int.TryParse(parts[2], out var effectiveUid))
184+
return effectiveUid == 0;
185+
}
186+
}
187+
}
188+
catch
189+
{
190+
// Fall through
191+
}
192+
193+
return false;
194+
}
195+
196+
/// <summary>
197+
/// Returns the executable path of the given process, if permitted.
198+
/// </summary>
199+
public string GetProcessFilename(Process p)
200+
{
201+
try
202+
{
203+
// /proc/<pid>/exe is a symlink to the executable
204+
return Path.GetFullPath($"/proc/{p.Id}/exe");
205+
}
206+
catch
207+
{
208+
return null;
209+
}
210+
}
211+
212+
/// <summary>
213+
/// Returns the default Steam installation directory on Linux.
214+
/// Checks ~/.local/share/Steam and the common flatpak path.
215+
/// </summary>
216+
public string GetSteamInstallDirectory()
217+
{
218+
var home = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
219+
string[] candidates =
220+
{
221+
Path.Combine(home, ".local/share/Steam"), Path.Combine(home, ".steam/steam"),
222+
Path.Combine(home, ".var/app/com.valvesoftware.Steam/.local/share/Steam") // Flatpak
223+
};
224+
225+
foreach (var c in candidates)
226+
{
227+
if (Directory.Exists(c))
228+
return c;
229+
}
230+
231+
throw new DirectoryNotFoundException("Steam installation directory not found!");
232+
}
233+
234+
// POSIX getuid/euid
235+
[DllImport("libc")]
236+
private static extern uint geteuid();
237+
private static uint GetEuid() => geteuid();
238+
239+
private static void TryStartProcess(string fileName, string args)
240+
{
241+
try
242+
{
243+
Process.Start(new ProcessStartInfo
244+
{
245+
FileName = fileName,
246+
Arguments = args,
247+
UseShellExecute = false
248+
});
249+
}
250+
catch
251+
{
252+
// Ignore failures (command not found, etc.)
253+
}
254+
}
255+
256+
private static bool CommandExists(string command)
257+
{
258+
try
259+
{
260+
var psi = new ProcessStartInfo
261+
{
262+
FileName = "which",
263+
Arguments = command,
264+
RedirectStandardOutput = true,
265+
UseShellExecute = false
266+
};
267+
using var proc = Process.Start(psi);
268+
proc?.WaitForExit();
269+
return proc?.ExitCode == 0;
270+
}
271+
catch
272+
{
273+
return false;
274+
}
275+
}
276+
277+
private static bool IsImage(string filePath)
278+
{
279+
string ext = Path.GetExtension(filePath).ToLowerInvariant();
280+
return ext is ".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".tiff" or ".webp";
281+
}
282+
}

0 commit comments

Comments
 (0)