using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Threading; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: InternalsVisibleTo("WristHub")] [assembly: InternalsVisibleTo("BoneHub.Tests")] [assembly: TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")] [assembly: AssemblyCompany("WristHub.SDK")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Stable public app and control API for WristHubOS.")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("4.0.1")] [assembly: AssemblyProduct("WristHub.SDK")] [assembly: AssemblyTitle("WristHub.SDK")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace WristHub.SDK; public static class WristHubApi { public const int ApiVersion = 2; private static readonly object Sync = new object(); private static readonly Dictionary Apps = new Dictionary(StringComparer.OrdinalIgnoreCase); private static IWristHubHost? _host; public static bool IsWristHubAvailable { get { lock (Sync) { return _host != null; } } } public static WristHubRuntimeInfo Runtime { get { lock (Sync) { return _host?.Runtime ?? WristHubRuntimeInfo.Unavailable; } } } public static int RegisteredAppCount { get { lock (Sync) { return Apps.Count; } } } public static event Action? AppsChanged; public static event Action? RuntimeChanged; public static WristHubApp RegisterApp(WristHubAppDefinition definition) { ArgumentNullException.ThrowIfNull(definition, "definition"); definition.Validate(); WristHubApp wristHubApp; lock (Sync) { if (Apps.ContainsKey(definition.Id)) { throw new InvalidOperationException("A WristHub app with id '" + definition.Id + "' is already registered."); } wristHubApp = new WristHubApp(definition, OnAppChanged, Unregister); Apps.Add(definition.Id, wristHubApp); } SafeRaise(WristHubApi.AppsChanged); return wristHubApp; } public static WristHubApp RegisterApp(string id, string name, string author, string version, string description = "", string icon = "APP") { return RegisterApp(new WristHubAppDefinition(id, name, author, version, description, icon)); } public static IReadOnlyList GetRegisteredApps() { lock (Sync) { return new ReadOnlyCollection(Apps.Values.OrderBy((WristHubApp app) => app.Name, StringComparer.OrdinalIgnoreCase).ToArray()); } } public static void Notify(string appId, string message, WristHubNotificationKind kind = WristHubNotificationKind.Info) { if (string.IsNullOrWhiteSpace(message)) { return; } IWristHubHost host; lock (Sync) { host = _host; } try { host?.Notify(appId?.Trim() ?? string.Empty, message.Trim(), kind); } catch { } } public static void OpenApp(string appId) { IWristHubHost host; lock (Sync) { host = _host; } try { host?.RequestOpen(appId?.Trim() ?? string.Empty); } catch { } } internal static void AttachHost(IWristHubHost host) { ArgumentNullException.ThrowIfNull(host, "host"); lock (Sync) { _host = host; } SafeRaise(WristHubApi.RuntimeChanged, host.Runtime); SafeRaise(WristHubApi.AppsChanged); } internal static void DetachHost(IWristHubHost host) { lock (Sync) { if (_host != host) { return; } _host = null; } SafeRaise(WristHubApi.RuntimeChanged, WristHubRuntimeInfo.Unavailable); } private static void Unregister(WristHubApp app) { lock (Sync) { if (Apps.TryGetValue(app.Id, out WristHubApp value) && value == app) { Apps.Remove(app.Id); } } SafeRaise(WristHubApi.AppsChanged); } private static void OnAppChanged() { SafeRaise(WristHubApi.AppsChanged); } private static void SafeRaise(Action? callback) { if (callback == null) { return; } Delegate[] invocationList = callback.GetInvocationList(); for (int i = 0; i < invocationList.Length; i++) { Action action = (Action)invocationList[i]; try { action(); } catch { } } } private static void SafeRaise(Action? callback, T value) { if (callback == null) { return; } Delegate[] invocationList = callback.GetInvocationList(); for (int i = 0; i < invocationList.Length; i++) { Action action = (Action)invocationList[i]; try { action(value); } catch { } } } } internal interface IWristHubHost { WristHubRuntimeInfo Runtime { get; } void Notify(string appId, string message, WristHubNotificationKind kind); void RequestOpen(string appId); } public readonly record struct WristHubRuntimeInfo(bool Available, bool IsQuest, bool IsFusionConnected, string Theme, int ApiVersion) { public static WristHubRuntimeInfo Unavailable => new WristHubRuntimeInfo(Available: false, IsQuest: false, IsFusionConnected: false, "Cyan", 2); } public enum WristHubNotificationKind { Info, Success, Warning, Error } public enum WristHubAccent { Theme, Neutral, Success, Warning, Danger } public enum WristHubCommunityCategory { Utility, Gameplay, Social, Accessibility, Developer, Other } public sealed record WristHubCommunityProfile(WristHubCommunityCategory Category, string Summary, string Homepage = "", string SupportContact = "", IReadOnlyList? Tags = null) { internal void Validate() { WristHubAppDefinition.ValidateText(Summary, "Summary", 160); ValidateOptional(Homepage, "Homepage", 240); ValidateOptional(SupportContact, "SupportContact", 96); IReadOnlyList tags = Tags; if (tags != null && tags.Count > 8) { throw new ArgumentOutOfRangeException("Tags"); } if (Tags == null) { return; } foreach (string tag in Tags) { WristHubAppDefinition.ValidateText(tag, "Tags", 24); } } private static void ValidateOptional(string? value, string name, int length) { if (!string.IsNullOrEmpty(value) && value.Length > length) { throw new ArgumentOutOfRangeException(name); } } } public sealed record WristHubAppDefinition(string Id, string Name, string Author, string Version, string Description = "", string Icon = "APP") { internal void Validate() { ValidateToken(Id, "Id", 64); ValidateText(Name, "Name", 48); ValidateText(Author, "Author", 48); ValidateText(Version, "Version", 24); string description = Description; if (description != null && description.Length > 240) { throw new ArgumentOutOfRangeException("Description"); } string icon = Icon; if (icon != null && icon.Length > 8) { throw new ArgumentOutOfRangeException("Icon"); } } internal static void ValidateToken(string value, string name, int length) { ValidateText(value, name, length); if (value.Any(delegate(char character) { bool flag = char.IsLetterOrDigit(character); if (!flag) { bool flag2 = ((character == '-' || character == '.' || character == '_') ? true : false); flag = flag2; } return !flag; })) { throw new ArgumentException("Use only letters, numbers, dots, dashes, and underscores.", name); } } internal static void ValidateText(string value, string name, int length) { if (string.IsNullOrWhiteSpace(value)) { throw new ArgumentException("Value is required.", name); } if (value.Length > length) { throw new ArgumentOutOfRangeException(name); } } } public sealed class WristHubApp : IDisposable { private readonly Action _changed; private readonly Action _dispose; private int _disposed; public string Id { get; } public string Name { get; } public string Author { get; } public string Version { get; } public string Description { get; } public string Icon { get; } public WristHubPage Root { get; } public bool IsDisposed => _disposed != 0; public Func? IsAvailable { get; set; } public Func? NotificationCount { get; set; } public WristHubCommunityProfile? Community { get; private set; } public bool Available { get { try { return !IsDisposed && (IsAvailable?.Invoke() ?? true); } catch { return false; } } } public int Notifications { get { try { return Math.Clamp(NotificationCount?.Invoke() ?? 0, 0, 99); } catch { return 0; } } } internal WristHubApp(WristHubAppDefinition definition, Action changed, Action dispose) { Id = definition.Id.Trim(); Name = definition.Name.Trim(); Author = definition.Author.Trim(); Version = definition.Version.Trim(); Description = definition.Description?.Trim() ?? string.Empty; Icon = (string.IsNullOrWhiteSpace(definition.Icon) ? "APP" : definition.Icon.Trim()); _changed = changed; _dispose = dispose; Root = new WristHubPage("root", Name, null, changed); } public WristHubApp PublishToCommunity(WristHubCommunityProfile profile) { ArgumentNullException.ThrowIfNull(profile, "profile"); profile.Validate(); Community = profile with { Summary = profile.Summary.Trim(), Homepage = (profile.Homepage?.Trim() ?? string.Empty), SupportContact = (profile.SupportContact?.Trim() ?? string.Empty), Tags = profile.Tags?.Select((string tag) => tag.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray() }; _changed(); return this; } public void RemoveFromCommunity() { if (!(Community == null)) { Community = null; _changed(); } } public void Refresh() { _changed(); } public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) == 0) { _dispose(this); } } } public sealed class WristHubPage { private readonly object _sync = new object(); private readonly List _controls = new List(); private readonly Action _changed; public string Id { get; } public string Title { get; set; } public WristHubPage? Parent { get; } public IReadOnlyList Controls { get { lock (_sync) { return _controls.ToArray(); } } } internal WristHubPage(string id, string title, WristHubPage? parent, Action changed) { WristHubAppDefinition.ValidateToken(id, "id", 64); WristHubAppDefinition.ValidateText(title, "title", 64); Id = id.Trim(); Title = title.Trim(); Parent = parent; _changed = changed; } public WristHubPage AddPage(string id, string title, string description = "") { WristHubPage wristHubPage = new WristHubPage(id, title, this, _changed); Add(new WristHubPageLink(id, title, wristHubPage, description)); return wristHubPage; } public WristHubAction AddButton(string id, string label, Action action, string description = "", WristHubAccent accent = WristHubAccent.Theme) { return Add(new WristHubAction(id, label, action, description, accent)); } public WristHubToggle AddToggle(string id, string label, Func read, Action write, string description = "") { return Add(new WristHubToggle(id, label, read, write, description)); } public WristHubSlider AddSlider(string id, string label, double minimum, double maximum, double step, Func read, Action write, string description = "") { return Add(new WristHubSlider(id, label, minimum, maximum, step, read, write, description)); } public WristHubChoice AddChoice(string id, string label, IReadOnlyList choices, Func read, Action write, string description = "") { return Add(new WristHubChoice(id, label, choices, read, write, description)); } public WristHubTextInput AddText(string id, string label, Func read, Action write, string description = "") { return Add(new WristHubTextInput(id, label, read, write, description)); } public WristHubColorInput AddColor(string id, string label, Func read, Action write, IReadOnlyList? palette = null, string description = "") { return Add(new WristHubColorInput(id, label, read, write, palette, description)); } public WristHubLabel AddLabel(string id, string label, Func read, string description = "") { return Add(new WristHubLabel(id, label, read, description)); } public T Add(T control) where T : WristHubControl { ArgumentNullException.ThrowIfNull(control, "control"); lock (_sync) { if (_controls.Any((WristHubControl existing) => existing.Id.Equals(control.Id, StringComparison.OrdinalIgnoreCase))) { throw new InvalidOperationException($"Control id '{control.Id}' already exists on page '{Id}'."); } _controls.Add(control); } _changed(); return control; } public bool Remove(string id) { bool flag; lock (_sync) { flag = _controls.RemoveAll((WristHubControl control) => control.Id.Equals(id, StringComparison.OrdinalIgnoreCase)) > 0; } if (flag) { _changed(); } return flag; } } public abstract class WristHubControl { public string Id { get; } public string Label { get; set; } public string Description { get; set; } public WristHubAccent Accent { get; set; } public Func? IsEnabled { get; set; } public bool Enabled { get { try { return IsEnabled?.Invoke() ?? true; } catch { return false; } } } protected WristHubControl(string id, string label, string description, WristHubAccent accent = WristHubAccent.Theme) { WristHubAppDefinition.ValidateToken(id, "id", 64); WristHubAppDefinition.ValidateText(label, "label", 64); Id = id.Trim(); Label = label.Trim(); Description = description?.Trim() ?? string.Empty; Accent = accent; } } public sealed class WristHubAction : WristHubControl { private readonly Action _action; internal WristHubAction(string id, string label, Action action, string description, WristHubAccent accent) : base(id, label, description, accent) { _action = action ?? throw new ArgumentNullException("action"); } public void Invoke() { _action(); } } public sealed class WristHubToggle : WristHubControl { private readonly Func _read; private readonly Action _write; public bool Value => _read(); internal WristHubToggle(string id, string label, Func read, Action write, string description) : base(id, label, description) { _read = read; _write = write; } public void Toggle() { _write(!Value); } } public sealed class WristHubSlider : WristHubControl { private readonly Func _read; private readonly Action _write; public double Minimum { get; } public double Maximum { get; } public double Step { get; } public double Value => Math.Clamp(_read(), Minimum, Maximum); internal WristHubSlider(string id, string label, double min, double max, double step, Func read, Action write, string description) : base(id, label, description) { if (!double.IsFinite(min) || !double.IsFinite(max) || max <= min) { throw new ArgumentOutOfRangeException("max"); } if (!double.IsFinite(step) || step <= 0.0) { throw new ArgumentOutOfRangeException("step"); } Minimum = min; Maximum = max; Step = step; _read = read; _write = write; } public void Adjust(int direction) { _write(Math.Clamp(Value + (double)Math.Sign(direction) * Step, Minimum, Maximum)); } } public sealed class WristHubChoice : WristHubControl { private readonly Func _read; private readonly Action _write; public IReadOnlyList Choices { get; } public int Index => Math.Clamp(_read(), 0, Choices.Count - 1); public string Value => Choices[Index]; internal WristHubChoice(string id, string label, IReadOnlyList choices, Func read, Action write, string description) : base(id, label, description) { Choices = (from choice in choices?.Where((string choice) => !string.IsNullOrWhiteSpace(choice)) select choice.Trim()).ToArray() ?? throw new ArgumentNullException("choices"); if (Choices.Count == 0) { throw new ArgumentException("At least one choice is required.", "choices"); } _read = read; _write = write; } public void Adjust(int direction) { _write((Index + Math.Sign(direction) + Choices.Count) % Choices.Count); } } public sealed class WristHubTextInput : WristHubControl { private readonly Func _read; private readonly Action _write; public string Value => _read() ?? string.Empty; internal WristHubTextInput(string id, string label, Func read, Action write, string description) : base(id, label, description) { _read = read; _write = write; } public void Set(string value) { _write(value ?? string.Empty); } } public readonly record struct WristHubColorValue(byte Red, byte Green, byte Blue, byte Alpha = byte.MaxValue) { public string Hex => $"#{Red:X2}{Green:X2}{Blue:X2}{Alpha:X2}"; public static WristHubColorValue Cyan => new WristHubColorValue(38, 221, byte.MaxValue); public static WristHubColorValue Graphite => new WristHubColorValue(173, 184, 194); public static WristHubColorValue Crimson => new WristHubColorValue(byte.MaxValue, 61, 79); public static WristHubColorValue Emerald => new WristHubColorValue(61, 240, 140); public static WristHubColorValue Violet => new WristHubColorValue(173, 107, byte.MaxValue); public static WristHubColorValue Amber => new WristHubColorValue(byte.MaxValue, 168, 46); } public sealed class WristHubColorInput : WristHubControl { private static readonly WristHubColorValue[] DefaultPalette = new WristHubColorValue[6] { WristHubColorValue.Cyan, WristHubColorValue.Graphite, WristHubColorValue.Crimson, WristHubColorValue.Emerald, WristHubColorValue.Violet, WristHubColorValue.Amber }; private readonly Func _read; private readonly Action _write; public IReadOnlyList Palette { get; } public WristHubColorValue Value => _read(); internal WristHubColorInput(string id, string label, Func read, Action write, IReadOnlyList? palette, string description) : base(id, label, description) { _read = read; _write = write; Palette = ((palette != null && palette.Count > 0) ? palette.ToArray() : DefaultPalette); } public void Set(WristHubColorValue value) { _write(value); } public void Adjust(int direction) { WristHubColorValue value = Value; int val = -1; for (int i = 0; i < Palette.Count; i++) { if (Palette[i].Equals(value)) { val = i; break; } } val = (Math.Max(0, val) + Math.Sign(direction) + Palette.Count) % Palette.Count; _write(Palette[val]); } } public sealed class WristHubLabel : WristHubControl { private readonly Func _read; public string Value => _read() ?? string.Empty; internal WristHubLabel(string id, string label, Func read, string description) : base(id, label, description, WristHubAccent.Neutral) { _read = read; } } public sealed class WristHubPageLink : WristHubControl { public WristHubPage Page { get; } internal WristHubPageLink(string id, string label, WristHubPage page, string description) : base(id, label, description) { Page = page; } }