Nyxo Code · مفتوح للقراءة
اقرأ كل سطر من Nyxo Code
Nyxo Code تطبيق صغير لنظام Windows. يسجّل دخولك برمز الدخول، ويشغّل Claude Code في المجلد الذي تختاره متصلًا بـ Nyxo، ويعرض استهلاكك مباشرة. تعرض هذه الصفحة شيفرته المصدرية كاملة، لترى بالضبط ما يفعله قبل تشغيله.
النسخة الرسميةNyxo Code.exe · 291,328 بايت
الشيفرة المصدرية8 ملفات C# + سكربت البناء · 4,812 سطر
يعمل علىWindows 10 / 11, .NET Framework 4.8
بصمة SHA-256 لملف Nyxo Code.exe الرسمي
2893C03CACF0A9439ABA167DE714D8005819DCD021BB57A30087D31C38593377ملفات المصدر
Core.csالاتصالات بالخادم، ورموز الدخول المحفوظة (Windows DPAPI)، والعثور على وكيل البرمجة وتشغيله.704 سطر
// Nyxo Code: a small Windows app that opens Claude Code with the Nyxo server as its only model provider.
// It changes no saved Claude Code settings; the only write to Claude Code's files is a "seen" flag that hides
// Claude Code's one-time auto-mode announcement. Passcodes you choose to save are kept in %APPDATA%\Nyxo\passcodes.json,
// encrypted with Windows (DPAPI) so only your Windows account on this PC can read them (same file and format
// as the older Nyxo Code.cmd). %APPDATA%\Nyxo also holds claude-path.txt (a Claude Code you located by hand),
// recent-folders.txt, error.log (unexpected errors) and launch\ (the settings of each open Claude Code
// window, with no secrets in them; each file is removed once its window closes).
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Web.Script.Serialization;
using System.Windows.Forms;
[assembly: AssemblyTitle("Nyxo Code")]
[assembly: AssemblyDescription("Opens Claude Code with the Nyxo server")]
[assembly: AssemblyCompany("Nyxo")]
[assembly: AssemblyProduct("Nyxo Code")]
[assembly: AssemblyCopyright("Nyxo")]
[assembly: AssemblyVersion(Nyxo.Version + ".0")]
[assembly: AssemblyFileVersion(Nyxo.Version + ".0")]
static class Nyxo
{
public const string Version = "1.0.6";
public const string Server = "https://nyxo.space/app/code";
const string Model = "claude-opus-5-5";
public const string InstallUrl = "https://docs.claude.com/en/docs/claude-code";
static readonly string Dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Nyxo");
static readonly string Store = Path.Combine(Dir, "passcodes.json");
static readonly string ClaudeSaved = Path.Combine(Dir, "claude-path.txt");
static readonly string RecentFile = Path.Combine(Dir, "recent-folders.txt");
public static readonly JavaScriptSerializer Json = new JavaScriptSerializer();
public static readonly CultureInfo Inv = CultureInfo.InvariantCulture;
[DllImport("user32.dll")] static extern bool SetProcessDPIAware();
[STAThread]
static void Main()
{
// TLS 1.2 + 1.3, named explicitly: this build has no target-framework attribute, so .NET runs it in legacy mode,
// where SystemDefault is rejected and every connection fails.
try { ServicePointManager.SecurityProtocol = (SecurityProtocolType)(3072 | 12288); }
catch { ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; }
ServicePointManager.Expect100Continue = false;
ServicePointManager.DefaultConnectionLimit = 8; // the usage stream holds one connection open all the time
try { SetProcessDPIAware(); } catch { }
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
T.Init();
L.Load();
Application.ThreadException += (s, e) =>
{
LogError(e.Exception);
Dlg.Ask(null, "Nyxo Code", L.T("Something went wrong:") + " " + e.Exception.Message, null, L.T("OK"), null, false);
};
// An error on a background thread ends the app: it is logged, and explained in the same dialog first.
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
{
var ex = e.ExceptionObject as Exception;
LogError(ex);
try
{
string msg = L.T("Nyxo Code ran into a problem and has to close:") + " " + (ex != null ? ex.Message : Convert.ToString(e.ExceptionObject, Inv));
var t = new System.Threading.Thread(() => { try { Dlg.Ask(null, "Nyxo Code", msg, null, L.T("OK"), null, true); } catch { } });
t.SetApartmentState(System.Threading.ApartmentState.STA);
t.Start();
t.Join(TimeSpan.FromMinutes(5));
}
catch { }
};
CleanLaunchFiles();
Application.Run(new MainForm());
BuyForm.ForgetClipboard();
WaitBackground(2500);
}
// ---------- error log and background work that should finish before the app exits ----------
static readonly string ErrorLog = Path.Combine(Dir, "error.log");
public static void LogError(Exception ex)
{
if (ex == null) return;
try
{
Directory.CreateDirectory(Dir);
var fi = new FileInfo(ErrorLog);
if (fi.Exists && fi.Length > 256 * 1024) fi.Delete();
File.AppendAllText(ErrorLog, DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss", Inv) + "Z Nyxo Code " + Version + "\r\n" + ex + "\r\n\r\n");
}
catch { }
}
static readonly List<System.Threading.Tasks.Task> pending = new List<System.Threading.Tasks.Task>();
// Runs a short job (such as ending a session) on a worker thread; the app waits a moment for it when it exits.
public static void Background(Action a)
{
var t = System.Threading.Tasks.Task.Run(() => { try { a(); } catch { } });
lock (pending) { pending.RemoveAll(x => x.IsCompleted); pending.Add(t); }
}
static void WaitBackground(int ms)
{
System.Threading.Tasks.Task[] all;
lock (pending) all = pending.Where(x => !x.IsCompleted).ToArray();
try { if (all.Length > 0) System.Threading.Tasks.Task.WaitAll(all, ms); } catch { }
}
// ---------- JSON and formatting helpers ----------
public static Dictionary<string, object> Obj(object o) { return o as Dictionary<string, object>; }
public static object Get(Dictionary<string, object> d, string k) { object v; return d != null && d.TryGetValue(k, out v) ? v : null; }
public static double Num(Dictionary<string, object> d, string k)
{
var v = Get(d, k);
if (v == null) return 0;
try { return Convert.ToDouble(v, Inv); } catch { return 0; }
}
public static bool Has(Dictionary<string, object> d, string k) { return Get(d, k) != null && Num(d, k) != 0; }
public static string Str(Dictionary<string, object> d, string k) { var v = Get(d, k); return v == null ? null : Convert.ToString(v, Inv); }
public static DateTime When(double ms)
{
if (double.IsNaN(ms)) ms = 0;
ms = Math.Max(-62135596800000d, Math.Min(253402300799999d, ms));
return DateTimeOffset.FromUnixTimeMilliseconds((long)ms).LocalDateTime;
}
public static long NowMs() { return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); }
public static Dictionary<string, object> ReadJson(WebResponse r)
{
using (var sr = new StreamReader(r.GetResponseStream(), Encoding.UTF8)) return Obj(new JavaScriptSerializer().DeserializeObject(sr.ReadToEnd()));
}
public static string Usd(double v) { return "$" + (v >= 1 || v == 0 ? v.ToString("#,0.00", Inv) : v.ToString("0.00##", Inv)); }
public static string Tok(double n)
{
if (n >= 1e6) return (n / 1e6).ToString("0.##", Inv) + "M";
if (n >= 1e4) return (n / 1e3).ToString("0", Inv) + "K";
return n.ToString("#,0", Inv);
}
public static string Pct(double p) { return p > 0 && p < 1 ? "<1%" : Math.Floor(p).ToString(Inv) + "%"; }
public static string Rel(long ms)
{
var t = When(ms);
int days = (DateTime.Today - t.Date).Days;
if (days == 0) return L.F("today {0}", t.ToString("HH:mm", Inv));
if (days == 1) return L.F("yesterday {0}", t.ToString("HH:mm", Inv));
if (days < 7) return t.ToString("dddd", L.Culture) + " " + t.ToString("HH:mm", Inv);
return L.Date(t, false);
}
// ---------- saved passcodes (DPAPI, same format as PowerShell ConvertFrom-SecureString) ----------
public class Saved { public string Name; public string Secret; public long LastUsed; }
public static List<Saved> LoadSaved()
{
try { return ReadSaved(); }
catch { return new List<Saved>(); }
}
// Throws when the file exists but can't be read (locked, no access), so an edit never overwrites it with an empty list.
static List<Saved> ReadSaved()
{
var list = new List<Saved>();
if (!File.Exists(Store)) return list;
string text = File.ReadAllText(Store);
try
{
var items = Get(Obj(Json.DeserializeObject(text)), "items");
IEnumerable all = items is Dictionary<string, object> ? new object[] { items } : items as IEnumerable;
if (all == null || items is string) return list;
foreach (var o in all)
{
var d = Obj(o);
var s = new Saved { Name = Str(d, "name"), Secret = Str(d, "secret"), LastUsed = (long)Num(d, "lastUsed") };
if (!string.IsNullOrEmpty(s.Name) && !string.IsNullOrEmpty(s.Secret)) list.Add(s);
}
}
catch { }
return list.OrderByDescending(s => s.LastUsed).ToList();
}
// Changes the saved passcodes: the list is read again and written back while no other copy of the app can do the
// same, so two open copies never overwrite each other's changes. edit returns false when nothing changed.
public static void EditSaved(Func<List<Saved>, bool> edit)
{
using (var m = new System.Threading.Mutex(false, @"Local\NyxoCode.passcodes"))
{
bool owned = false;
try
{
try { owned = m.WaitOne(5000); } catch (System.Threading.AbandonedMutexException) { owned = true; }
var list = ReadSaved();
if (edit(list)) SaveAll(list);
}
finally { if (owned) m.ReleaseMutex(); }
}
}
static void SaveAll(IEnumerable<Saved> items)
{
Directory.CreateDirectory(Dir);
var arr = items.Select(s => new Dictionary<string, object> { { "name", s.Name }, { "secret", s.Secret }, { "lastUsed", s.LastUsed } }).ToArray();
var tmp = Store + ".tmp";
File.WriteAllText(tmp, Json.Serialize(new Dictionary<string, object> { { "items", arr } }));
if (File.Exists(Store)) File.Replace(tmp, Store, null); else File.Move(tmp, Store);
}
public static string UniqueName(List<Saved> list, string name, Saved except)
{
string baseName = name; int k = 2;
while (list.Any(s => s != except && s.Name == name)) { name = baseName + " " + k; k++; }
return name;
}
public static string Protect(string s)
{
var b = ProtectedData.Protect(Encoding.Unicode.GetBytes(s), null, DataProtectionScope.CurrentUser);
return BitConverter.ToString(b).Replace("-", "").ToLowerInvariant();
}
public static string Unprotect(string hex)
{
try
{
var b = new byte[hex.Length / 2];
for (int i = 0; i < b.Length; i++) b[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
return Encoding.Unicode.GetString(ProtectedData.Unprotect(b, null, DataProtectionScope.CurrentUser));
}
catch { return null; }
}
// ---------- recent folders ----------
// Entries are not checked here: a folder on an unreachable network share could stall for a long time.
// The app checks the chosen folder in the background before using it, and drops it if it is gone.
public static List<string> LoadRecent()
{
try
{
if (!File.Exists(RecentFile)) return new List<string>();
return File.ReadAllLines(RecentFile).Select(s => s.Trim()).Where(s => s != "")
.Distinct(StringComparer.OrdinalIgnoreCase).Take(8).ToList();
}
catch { return new List<string>(); }
}
public static void SaveRecent(IEnumerable<string> list)
{
try { Directory.CreateDirectory(Dir); File.WriteAllLines(RecentFile, list.Take(8).ToArray()); } catch { }
}
public static void AddRecent(string folder)
{
var l = LoadRecent();
l.RemoveAll(s => s.Equals(folder, StringComparison.OrdinalIgnoreCase));
l.Insert(0, folder);
SaveRecent(l);
}
// ---------- login ----------
// A passcode works on one device only. The device is identified by a one-way hash of the Windows
// MachineGuid, so the raw MachineGuid never leaves this PC.
static string deviceId;
public static string DeviceId()
{
if (deviceId != null) return deviceId;
string guid = null;
try
{
using (var k = Microsoft.Win32.RegistryKey.OpenBaseKey(Microsoft.Win32.RegistryHive.LocalMachine, Microsoft.Win32.RegistryView.Registry64).OpenSubKey(@"SOFTWARE\Microsoft\Cryptography"))
guid = k == null ? null : k.GetValue("MachineGuid") as string;
}
catch { }
if (string.IsNullOrEmpty(guid)) guid = Environment.MachineName + "|" + Environment.UserName;
using (var sha = SHA256.Create())
deviceId = BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes("nyxo-device-v1:" + guid))).Replace("-", "").ToLowerInvariant();
return deviceId;
}
public class LoginResult { public int Status; public Dictionary<string, object> Body; public string Message; }
public static LoginResult TryLogin(string passcode)
{
var r = new LoginResult();
try
{
var req = (HttpWebRequest)WebRequest.Create(Server + "/login");
req.Method = "POST"; req.ContentType = "application/json"; req.Timeout = 30000; req.ReadWriteTimeout = 30000; req.AllowAutoRedirect = false;
var data = Encoding.UTF8.GetBytes(Json.Serialize(new Dictionary<string, object> { { "passcode", passcode }, { "device", DeviceId() }, { "deviceName", Environment.MachineName }, { "app", Version } }));
using (var s = req.GetRequestStream()) s.Write(data, 0, data.Length);
using (var resp = (HttpWebResponse)req.GetResponse()) { r.Status = (int)resp.StatusCode; r.Body = ReadJson(resp); }
}
catch (WebException e)
{
var resp = e.Response as HttpWebResponse;
if (resp == null) return r;
r.Status = (int)resp.StatusCode;
try { r.Message = Str(Obj(Get(ReadJson(resp), "error")), "message"); } catch { }
finally { resp.Close(); }
}
catch { }
return r;
}
// Current usage totals (with the usage-policy status) for a session, or null when they can't be read.
public static Dictionary<string, object> Usage(string token) { return GetJson(token, "/usage"); }
// Spend per UTC day for the last 7, 30 or 90 days (oldest first), or null when it can't be read.
public static Dictionary<string, object> History(string token, int days) { return GetJson(token, "/usage/history?days=" + days.ToString(Inv)); }
static Dictionary<string, object> GetJson(string token, string path)
{
if (string.IsNullOrEmpty(token)) return null;
try
{
var req = (HttpWebRequest)WebRequest.Create(Server + path);
req.Timeout = 15000; req.ReadWriteTimeout = 15000; req.AllowAutoRedirect = false;
req.Headers["x-api-key"] = token;
using (var resp = (HttpWebResponse)req.GetResponse()) return ReadJson(resp);
}
catch (WebException e) { if (e.Response != null) e.Response.Close(); return null; }
catch { return null; }
}
// ---------- buying and renewing passcodes (the nyxo.space checkout) ----------
public const string Site = "https://nyxo.space/api";
public class ApiResult { public int Status; public Dictionary<string, object> Body; public string Error; }
// Status 0 means the site could not be reached. Errors come back as {"error": "message"} in English.
public static ApiResult Api(string method, string path, object body)
{
var r = new ApiResult();
try
{
var req = (HttpWebRequest)WebRequest.Create(Site + path);
req.Method = method; req.Timeout = 20000; req.ReadWriteTimeout = 20000; req.AllowAutoRedirect = false;
req.UserAgent = "NyxoCode/" + Version; req.Accept = "application/json";
if (method != "GET")
{
var data = Encoding.UTF8.GetBytes(body == null ? "{}" : Json.Serialize(body));
req.ContentType = "application/json";
using (var s = req.GetRequestStream()) s.Write(data, 0, data.Length);
}
using (var resp = (HttpWebResponse)req.GetResponse()) { r.Status = (int)resp.StatusCode; r.Body = ReadJson(resp); }
}
catch (WebException e)
{
var resp = e.Response as HttpWebResponse;
if (resp == null) return r;
r.Status = (int)resp.StatusCode;
try { r.Error = Str(ReadJson(resp), "error"); } catch { }
finally { resp.Close(); }
}
catch { r.Status = r.Status == 200 ? 502 : r.Status; }
return r;
}
// The open order is kept (encrypted, like saved passcodes) so closing the window never loses a payment.
static readonly string OrderFile = Path.Combine(Dir, "order.dat");
public static Dictionary<string, object> LoadOrder()
{
try { return File.Exists(OrderFile) ? Obj(Json.DeserializeObject(Unprotect(File.ReadAllText(OrderFile).Trim()) ?? "null")) : null; }
catch { return null; }
}
public static void SaveOrder(Dictionary<string, object> o)
{
try { Directory.CreateDirectory(Dir); File.WriteAllText(OrderFile, Protect(Json.Serialize(o))); } catch { }
}
public static void ClearOrder() { try { File.Delete(OrderFile); } catch { } }
// Ends the session on the server, so the token stops working right away. Best effort.
public static void Logout(string token)
{
if (string.IsNullOrEmpty(token)) return;
try
{
var req = (HttpWebRequest)WebRequest.Create(Server + "/logout");
req.Method = "POST"; req.ContentLength = 0; req.Timeout = 3000; req.ReadWriteTimeout = 3000; req.AllowAutoRedirect = false;
req.Headers["x-api-key"] = token;
using (req.GetResponse()) { }
}
catch (WebException e) { if (e.Response != null) e.Response.Close(); }
catch { }
}
// ---------- finding Claude Code ----------
public static string ClaudeAt(string p)
{
if (string.IsNullOrWhiteSpace(p)) return null;
try
{
p = p.Trim().Trim('"'); // literal path: a folder may really contain %...%
if (Directory.Exists(p))
{
foreach (var n in new[] { "claude.exe", "claude.cmd", "claude.bat" })
{
var f = Path.Combine(p, n);
if (File.Exists(f)) return Path.GetFullPath(f);
}
return null;
}
var ext = Path.GetExtension(p).ToLowerInvariant();
if (File.Exists(p) && Path.GetFileNameWithoutExtension(p).Equals("claude", StringComparison.OrdinalIgnoreCase) && (ext == ".exe" || ext == ".cmd" || ext == ".bat"))
return Path.GetFullPath(p);
}
catch { }
return null;
}
// Saved path, then PATH (in PATHEXT order like the command line), then the usual install folders. Null = not found.
public static string FindClaude()
{
string c = null;
try { if (File.Exists(ClaudeSaved)) c = ClaudeAt(File.ReadAllText(ClaudeSaved)); } catch { }
if (c != null) return c;
foreach (var dir in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(';'))
{
string expanded;
try { expanded = Environment.ExpandEnvironmentVariables(dir.Trim().Trim('"')); } catch { continue; }
if (!FullyQualified(expanded)) continue;
foreach (var n in new[] { "claude.exe", "claude.cmd", "claude.bat" })
{
try { var f = Path.Combine(expanded, n); if (File.Exists(f)) return Path.GetFullPath(f); } catch { }
}
}
string home = Environment.GetEnvironmentVariable("USERPROFILE") ?? "", appdata = Environment.GetEnvironmentVariable("APPDATA") ?? "",
local = Environment.GetEnvironmentVariable("LOCALAPPDATA") ?? "", pf = Environment.GetEnvironmentVariable("ProgramFiles") ?? "";
foreach (var d in new[] {
Path.Combine(home, @".local\bin"), Path.Combine(appdata, "npm"), Path.Combine(local, @"Programs\claude"), Path.Combine(local, @"Programs\Claude Code"),
Path.Combine(local, @"Microsoft\WinGet\Links"), Path.Combine(home, @"scoop\shims"), Path.Combine(pf, "Claude Code"), Path.Combine(pf, "nodejs") })
{
if (!FullyQualified(d)) continue; // a missing profile variable would leave a path relative to the current folder
c = ClaudeAt(d);
if (c != null) return c;
}
return null;
}
// "C:\x" or "\\server\share": never relative to the current folder or drive (unlike "C:x" or "\x").
static bool FullyQualified(string path)
{
try { return !string.IsNullOrWhiteSpace(path) && Path.IsPathRooted(path) && Path.GetPathRoot(path).Length >= 3; }
catch { return false; }
}
public static void RememberClaude(string path)
{
try { Directory.CreateDirectory(Dir); File.WriteAllText(ClaudeSaved, path); } catch { }
}
// Quotes one argument the way Windows programs (and node) split their command line.
static string Arg(string s)
{
var sb = new StringBuilder("\"");
int slashes = 0;
foreach (var ch in s)
{
if (ch == '\\') { slashes++; continue; }
if (ch == '"') { sb.Append('\\', slashes * 2 + 1); sb.Append('"'); }
else { sb.Append('\\', slashes); sb.Append(ch); }
slashes = 0;
}
sb.Append('\\', slashes * 2).Append('"');
return sb.ToString();
}
// Starts Claude Code in its own console window. Command-line settings apply to this run only and
// outrank any saved Claude Code settings. The session token is passed in the environment, never on
// the command line or in a file, where other programs and process logs could read it.
// The settings go in a small file of their own (no secrets in it) that is removed once this Claude Code closes.
public static Process StartClaude(string claude, string folder, string token, out string settingsFile)
{
var env = new Dictionary<string, object>();
env["ANTHROPIC_BASE_URL"] = Server;
foreach (var n in new[] { "ANTHROPIC_MODEL", "ANTHROPIC_SMALL_FAST_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "CLAUDE_CODE_SUBAGENT_MODEL" }) env[n] = Model;
env["CLAUDE_CODE_EFFORT_LEVEL"] = "medium";
env["CLAUDE_CODE_MAX_CONTEXT_TOKENS"] = "200000";
env["CLAUDE_CODE_AUTO_COMPACT_WINDOW"] = "200000";
env["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"] = "1";
var settings = Json.Serialize(new Dictionary<string, object> { { "env", env }, { "model", Model }, { "availableModels", new[] { Model } }, { "skipAutoPermissionPrompt", true } });
MarkAutoDefaultNoticeSeen();
var mcp = Json.Serialize(new Dictionary<string, object> { { "mcpServers", new Dictionary<string, object> { { "nyxo", new Dictionary<string, object> {
{ "type", "http" }, { "url", Server + "/mcp" }, { "headers", new Dictionary<string, object> { { "x-api-key", "${NYXO_KEY}" } } } } } } } });
Directory.CreateDirectory(LaunchDir);
string file = Path.Combine(LaunchDir, "settings-" + Guid.NewGuid().ToString("N") + ".json");
File.WriteAllText(file, settings, new UTF8Encoding(false));
try
{
var psi = new ProcessStartInfo(claude, "--settings " + Arg(file) + " --mcp-config " + Arg(mcp));
psi.UseShellExecute = false; // this app has no console, so Claude Code gets a new console window of its own
psi.WorkingDirectory = folder;
foreach (var v in new[] { "ANTHROPIC_API_KEY", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "ANTHROPIC_CUSTOM_HEADERS" }) psi.EnvironmentVariables.Remove(v);
psi.EnvironmentVariables["ANTHROPIC_AUTH_TOKEN"] = token;
psi.EnvironmentVariables["NYXO_KEY"] = token;
// cmd.exe (which runs .cmd/.bat shims) must not run a program planted in the project folder.
psi.EnvironmentVariables["NoDefaultCurrentDirectoryInExePath"] = "1";
if (!claude.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
// Run the shim through cmd by environment reference, so % in its path (or the settings path) is never expanded.
psi.FileName = Path.Combine(Environment.SystemDirectory, "cmd.exe");
psi.EnvironmentVariables["NYXO_CLAUDE"] = claude;
psi.EnvironmentVariables["NYXO_SETTINGS"] = file;
psi.Arguments = "/d /v:off /s /c \"\"%NYXO_CLAUDE%\" --settings \"%NYXO_SETTINGS%\" --mcp-config " + Arg(mcp) + "\"";
}
// Auto mode's server-side checks can't pass through Nyxo; this makes Claude Code use its own checks
// from the start instead of pausing the first command with a "session isn't eligible" notice.
psi.EnvironmentVariables["CLAUDE_CODE_AUTO_MODE_SERVER"] = "0";
var p = Process.Start(psi);
// Remember which program uses the file, so a later start of the app can tell when it is safe to remove it.
try { File.WriteAllText(file + ".pid", p.Id.ToString(Inv) + " " + p.StartTime.ToUniversalTime().Ticks.ToString(Inv)); } catch { }
settingsFile = file;
return p;
}
catch { DeleteLaunchFile(file); throw; }
}
static readonly string LaunchDir = Path.Combine(Dir, "launch");
public static void DeleteLaunchFile(string file)
{
if (string.IsNullOrEmpty(file)) return;
try { File.Delete(file); } catch { }
try { File.Delete(file + ".pid"); } catch { }
}
// Settings files left by Claude Code windows that closed while the app was not running. A file still used by a
// running Claude Code (its process is alive and started when recorded) stays.
static void CleanLaunchFiles()
{
Background(() =>
{
if (!Directory.Exists(LaunchDir)) return;
foreach (var f in Directory.GetFiles(LaunchDir, "settings-*.json"))
{
try
{
bool inUse = false;
string pid = f + ".pid";
if (File.Exists(pid))
{
var parts = File.ReadAllText(pid).Trim().Split(' ');
int id; long ticks;
if (parts.Length == 2 && int.TryParse(parts[0], NumberStyles.None, Inv, out id) && long.TryParse(parts[1], NumberStyles.None, Inv, out ticks))
{
try { using (var p = Process.GetProcessById(id)) inUse = !p.HasExited && p.StartTime.ToUniversalTime().Ticks == ticks; }
catch { }
}
}
// No record yet: the app may have stopped right after starting Claude Code; keep a fresh file for a while.
else inUse = DateTime.UtcNow - File.GetLastWriteTimeUtc(f) < TimeSpan.FromDays(2);
if (!inUse) DeleteLaunchFile(f);
}
catch { }
}
foreach (var pid in Directory.GetFiles(LaunchDir, "settings-*.json.pid"))
if (!File.Exists(pid.Substring(0, pid.Length - 4))) try { File.Delete(pid); } catch { }
});
}
// Records in Claude Code's own config that its one-time "Auto mode is now the default" message was seen,
// so it never pops up in a Nyxo session. Only that one top-level flag is set; nothing else in the file changes.
// Nothing is written when the flag is already set, or when the file is not one valid JSON object. If another
// Claude Code window saves the file meanwhile, the change is made again on its new content (up to 3 tries).
const string SeenFlag = "hasSeenAutoDefaultNotice";
static void MarkAutoDefaultNoticeSeen()
{
try
{
string dir = Environment.GetEnvironmentVariable("CLAUDE_CONFIG_DIR");
if (string.IsNullOrEmpty(dir)) dir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
string path = Path.Combine(dir, ".claude.json");
var js = new JavaScriptSerializer { MaxJsonLength = int.MaxValue, RecursionLimit = 512 };
for (int attempt = 0; attempt < 3; attempt++)
{
var before = new FileInfo(path);
if (!before.Exists) return;
DateTime stamp = before.LastWriteTimeUtc; long size = before.Length;
string text = File.ReadAllText(path, new UTF8Encoding(false));
var root = Obj(js.DeserializeObject(text));
if (root == null) return;
object have;
bool present = root.TryGetValue(SeenFlag, out have);
if (have is bool && (bool)have) return;
int at, len;
if (!TopLevelValue(text, SeenFlag, out at, out len) || present != (at >= 0)) return;
string updated;
if (at >= 0)
{
if (len <= 0) return; // not a plain literal (a string, object or array): leave it alone
updated = text.Substring(0, at) + "true" + text.Substring(at + len);
}
else
{
int brace = text.IndexOf('{');
bool empty = text.Substring(brace + 1).TrimStart().StartsWith("}");
updated = text.Substring(0, brace + 1) + "\"" + SeenFlag + "\":true" + (empty ? "" : ",") + text.Substring(brace + 1);
}
// The edited text must still be the same object, with only the flag changed or added.
var check = Obj(js.DeserializeObject(updated));
if (check == null || !(Get(check, SeenFlag) is bool) || !(bool)Get(check, SeenFlag) || check.Count != root.Count + (present ? 0 : 1)) return;
string tmp = path + ".nyxo-" + Guid.NewGuid().ToString("N").Substring(0, 8) + ".tmp";
File.WriteAllText(tmp, updated, new UTF8Encoding(false));
var now = new FileInfo(path);
if (!now.Exists || now.LastWriteTimeUtc != stamp || now.Length != size)
{
try { File.Delete(tmp); } catch { }
continue; // changed by another program meanwhile: read it again
}
try { File.Replace(tmp, path, null); }
catch { try { File.Delete(tmp); } catch { } }
return;
}
}
catch { }
}
// Finds a key of the outermost JSON object, never one inside a nested object or array. Returns false when the
// text can't be scanned or has the key twice. at = -1 when the key is absent; len = 0 when its value is not a
// plain literal (true, false, null or a number).
static bool TopLevelValue(string s, string key, out int at, out int len)
{
at = -1; len = 0;
int depth = 0, i = 0, n = s.Length;
bool wantKey = false, found = false;
while (i < n)
{
char c = s[i];
if (c == '"')
{
int j = i + 1;
while (j < n && s[j] != '"') j += s[j] == '\\' ? 2 : 1;
if (j >= n) return false;
bool isKey = depth == 1 && wantKey;
string name = isKey ? s.Substring(i + 1, j - i - 1) : null;
i = j + 1;
if (!isKey) continue;
wantKey = false;
if (name != key) continue;
if (found) return false;
found = true;
while (i < n && char.IsWhiteSpace(s[i])) i++;
if (i >= n || s[i] != ':') return false;
i++;
while (i < n && char.IsWhiteSpace(s[i])) i++;
at = i;
int k = i;
while (k < n && (char.IsLetterOrDigit(s[k]) || s[k] == '-' || s[k] == '+' || s[k] == '.')) k++;
len = k - i;
continue;
}
if (c == '{') { depth++; if (depth == 1) wantKey = true; }
else if (c == '[') { depth++; if (depth == 1) return false; }
else if (c == '}' || c == ']') { depth--; if (depth < 0) return false; }
else if (c == ',' && depth == 1) wantKey = true;
i++;
}
return depth == 0;
}
}
// The modern Windows folder picker (IFileOpenDialog).
static class FolderPicker
{
[ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")] class FileOpenDialogCo { }
[ComImport, Guid("42f85136-db7e-439c-85f1-e4075d135fc8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IFileDialog
{
[PreserveSig] int Show(IntPtr parent);
void SetFileTypes(uint n, IntPtr specs);
void SetFileTypeIndex(uint i);
void GetFileTypeIndex(out uint i);
void Advise(IntPtr sink, out uint cookie);
void Unadvise(uint cookie);
void SetOptions(uint fos);
void GetOptions(out uint fos);
void SetDefaultFolder(IShellItem si);
void SetFolder(IShellItem si);
void GetFolder(out IShellItem si);
void GetCurrentSelection(out IShellItem si);
void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string name);
void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string name);
void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string title);
void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string text);
void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string label);
void GetResult(out IShellItem si);
}
[ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IShellItem
{
void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
void GetParent(out IShellItem si);
void GetDisplayName(uint sigdn, [MarshalAs(UnmanagedType.LPWStr)] out string name);
void GetAttributes(uint mask, out uint attrs);
void Compare(IShellItem si, uint hint, out int order);
}
[DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = true)]
static extern int SHCreateItemFromParsingName(string path, IntPtr pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IShellItem si);
// Returns the chosen folder, or null when cancelled or unavailable.
public static string Pick(IntPtr owner, string title, string start)
{
IFileDialog dlg = null; IShellItem startItem = null, result = null;
try
{
dlg = (IFileDialog)new FileOpenDialogCo();
uint opts; dlg.GetOptions(out opts);
dlg.SetOptions(opts | 0x20 | 0x40 | 0x800); // pick folders, file system only, must exist
dlg.SetTitle(title);
dlg.SetOkButtonLabel(L.T("Use this folder"));
if (!string.IsNullOrEmpty(start) && SHCreateItemFromParsingName(start, IntPtr.Zero, typeof(IShellItem).GUID, out startItem) == 0) dlg.SetFolder(startItem);
if (dlg.Show(owner) != 0) return null;
dlg.GetResult(out result);
string path; result.GetDisplayName(0x80058000, out path); // SIGDN_FILESYSPATH
return path;
}
catch { return null; }
finally
{
foreach (var o in new object[] { result, startItem, dlg })
if (o != null) try { Marshal.ReleaseComObject(o); } catch { }
}
}
}
L.csلغات التطبيق: الإنجليزية والفرنسية والعربية (من اليمين إلى اليسار).455 سطر
// The app's languages: English, French and Arabic (right to left). The language follows Windows until one is picked
// with the globe button; the choice is kept in %APPDATA%\Nyxo\lang.txt. Texts are written in English in the code and
// looked up here; anything missing stays in English.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Windows.Forms;
static class L
{
public static readonly string[] Codes = { "en", "fr", "ar" };
public static readonly string[] Names = { "English", "Français", "العربية" };
static readonly string File = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"Nyxo\lang.txt");
public static string Lang = "en";
public static CultureInfo Culture = CultureInfo.InvariantCulture;
public static event Action Changed;
public static void Load()
{
string code = null;
try { if (System.IO.File.Exists(File)) code = System.IO.File.ReadAllText(File).Trim(); } catch { }
if (Array.IndexOf(Codes, code) < 0) code = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
Apply(Array.IndexOf(Codes, code) < 0 ? "en" : code);
}
static void Apply(string code)
{
Lang = code;
// Arabic dates use the Gregorian calendar with Arabic month names (ar-EG); numbers stay as 0-9.
try { Culture = code == "fr" ? new CultureInfo("fr-FR") : code == "ar" ? new CultureInfo("ar-EG") : CultureInfo.InvariantCulture; }
catch { Culture = CultureInfo.InvariantCulture; }
global::T.Rtl = code == "ar";
}
public static void Set(string code)
{
if (Array.IndexOf(Codes, code) < 0 || code == Lang) return;
Apply(code);
try { Directory.CreateDirectory(Path.GetDirectoryName(File)); System.IO.File.WriteAllText(File, code); } catch { }
if (Changed != null) Changed();
}
public static string T(string en)
{
string[] t;
if (Lang == "en" || en == null || !Dict.TryGetValue(en, out t)) return en;
return t[Lang == "fr" ? 0 : 1];
}
public static string F(string en, params object[] args) { return string.Format(CultureInfo.InvariantCulture, T(en), args); }
public static string Span(int mins)
{
mins = Math.Max(1, mins);
return mins >= 60 ? F("{0} h {1} min", mins / 60, mins % 60) : F("{0} min", mins);
}
public static string Date(DateTime t, bool withTime)
{
if (Lang == "en") return t.ToString(withTime ? "d MMM yyyy HH:mm" : "d MMM yyyy", CultureInfo.InvariantCulture);
string d = t.ToString(Lang == "fr" ? "d MMM yyyy" : "d MMMM yyyy", Culture);
return withTime ? d + (Lang == "fr" ? " à " : " الساعة ") + t.ToString("HH:mm", CultureInfo.InvariantCulture) : d;
}
// Pages of nyxo.space in the chosen language (the site has /fr/ and /ar/ versions).
public static string Site(string url)
{
const string root = "https://nyxo.space/";
if (Lang == "en" || url == null || !url.StartsWith(root, StringComparison.Ordinal)) return url;
string rest = url.Substring(root.Length);
if (rest.StartsWith("fr/") || rest.StartsWith("ar/")) rest = rest.Substring(3);
return root + Lang + "/" + rest;
}
// Messages from the Nyxo server come in English: known ones are shown in the chosen language.
public static string Server(string msg)
{
if (msg == null || Lang == "en") return msg;
var m = Regex.Match(msg, @"^This passcode is paused for 1 hour after repeated usage-policy refusals\. It resumes at (\d\d:\d\d) UTC \(in (\d+) min\)\.$");
if (m.Success)
{
int mins;
if (int.TryParse(m.Groups[2].Value, NumberStyles.None, CultureInfo.InvariantCulture, out mins))
return F("This passcode is paused for 1 hour after repeated usage-policy refusals. It resumes at {0} UTC (in {1}).", m.Groups[1].Value, Span(mins));
return msg;
}
return T(msg);
}
// The globe button that picks the language.
public static Btn Button()
{
var b = new Btn("", Btn.Kind.Ghost) { Glyph = "\uE774" };
var menu = new ContextMenuStrip { Renderer = new ToolStripProfessionalRenderer(new DarkColors()), ShowImageMargin = false, ShowCheckMargin = true, Font = global::T.Body };
for (int i = 0; i < Codes.Length; i++)
{
string code = Codes[i];
var mi = new ToolStripMenuItem(Names[i]) { ForeColor = global::T.Text, Padding = new Padding(global::T.P(4)) };
mi.Click += (s, e) => Set(code);
menu.Items.Add(mi);
}
b.Click += (s, e) =>
{
for (int i = 0; i < Codes.Length; i++) ((ToolStripMenuItem)menu.Items[i]).Checked = Codes[i] == Lang;
menu.RightToLeft = global::T.Rtl ? RightToLeft.Yes : RightToLeft.No;
menu.Show(b, new System.Drawing.Point(global::T.Rtl ? b.Width : 0, b.Height));
};
Label(b);
return b;
}
public static void Label(Btn b) { b.Text = Names[Array.IndexOf(Codes, Lang)]; b.AccessibleName = F("Language: {0}", b.Text); b.Invalidate(); }
public static int ButtonWidth(Btn b) { return global::T.W(b.Text, b.Font) + global::T.P(22 + 24); }
static readonly Dictionary<string, string[]> Dict = new Dictionary<string, string[]>
{
{ "Could not reach the network. Try again in a moment.", new[] { "Impossible de joindre le réseau. Réessayez dans un instant.", "تعذّر الوصول إلى الشبكة. حاول مرة أخرى بعد قليل." } },
{ "Invalid request.", new[] { "Requête invalide.", "طلب غير صالح." } },
{ "Details were not recorded for this day.", new[] { "Les détails n'ont pas été enregistrés pour ce jour.", "لم تُسجَّل التفاصيل لهذا اليوم." } },
{ "History", new[] { "Historique", "السجل" } },
{ "Usage history", new[] { "Historique d'utilisation", "سجل الاستخدام" } },
{ "{0} days", new[] { "{0} jours", "{0} يومًا" } },
{ "Close", new[] { "Fermer", "إغلاق" } },
{ "Loading...", new[] { "Chargement...", "جارٍ التحميل..." } },
{ "Couldn't load the history. Check your connection and try again.", new[] { "Impossible de charger l'historique. Vérifiez votre connexion et réessayez.", "تعذّر تحميل السجل. تحقّق من اتصالك وحاول مرة أخرى." } },
{ "No usage in this period yet.", new[] { "Aucune utilisation sur cette période pour l'instant.", "لا يوجد استخدام في هذه الفترة بعد." } },
{ "Spend per day for this passcode. Days run from 00:00 to 24:00 UTC.", new[] { "Dépenses par jour pour ce code d'accès. Les jours vont de 00:00 à 24:00 UTC.", "الإنفاق اليومي لرمز المرور هذا. يبدأ اليوم من 00:00 إلى 24:00 بتوقيت UTC." } },
{ "TOTAL", new[] { "TOTAL", "الإجمالي" } },
{ "last {0} days", new[] { "{0} derniers jours", "آخر {0} يومًا" } },
{ "AVERAGE PER DAY", new[] { "MOYENNE PAR JOUR", "المتوسط اليومي" } },
{ "{0} of {1} days used", new[] { "{0} jours utilisés sur {1}", "{0} من {1} يومًا مستخدمة" } },
{ "BUSIEST DAY", new[] { "JOUR LE PLUS ACTIF", "أكثر يوم استخدامًا" } },
{ "REQUESTS", new[] { "REQUÊTES", "الطلبات" } },
{ "Hover a bar for details. Kept for {0} days.", new[] { "Survolez une barre pour les détails. Conservé {0} jours.", "مرّر المؤشر على عمود لرؤية التفاصيل. يُحفظ لمدة {0} يومًا." } },
{ "Recorded from {0}. Kept for {1} days.", new[] { "Enregistré depuis le {0}. Conservé {1} jours.", "مُسجّل منذ {0}. يُحفظ لمدة {1} يومًا." } },
{ "Daily allowance {0}", new[] { "Quota journalier {0}", "الحد اليومي {0}" } },
{ "{0} requests", new[] { "{0} requêtes", "{0} طلب" } },
{ "Tokens: {0} in, {1} out", new[] { "Jetons : {0} en entrée, {1} en sortie", "الرموز: {0} إدخال، {1} إخراج" } },
{ "Cache: {0} read, {1} written", new[] { "Cache : {0} lus, {1} écrits", "الكاش: {0} قراءة، {1} كتابة" } },
{ "Spend per day", new[] { "Dépenses par jour", "الإنفاق اليومي" } },
{ "Use the arrow keys to move between days.", new[] { "Utilisez les flèches pour passer d'un jour à l'autre.", "استخدم مفاتيح الأسهم للتنقل بين الأيام." } },
{ "Signed out.", new[] { "Déconnecté.", "تم تسجيل الخروج." } },
{ "Nyxo Code was updated to {0} (from {1}). Sign in again to continue.", new[] { "Nyxo Code a été mis à jour en {0} (depuis {1}). Reconnectez-vous pour continuer.", "تم تحديث Nyxo Code إلى {0} (من {1}). سجّل الدخول مرة أخرى للمتابعة." } },
{ "Nyxo Code {0} is available. You have {1}.", new[] { "Nyxo Code {0} est disponible. Vous avez la {1}.", "الإصدار {0} من Nyxo Code متاح. لديك الإصدار {1}." } },
{ "Update", new[] { "Mettre à jour", "تحديث" } },
{ "What's new", new[] { "Nouveautés", "ما الجديد" } },
{ "What's new in Nyxo Code {0}", new[] { "Nouveautés de Nyxo Code {0}", "ما الجديد في Nyxo Code {0}" } },
{ "Version {0}", new[] { "Version {0}", "الإصدار {0}" } },
{ "Nyxo Code was updated to {0}. Close it and start it again to use the new version.", new[] { "Nyxo Code a été mis à jour en {0}. Fermez-le et relancez-le pour utiliser la nouvelle version.", "تم تحديث Nyxo Code إلى {0}. أغلقه وشغّله مرة أخرى لاستخدام الإصدار الجديد." } },
{ "The file is in use. Wait a moment and try again.", new[] { "Le fichier est en cours d’utilisation. Patientez un instant et réessayez.", "الملف قيد الاستخدام. انتظر قليلًا ثم حاول مرة أخرى." } },
{ "Couldn't save the passcode on this PC. Copy it now and keep it safe.", new[] { "Impossible d’enregistrer le code d’accès sur ce PC. Copiez-le maintenant et gardez-le en lieu sûr.", "تعذّر حفظ رمز المرور على هذا الكمبيوتر. انسخه الآن واحتفظ به في مكان آمن." } },
{ "Couldn't save the change on this PC. Try again.", new[] { "Impossible d’enregistrer la modification sur ce PC. Réessayez.", "تعذّر حفظ التغيير على هذا الكمبيوتر. حاول مرة أخرى." } },
{ "Wait for the sign-in to finish, then click Update.", new[] { "Attendez la fin de la connexion, puis cliquez sur Mettre à jour.", "انتظر انتهاء تسجيل الدخول، ثم اضغط على تحديث." } },
{ "Cancel", new[] { "Annuler", "إلغاء" } },
{ "Update Nyxo Code?", new[] { "Mettre à jour Nyxo Code ?", "تحديث Nyxo Code؟" } },
{ "Update Nyxo Code", new[] { "Mettre à jour Nyxo Code", "تحديث Nyxo Code" } },
{ "Nyxo Code can't update itself in this folder:", new[] { "Nyxo Code ne peut pas se mettre à jour dans ce dossier :", "لا يستطيع Nyxo Code تحديث نفسه في هذا المجلد:" } },
{ "Your browser will download the new version. Close Nyxo Code, then use the new file instead of this one.", new[] { "Votre navigateur va télécharger la nouvelle version. Fermez Nyxo Code, puis utilisez le nouveau fichier à la place de celui-ci.", "سيقوم متصفحك بتنزيل الإصدار الجديد. أغلق Nyxo Code، ثم استخدم الملف الجديد بدلًا من هذا الملف." } },
{ "Download", new[] { "Télécharger", "تنزيل" } },
{ "Downloading Nyxo Code {0}...", new[] { "Téléchargement de Nyxo Code {0}...", "جارٍ تنزيل Nyxo Code {0}..." } },
{ "Updating...", new[] { "Mise à jour...", "جارٍ التحديث..." } },
{ "Installing Nyxo Code {0}...", new[] { "Installation de Nyxo Code {0}...", "جارٍ تثبيت Nyxo Code {0}..." } },
{ "Update failed: {0} Nothing was changed.", new[] { "Échec de la mise à jour : {0} Rien n'a été modifié.", "فشل التحديث: {0} لم يتم تغيير أي شيء." } },
{ "Try again", new[] { "Réessayer", "إعادة المحاولة" } },
{ "Last used {0}", new[] { "Dernière utilisation {0}", "آخر استخدام {0}" } },
{ "Not used yet", new[] { "Pas encore utilisé", "لم يُستخدم بعد" } },
{ "\"{0}\" cannot be read by this Windows account. Remove it and add it again.", new[] { "« {0} » ne peut pas être lu par ce compte Windows. Supprimez-le et ajoutez-le de nouveau.", "لا يمكن لحساب Windows هذا قراءة «{0}». احذفه ثم أضفه من جديد." } },
{ "Enter your passcode.", new[] { "Saisissez votre code d'accès.", "أدخل رمز المرور." } },
{ "Connecting as \"{0}\"...", new[] { "Connexion en tant que « {0} »...", "جارٍ الاتصال باسم «{0}»..." } },
{ "Connecting...", new[] { "Connexion...", "جارٍ الاتصال..." } },
{ "\"{0}\" was not accepted.", new[] { "« {0} » n'a pas été accepté.", "لم يتم قبول «{0}»." } },
{ "Passcode not accepted", new[] { "Code d'accès refusé", "لم يتم قبول رمز المرور" } },
{ "\"{0}\" was not accepted. It may have expired, been revoked or changed.", new[] { "« {0} » n'a pas été accepté. Il a peut-être expiré, été révoqué ou modifié.", "لم يتم قبول «{0}». ربما انتهت صلاحيته أو أُلغي أو تغيّر." } },
{ "Remove it from this PC?", new[] { "Le supprimer de ce PC ?", "هل تريد حذفه من هذا الكمبيوتر؟" } },
{ "Keep", new[] { "Garder", "الإبقاء عليه" } },
{ "Removed \"{0}\".", new[] { "« {0} » supprimé.", "تم حذف «{0}»." } },
{ "Passcode not accepted.", new[] { "Code d'accès refusé.", "لم يتم قبول رمز المرور." } },
{ "This passcode is paused by the admin. Try again once it is resumed.", new[] { "Ce code d'accès est suspendu par l'administrateur. Réessayez une fois qu'il sera réactivé.", "رمز المرور هذا موقوف مؤقتًا من قِبل المسؤول. حاول مرة أخرى بعد استئنافه." } },
{ "Passcode revoked", new[] { "Code d'accès révoqué", "تم إلغاء رمز المرور" } },
{ "Passcode paused", new[] { "Code d'accès suspendu", "رمز المرور موقوف مؤقتًا" } },
{ "OK", new[] { "OK", "موافق" } },
{ "Too many attempts. Try again in 30 minutes.", new[] { "Trop de tentatives. Réessayez dans 30 minutes.", "محاولات كثيرة جدًا. حاول مرة أخرى بعد 30 دقيقة." } },
{ "Nyxo is temporarily unavailable. Please try again shortly.", new[] { "Nyxo est momentanément indisponible. Réessayez dans un instant.", "Nyxo غير متاح مؤقتًا. يرجى المحاولة مرة أخرى بعد قليل." } },
{ "Nyxo returned an unexpected response. Please try again shortly.", new[] { "Nyxo a renvoyé une réponse inattendue. Réessayez dans un instant.", "أرسل Nyxo ردًا غير متوقع. يرجى المحاولة مرة أخرى بعد قليل." } },
{ "Could not reach the Nyxo server. Check your internet connection.", new[] { "Impossible de joindre le serveur Nyxo. Vérifiez votre connexion internet.", "تعذّر الوصول إلى خادم Nyxo. تحقّق من اتصالك بالإنترنت." } },
{ "Rename passcode", new[] { "Renommer le code d'accès", "إعادة تسمية رمز المرور" } },
{ "Name for this saved passcode:", new[] { "Nom de ce code d'accès enregistré :", "اسم رمز المرور المحفوظ هذا:" } },
{ "Save", new[] { "Enregistrer", "حفظ" } },
{ "Remove saved passcode", new[] { "Supprimer le code d'accès enregistré", "حذف رمز المرور المحفوظ" } },
{ "Remove \"{0}\" from this PC?", new[] { "Supprimer « {0} » de ce PC ?", "هل تريد حذف «{0}» من هذا الكمبيوتر؟" } },
{ "The passcode keeps working; you will just need to type it again.", new[] { "Le code d'accès reste valable ; il faudra simplement le saisir de nouveau.", "سيظل رمز المرور صالحًا؛ ستحتاج فقط إلى كتابته مرة أخرى." } },
{ "Remove", new[] { "Supprimer", "حذف" } },
{ "Locate Claude Code...", new[] { "Trouver Claude Code...", "تحديد مكان Claude Code..." } },
{ "Open Claude Code", new[] { "Ouvrir Claude Code", "فتح Claude Code" } },
{ "Claude Code was not found on this PC.", new[] { "Claude Code est introuvable sur ce PC.", "لم يتم العثور على Claude Code على هذا الكمبيوتر." } },
{ "Claude Code is running in {0} window.", new[] { "Claude Code est ouvert dans {0} fenêtre.", "Claude Code يعمل في نافذة واحدة." } },
{ "Claude Code is running in {0} windows.", new[] { "Claude Code est ouvert dans {0} fenêtres.", "Claude Code يعمل في {0} نوافذ." } },
{ "That folder no longer exists. Removed it from Recent.", new[] { "Ce dossier n'existe plus. Il a été retiré des récents.", "هذا المجلد لم يعد موجودًا. تمت إزالته من الأخيرة." } },
{ "Choose the folder to open Claude Code in", new[] { "Choisissez le dossier où ouvrir Claude Code", "اختر المجلد الذي سيُفتح فيه Claude Code" } },
{ "Find Claude Code (claude.exe)", new[] { "Trouver Claude Code (claude.exe)", "البحث عن Claude Code (claude.exe)" } },
{ "Programs", new[] { "Programmes", "البرامج" } },
{ "That is not Claude Code. Choose claude.exe.", new[] { "Ce n'est pas Claude Code. Choisissez claude.exe.", "هذا ليس Claude Code. اختر claude.exe." } },
{ "Found Claude Code. Saved for next time.", new[] { "Claude Code trouvé. Enregistré pour la prochaine fois.", "تم العثور على Claude Code. تم حفظه للمرة القادمة." } },
{ "This passcode was revoked for usage-policy violations.", new[] { "Ce code d'accès a été révoqué pour non-respect des règles d'utilisation.", "تم إلغاء رمز المرور هذا بسبب مخالفة سياسة الاستخدام." } },
{ "That folder does not exist.", new[] { "Ce dossier n'existe pas.", "هذا المجلد غير موجود." } },
{ "Network folders need claude.exe. Map the share to a drive letter first.", new[] { "Les dossiers réseau nécessitent claude.exe. Associez d'abord le partage à une lettre de lecteur.", "تتطلّب مجلدات الشبكة claude.exe. اربط المشاركة بحرف محرك أقراص أولًا." } },
{ "Could not start Claude Code:", new[] { "Impossible de lancer Claude Code :", "تعذّر تشغيل Claude Code:" } },
{ "Claude Code closed right away (exit code {0}).", new[] { "Claude Code s'est fermé aussitôt (code de sortie {0}).", "أُغلق Claude Code فورًا (رمز الخروج {0})." } },
{ "Your session ended. Connect again to continue.", new[] { "Votre session est terminée. Reconnectez-vous pour continuer.", "انتهت جلستك. اتصل مرة أخرى للمتابعة." } },
{ "Reconnecting...", new[] { "Reconnexion...", "جارٍ إعادة الاتصال..." } },
{ "Passcode paused for 1 hour", new[] { "Code d'accès suspendu pendant 1 heure", "تم إيقاف رمز المرور لمدة ساعة" } },
{ "Another request was refused by the model's safety rules within 24 hours, so this passcode is paused.", new[] { "Une autre requête a été refusée par les règles de sécurité du modèle en moins de 24 heures : ce code d'accès est donc suspendu.", "رُفض طلب آخر بموجب قواعد الأمان الخاصة بالنموذج خلال 24 ساعة، لذلك تم إيقاف رمز المرور هذا مؤقتًا." } },
{ "It resumes on its own.", new[] { "Il reprend tout seul.", "سيُستأنف تلقائيًا." } },
{ "One more refusal within 24 hours revokes this passcode permanently, with no refund.", new[] { "Un refus de plus en moins de 24 heures révoque définitivement ce code d'accès, sans remboursement.", "رفض واحد آخر خلال 24 ساعة يلغي رمز المرور هذا نهائيًا، دون استرداد المبلغ." } },
{ "Usage-policy warning", new[] { "Avertissement : règles d'utilisation", "تحذير بشأن سياسة الاستخدام" } },
{ "A request was refused by the model's safety rules.", new[] { "Une requête a été refusée par les règles de sécurité du modèle.", "رُفض طلب بموجب قواعد الأمان الخاصة بالنموذج." } },
{ "A 2nd refusal within 24 hours pauses this passcode for 1 hour; a 3rd revokes it with no refund.", new[] { "Un 2e refus en moins de 24 heures suspend ce code d'accès pendant 1 heure ; un 3e le révoque, sans remboursement.", "الرفض الثاني خلال 24 ساعة يوقف رمز المرور هذا لمدة ساعة؛ والثالث يلغيه دون استرداد المبلغ." } },
{ "Please wait for the connection attempt to finish.", new[] { "Veuillez attendre la fin de la tentative de connexion.", "يرجى الانتظار حتى تنتهي محاولة الاتصال." } },
{ "Quit Nyxo Code?", new[] { "Quitter Nyxo Code ?", "الخروج من Nyxo Code؟" } },
{ "Quit", new[] { "Quitter", "خروج" } },
{ "Claude Code is open in {0} window. It keeps working, but its usage is not shown here until you sign in again after the update.", new[] { "Claude Code est ouvert dans {0} fenêtre. Il continue de fonctionner, mais sa consommation ne s'affiche plus ici tant que vous ne vous reconnectez pas après la mise à jour.", "Claude Code مفتوح في نافذة واحدة. سيستمر في العمل، لكن استهلاكه لن يظهر هنا حتى تسجّل الدخول مرة أخرى بعد التحديث." } },
{ "Claude Code is open in {0} windows. It keeps working, but its usage is not shown here until you sign in again after the update.", new[] { "Claude Code est ouvert dans {0} fenêtres. Il continue de fonctionner, mais sa consommation ne s'affiche plus ici tant que vous ne vous reconnectez pas après la mise à jour.", "Claude Code مفتوح في {0} نوافذ. سيستمر في العمل، لكن استهلاكه لن يظهر هنا حتى تسجّل الدخول مرة أخرى بعد التحديث." } },
{ "Claude Code is still open in {0} window. It keeps working after you quit, but its usage will not be shown here.", new[] { "Claude Code est encore ouvert dans {0} fenêtre. Il continue de fonctionner après votre départ, mais sa consommation ne sera plus affichée ici.", "لا يزال Claude Code مفتوحًا في نافذة واحدة. سيستمر في العمل بعد الخروج، لكن استهلاكه لن يظهر هنا." } },
{ "Claude Code is still open in {0} windows. It keeps working after you quit, but its usage will not be shown here.", new[] { "Claude Code est encore ouvert dans {0} fenêtres. Il continue de fonctionner après votre départ, mais sa consommation ne sera plus affichée ici.", "لا يزال Claude Code مفتوحًا في {0} نوافذ. سيستمر في العمل بعد الخروج، لكن استهلاكه لن يظهر هنا." } },
{ "Something went wrong:", new[] { "Une erreur s'est produite :", "حدث خطأ ما:" } },
{ "today {0}", new[] { "aujourd'hui {0}", "اليوم {0}" } },
{ "yesterday {0}", new[] { "hier {0}", "أمس {0}" } },
{ "Use this folder", new[] { "Utiliser ce dossier", "استخدام هذا المجلد" } },
{ "Requests refused by the model's safety rules count as strikes: 2 in 24 h = 1-hour pause, 3 = passcode revoked, no refund.", new[] { "Les requêtes refusées par les règles de sécurité du modèle comptent comme des avertissements : 2 en 24 h = 1 heure de suspension, 3 = code révoqué, sans remboursement.", "الطلبات التي ترفضها قواعد الأمان الخاصة بالنموذج تُحتسب مخالفات: 2 خلال 24 ساعة = إيقاف لمدة ساعة، 3 = إلغاء رمز المرور دون استرداد المبلغ." } },
{ "This passcode was revoked for repeated usage-policy violations. As stated in the terms, no refund is issued.", new[] { "Ce code d'accès a été révoqué pour des infractions répétées aux règles d'utilisation. Comme indiqué dans les conditions, aucun remboursement n'est accordé.", "تم إلغاء رمز المرور هذا بسبب مخالفات متكررة لسياسة الاستخدام. وكما هو مذكور في الشروط، لا يتم استرداد المبلغ." } },
{ "Paused until {0} ({1} left)", new[] { "Suspendu jusqu'à {0} (encore {1})", "موقوف حتى {0} (متبقٍ {1})" } },
{ "Policy strikes: {0} of {1}", new[] { "Avertissements : {0} sur {1}", "المخالفات: {0} من {1}" } },
{ "(resets in {0})", new[] { "(remise à zéro dans {0})", "(تُصفَّر بعد {0})" } },
{ "Passcode revoked (usage-policy violations)", new[] { "Code d'accès révoqué (infractions aux règles d'utilisation)", "تم إلغاء رمز المرور (مخالفات سياسة الاستخدام)" } },
{ "One more refusal in 24 h = revoked, no refund.", new[] { "Un refus de plus en 24 h = révocation, sans remboursement.", "رفض واحد آخر خلال 24 ساعة = إلغاء دون استرداد المبلغ." } },
{ "A 2nd refusal in 24 h = 1-hour pause.", new[] { "Un 2e refus en 24 h = 1 heure de suspension.", "رفض ثانٍ خلال 24 ساعة = إيقاف لمدة ساعة." } },
{ "Today: {0} of {1} used. {2} left.", new[] { "Aujourd'hui : {0} utilisés sur {1}. Il reste {2}.", "اليوم: تم استخدام {0} من {1}. المتبقي {2}." } },
{ "Today's allowance is used up.", new[] { "Le quota du jour est épuisé.", "تم استهلاك رصيد اليوم بالكامل." } },
{ "At least 80 percent of today's allowance used.", new[] { "Au moins 80 % du quota du jour utilisé.", "تم استخدام 80 بالمئة على الأقل من رصيد اليوم." } },
{ "Renews at {0} your time, midnight UTC, in {1}.", new[] { "Renouvelé à {0} heure locale, minuit UTC, dans {1}.", "يتجدّد عند {0} بتوقيتك، منتصف الليل UTC، بعد {1}." } },
{ "Not available.", new[] { "Non disponible.", "غير متاح." } },
{ "{0} used. {1} input tokens. {2} output tokens. {3} cache read tokens. {4} cache write tokens. {5} requests.", new[] { "{0} utilisés. {1} jetons en entrée. {2} jetons en sortie. {3} jetons lus en cache. {4} jetons écrits en cache. {5} requêtes.", "تم استخدام {0}. {1} رموز إدخال. {2} رموز إخراج. {3} رموز قراءة من الذاكرة المؤقتة. {4} رموز كتابة في الذاكرة المؤقتة. {5} طلبات." } },
{ "Input", new[] { "Entrée", "الإدخال" } },
{ "Output", new[] { "Sortie", "الإخراج" } },
{ "{0}: {1} used, no limit.", new[] { "{0} : {1} utilisés, sans limite.", "{0}: تم استخدام {1}، بلا حد." } },
{ "{0}: {1} of {2} used. {3} left.", new[] { "{0} : {1} utilisés sur {2}. Il reste {3}.", "{0}: تم استخدام {1} من {2}. المتبقي {3}." } },
{ "Allowance used up. Buy a new passcode at nyxo.space.", new[] { "Quota épuisé. Achetez un nouveau code d'accès sur nyxo.space.", "تم استهلاك الرصيد. اشترِ رمز مرور جديدًا من nyxo.space." } },
{ "At least 80 percent used.", new[] { "Au moins 80 % utilisés.", "تم استخدام 80 بالمئة على الأقل." } },
{ "Live usage", new[] { "Consommation en direct", "الاستهلاك المباشر" } },
{ "Connecting.", new[] { "Connexion.", "جارٍ الاتصال." } },
{ "Paused by the admin.", new[] { "Suspendu par l'administrateur.", "موقوف من قِبل المسؤول." } },
{ "Claude is replying.", new[] { "Claude répond.", "Claude يردّ." } },
{ "This session.", new[] { "Cette session.", "هذه الجلسة." } },
{ "Started {0}.", new[] { "Commencée à {0}.", "بدأت الساعة {0}." } },
{ "All sessions.", new[] { "Toutes les sessions.", "كل الجلسات." } },
{ "Session ends after 24 hours unused.", new[] { "La session se termine après 24 heures d'inactivité.", "تنتهي الجلسة بعد 24 ساعة دون استخدام." } },
{ "Passcode expires {0}.", new[] { "Le code d'accès expire le {0}.", "تنتهي صلاحية رمز المرور في {0}." } },
{ "Passcode never expires.", new[] { "Le code d'accès n'expire jamais.", "رمز المرور لا تنتهي صلاحيته." } },
{ "Opus 5.5. 4 dollars per million input tokens; 20 dollars per million output tokens.", new[] { "Opus 5.5. 4 dollars par million de jetons en entrée ; 20 dollars par million de jetons en sortie.", "Opus 5.5. أربعة دولارات لكل مليون رمز إدخال؛ عشرون دولارًا لكل مليون رمز إخراج." } },
{ "Input, cache and output all come out of the daily allowance.", new[] { "L'entrée, le cache et la sortie sont tous décomptés du quota quotidien.", "الإدخال والذاكرة المؤقتة والإخراج كلها تُخصم من الرصيد اليومي." } },
{ "Cache billed as input.", new[] { "Cache facturé comme l'entrée.", "تُحتسب الذاكرة المؤقتة كإدخال." } },
{ "{0} used", new[] { "{0} utilisés", "تم استخدام {0}" } },
{ "No limit on this passcode", new[] { "Aucune limite sur ce code d'accès", "لا يوجد حد لرمز المرور هذا" } },
{ "{0} of {1}", new[] { "{0} sur {1}", "{0} من {1}" } },
{ "{0} left for input, cache and output", new[] { "Il reste {0} pour l'entrée, le cache et la sortie", "المتبقي {0} للإدخال والذاكرة المؤقتة والإخراج" } },
{ "{0} left", new[] { "Il reste {0}", "المتبقي {0}" } },
{ "~{0} tokens", new[] { "~{0} jetons", "~{0} رمز" } },
{ "Tokens", new[] { "Jetons E/S", "إدخال / إخراج" } },
{ "{0} in / {1} out", new[] { "{0} / {1}", "{0} / {1}" } },
{ "Cache", new[] { "Cache", "الكاش" } },
{ "Requests", new[] { "Requêtes", "الطلبات" } },
{ "Claude is replying...", new[] { "Claude répond...", "Claude يردّ..." } },
{ "Live", new[] { "En direct", "مباشر" } },
{ "Waiting for the Nyxo server...", new[] { "En attente du serveur Nyxo...", "في انتظار خادم Nyxo..." } },
{ "Today", new[] { "Aujourd'hui", "اليوم" } },
{ "Renews", new[] { "Renouvellement", "التجديد" } },
{ "in {0}", new[] { "dans {0}", "بعد {0}" } },
{ "Every day at 00:00 UTC ({0} your time).", new[] { "Chaque jour à 00:00 UTC ({0} heure locale).", "كل يوم عند 00:00 UTC ({0} بتوقيتك)." } },
{ "Drops to {0} a day on {1} (UTC).", new[] { "Passe à {0} par jour le {1} (UTC).", "ينخفض إلى {0} يوميًا في {1} (UTC)." } },
{ "Every day at 00:00 UTC ({0} your time). Unused allowance does not carry over.", new[] { "Chaque jour à 00:00 UTC ({0} heure locale). Le quota non utilisé n'est pas reporté.", "كل يوم عند 00:00 UTC ({0} بتوقيتك). الرصيد غير المستخدم لا يُرحَّل." } },
{ "Passcode revoked for repeated usage-policy violations. No refund is issued.", new[] { "Code d'accès révoqué pour des infractions répétées aux règles d'utilisation. Aucun remboursement n'est accordé.", "تم إلغاء رمز المرور بسبب مخالفات متكررة لسياسة الاستخدام. لا يتم استرداد المبلغ." } },
{ "Paused by the admin. Requests wait until it is resumed.", new[] { "Suspendu par l'administrateur. Les requêtes attendent sa réactivation.", "موقوف من قِبل المسؤول. تنتظر الطلبات حتى يُستأنف." } },
{ "Today's allowance is used up. It renews at {0} (00:00 UTC).", new[] { "Le quota du jour est épuisé. Il se renouvelle à {0} (00:00 UTC).", "تم استهلاك رصيد اليوم. يتجدّد عند {0} (00:00 UTC)." } },
{ "Input allowance used up. Buy a new passcode at nyxo.space.", new[] { "Quota d'entrée épuisé. Achetez un nouveau code d'accès sur nyxo.space.", "تم استهلاك رصيد الإدخال. اشترِ رمز مرور جديدًا من nyxo.space." } },
{ "Output allowance used up. Buy a new passcode at nyxo.space.", new[] { "Quota de sortie épuisé. Achetez un nouveau code d'accès sur nyxo.space.", "تم استهلاك رصيد الإخراج. اشترِ رمز مرور جديدًا من nyxo.space." } },
{ "Over 80% of today's allowance is used.", new[] { "Plus de 80 % du quota du jour est utilisé.", "تم استخدام أكثر من 80% من رصيد اليوم." } },
{ "Over 80% of the input budget is used.", new[] { "Plus de 80 % du budget d'entrée est utilisé.", "تم استخدام أكثر من 80% من ميزانية الإدخال." } },
{ "Over 80% of the output budget is used.", new[] { "Plus de 80 % du budget de sortie est utilisé.", "تم استخدام أكثر من 80% من ميزانية الإخراج." } },
{ "THIS SESSION", new[] { "EN COURS", "هذه الجلسة" } },
{ "since {0}", new[] { "depuis {0}", "منذ {0}" } },
{ "ALL SESSIONS", new[] { "TOUTES LES SESSIONS", "كل الجلسات" } },
{ "SESSION", new[] { "SESSION", "الجلسة" } },
{ "Ends after 24 h unused", new[] { "Fin : 24 h sans usage", "تنتهي بعد 24 ساعة خمول" } },
{ "PASSCODE", new[] { "CODE D'ACCÈS", "رمز المرور" } },
{ "Expires {0}", new[] { "Expire le {0}", "تنتهي في {0}" } },
{ "Never expires", new[] { "N'expire jamais", "لا تنتهي صلاحيته" } },
{ "Opus 5.5 - $4 / 1M input, $20 / 1M output, all from the daily allowance", new[] { "Opus 5.5 - 4 $ / 1M entrée, 20 $ / 1M sortie, tout sur le quota", "النموذج Opus 5.5: إدخال 4$ وإخراج 20$ لكل مليون رمز، من الرصيد اليومي" } },
{ "Opus 5.5 - $4 / 1M input, $20 / 1M output, cache billed as input", new[] { "Opus 5.5 - 4 $ / 1M entrée, 20 $ / 1M sortie, cache = entrée", "النموذج Opus 5.5: إدخال 4$ وإخراج 20$ لكل مليون رمز، الكاش كإدخال" } },
{ "Passcode", new[] { "Code d'accès", "رمز المرور" } },
{ "Remember on this PC", new[] { "Mémoriser sur ce PC", "التذكّر على هذا الكمبيوتر" } },
{ "Connect", new[] { "Se connecter", "اتصال" } },
{ "Saved passcodes", new[] { "Codes d'accès enregistrés", "رموز المرور المحفوظة" } },
{ "Claude Code on your Nyxo passcode", new[] { "Claude Code avec votre code d'accès Nyxo", "Claude Code برمز مرور Nyxo الخاص بك" } },
{ "SAVED PASSCODES", new[] { "CODES ENREGISTRÉS", "رموز المرور المحفوظة" } },
{ "Right-click to rename", new[] { "Clic droit pour renommer", "انقر بزر الفأرة الأيمن لإعادة التسمية" } },
{ "or enter a passcode", new[] { "ou saisissez un code d'accès", "أو أدخل رمز مرور" } },
{ "Sign out", new[] { "Se déconnecter", "تسجيل الخروج" } },
{ "Keep on top", new[] { "Toujours visible", "إبقاء في المقدمة" } },
{ "Choose a folder, or paste its path", new[] { "Choisissez un dossier ou collez son chemin", "اختر مجلدًا أو الصق مساره" } },
{ "Browse", new[] { "Parcourir", "استعراض" } },
{ "How to install Claude Code", new[] { "Comment installer Claude Code", "كيفية تثبيت Claude Code" } },
{ "Recent folders", new[] { "Dossiers récents", "المجلدات الأخيرة" } },
{ "Connected as \"{0}\"", new[] { "Connecté en tant que « {0} »", "متصل باسم «{0}»" } },
{ "Connected", new[] { "Connecté", "متصل" } },
{ "It opens in its own window, working in this folder.", new[] { "Il s'ouvre dans sa propre fenêtre, dans ce dossier.", "يُفتح في نافذته الخاصة ويعمل في هذا المجلد." } },
{ "FOLDER", new[] { "DOSSIER", "المجلد" } },
{ "RECENT", new[] { "RÉCENTS", "الأخيرة" } },
{ "Folders you open will show up here.", new[] { "Les dossiers que vous ouvrez apparaîtront ici.", "ستظهر هنا المجلدات التي تفتحها." } },
{ "Policy strikes: -", new[] { "Avertissements : -", "المخالفات: -" } },
{ "Hide this notice", new[] { "Masquer cet avis", "إخفاء هذا الإشعار" } },
{ "The download didn't finish. Check your connection.", new[] { "Le téléchargement n'a pas abouti. Vérifiez votre connexion.", "لم يكتمل التنزيل. تحقّق من اتصالك." } },
{ "Nyxo Code can't replace its file in this folder.", new[] { "Nyxo Code ne peut pas remplacer son fichier dans ce dossier.", "لا يستطيع Nyxo Code استبدال ملفه في هذا المجلد." } },
{ "Something went wrong.", new[] { "Une erreur s'est produite.", "حدث خطأ ما." } },
{ "The download is larger than announced.", new[] { "Le téléchargement est plus volumineux qu'annoncé.", "حجم التنزيل أكبر من المُعلن." } },
{ "The download is incomplete.", new[] { "Le téléchargement est incomplet.", "التنزيل غير مكتمل." } },
{ "The download failed its security check (SHA-256).", new[] { "Le téléchargement a échoué au contrôle de sécurité (SHA-256).", "فشل التنزيل في فحص الأمان (SHA-256)." } },
{ "The download is not the announced version.", new[] { "Le téléchargement n'est pas la version annoncée.", "التنزيل ليس الإصدار المُعلن." } },
{ "Rename...", new[] { "Renommer...", "إعادة التسمية..." } },
{ "Remove from this PC", new[] { "Supprimer de ce PC", "حذف من هذا الكمبيوتر" } },
{ "{0} h {1} min", new[] { "{0} h {1} min", "{0} س {1} د" } },
{ "{0} min", new[] { "{0} min", "{0} د" } },
{ "Too many attempts. Try again later.", new[] { "Trop de tentatives. Réessayez plus tard.", "محاولات كثيرة جدًا. حاول مرة أخرى لاحقًا." } },
{ "The service is unavailable right now. Try again in a minute.", new[] { "Le service est indisponible pour le moment. Réessayez dans une minute.", "الخدمة غير متاحة حاليًا. حاول مرة أخرى بعد دقيقة." } },
{ "The service is unavailable right now.", new[] { "Le service est indisponible pour le moment.", "الخدمة غير متاحة حاليًا." } },
{ "This passcode has expired. Get a new passcode at nyxo.space.", new[] { "Ce code d'accès a expiré. Obtenez un nouveau code d'accès sur nyxo.space.", "انتهت صلاحية رمز المرور هذا. احصل على رمز مرور جديد من nyxo.space." } },
{ "Passcode not accepted. Check the code; it may have expired or been revoked.", new[] { "Code d'accès refusé. Vérifiez le code ; il a peut-être expiré ou été révoqué.", "لم يتم قبول رمز المرور. تحقّق من الرمز؛ ربما انتهت صلاحيته أو أُلغي." } },
{ "This passcode is paused by the admin.", new[] { "Ce code d'accès est suspendu par l'administrateur.", "رمز المرور هذا موقوف من قِبل المسؤول." } },
{ "This passcode is paused by the admin. Wait until it is resumed.", new[] { "Ce code d'accès est suspendu par l'administrateur. Attendez sa réactivation.", "رمز المرور هذا موقوف من قِبل المسؤول. انتظر حتى يُستأنف." } },
{ "This passcode needs the latest Nyxo Code. Download it from nyxo.space.", new[] { "Ce code d'accès nécessite la dernière version de Nyxo Code. Téléchargez-la sur nyxo.space.", "يتطلّب رمز المرور هذا أحدث إصدار من Nyxo Code. نزّله من nyxo.space." } },
{ "This version of Nyxo Code is out of date. Download the latest one from nyxo.space.", new[] { "Cette version de Nyxo Code est obsolète. Téléchargez la dernière sur nyxo.space.", "هذا الإصدار من Nyxo Code قديم. نزّل أحدث إصدار من nyxo.space." } },
{ "This passcode is already in use on another device. Each passcode works on one device only.", new[] { "Ce code d'accès est déjà utilisé sur un autre appareil. Chaque code d'accès ne fonctionne que sur un seul appareil.", "رمز المرور هذا مستخدم بالفعل على جهاز آخر. كل رمز مرور يعمل على جهاز واحد فقط." } },
{ "Nyxo Code is busy right now. Try again in a few minutes.", new[] { "Nyxo Code est très sollicité en ce moment. Réessayez dans quelques minutes.", "Nyxo Code مشغول حاليًا. حاول مرة أخرى بعد بضع دقائق." } },
{ "Something went wrong. Please try again.", new[] { "Une erreur s'est produite. Veuillez réessayer.", "حدث خطأ ما. يرجى المحاولة مرة أخرى." } },
{ "This passcode is paused for 1 hour after repeated usage-policy refusals. It resumes at {0} UTC (in {1}).", new[] { "Ce code d'accès est suspendu pendant 1 heure après des refus répétés liés aux règles d'utilisation. Il reprend à {0} UTC (dans {1}).", "رمز المرور هذا موقوف لمدة ساعة بعد رفض متكرر بسبب سياسة الاستخدام. سيُستأنف عند {0} UTC (بعد {1})." } },
{ "Buy a passcode", new[] { "Acheter un code d'accès", "شراء رمز مرور" } },
{ "Renew", new[] { "Renouveler", "تجديد" } },
{ "Renew passcode", new[] { "Renouveler le code d'accès", "تجديد رمز المرور" } },
{ "Pay with USDT", new[] { "Payer en USDT", "الدفع بعملة USDT" } },
{ "Name for the passcode", new[] { "Nom du code d'accès", "اسم رمز المرور" } },
{ "Buy a new passcode instead", new[] { "Acheter plutôt un nouveau code", "شراء رمز مرور جديد بدلًا من ذلك" } },
{ "Copy", new[] { "Copier", "نسخ" } },
{ "Copy amount", new[] { "Copier le montant", "نسخ المبلغ" } },
{ "Plans", new[] { "Formules", "الباقات" } },
{ "Address", new[] { "Adresse", "العنوان" } },
{ "Continue", new[] { "Continuer", "متابعة" } },
{ "Cancel order", new[] { "Annuler la commande", "إلغاء الطلب" } },
{ "Use it", new[] { "L'utiliser", "استخدامه" } },
{ "This plan is sold out.", new[] { "Cette formule est épuisée.", "نفدت هذه الباقة." } },
{ "Copied.", new[] { "Copié.", "تم النسخ." } },
{ "Couldn't copy. Select the text and copy it instead.", new[] { "Copie impossible. Sélectionnez le texte et copiez-le.", "تعذّر النسخ. حدّد النص وانسخه يدويًا." } },
{ "Could not reach nyxo.space. Check your internet connection.", new[] { "Impossible de joindre nyxo.space. Vérifiez votre connexion Internet.", "تعذّر الوصول إلى nyxo.space. تحقّق من اتصالك بالإنترنت." } },
{ "Loading your order...", new[] { "Chargement de votre commande...", "جارٍ تحميل طلبك..." } },
{ "Loading plans...", new[] { "Chargement des formules...", "جارٍ تحميل الباقات..." } },
{ "Checkout is not available right now. Try again in a minute.", new[] { "Le paiement n'est pas disponible pour le moment. Réessayez dans une minute.", "الدفع غير متاح حاليًا. حاول مرة أخرى بعد دقيقة." } },
{ "This passcode can't be renewed in the app. Buy a new passcode instead.", new[] { "Ce code d'accès ne peut pas être renouvelé dans l'application. Achetez plutôt un nouveau code.", "لا يمكن تجديد رمز المرور هذا من التطبيق. اشترِ رمز مرور جديدًا بدلًا من ذلك." } },
{ "No plans are available right now.", new[] { "Aucune formule n'est disponible pour le moment.", "لا توجد باقات متاحة حاليًا." } },
{ "All plans are sold out right now.", new[] { "Toutes les formules sont épuisées pour le moment.", "نفدت جميع الباقات حاليًا." } },
{ "Best value", new[] { "Meilleur rapport", "أفضل قيمة" } },
{ "Sold out", new[] { "Épuisé", "نفدت" } },
{ "Adds {0} a day for {1} days", new[] { "Ajoute {0} par jour pendant {1} jours", "يضيف {0} يوميًا لمدة {1} يومًا" } },
{ "{0} a day for {1} days", new[] { "{0} par jour pendant {1} jours", "{0} يوميًا لمدة {1} يومًا" } },
{ "Adds {0} input and {1} output budget", new[] { "Ajoute {0} de budget d'entrée et {1} de budget de sortie", "يضيف {0} لميزانية الإدخال و{1} لميزانية الإخراج" } },
{ "{0} input and {1} output budget, never expires", new[] { "{0} de budget d'entrée et {1} de sortie, sans expiration", "{0} لميزانية الإدخال و{1} لميزانية الإخراج، بلا انتهاء صلاحية" } },
{ "Cancel this order?", new[] { "Annuler cette commande ?", "إلغاء هذا الطلب؟" } },
{ "Don't cancel if you already sent the payment: it would not be matched to this order.", new[] { "N'annulez pas si vous avez déjà envoyé le paiement : il ne serait pas associé à cette commande.", "لا تُلغِ الطلب إذا كنت قد أرسلت الدفعة بالفعل: لن تُطابَق مع هذا الطلب." } },
{ "Keep order", new[] { "Garder la commande", "الإبقاء على الطلب" } },
{ "Choose a plan.", new[] { "Choisissez une formule.", "اختر باقة." } },
{ "Enter a name for the passcode, so you can tell it apart.", new[] { "Saisissez un nom pour le code d'accès, afin de le reconnaître.", "أدخل اسمًا لرمز المرور لتمييزه." } },
{ "Use 1 to 40 letters, numbers, spaces, dots, dashes or underscores.", new[] { "Utilisez de 1 à 40 lettres, chiffres, espaces, points, tirets ou tirets bas.", "استخدم من 1 إلى 40 حرفًا أو رقمًا أو مسافة أو نقطة أو شرطة أو شرطة سفلية." } },
{ "Creating your order...", new[] { "Création de votre commande...", "جارٍ إنشاء طلبك..." } },
{ "Your last order is no longer available.", new[] { "Votre dernière commande n'est plus disponible.", "طلبك الأخير لم يعد متاحًا." } },
{ "Order cancelled.", new[] { "Commande annulée.", "تم إلغاء الطلب." } },
{ "The order closed with {0} USDT received, short of the full amount. Contact support at nyxo.space with the order address: {1}", new[] { "La commande s'est fermée avec {0} USDT reçus, en dessous du montant total. Contactez le support sur nyxo.space avec l'adresse de la commande : {1}", "أُغلق الطلب بعد استلام {0} USDT، وهو أقل من المبلغ الكامل. تواصل مع الدعم عبر nyxo.space مع ذكر عنوان الطلب: {1}" } },
{ "The order expired before a payment arrived.", new[] { "La commande a expiré avant l'arrivée d'un paiement.", "انتهت صلاحية الطلب قبل وصول أي دفعة." } },
{ "Renew \"{0}\"", new[] { "Renouveler « {0} »", "تجديد «{0}»" } },
{ "Adds the plan's daily allowance for 30 days from payment, on top of what you have. Pay in USDT (BEP20).", new[] { "Ajoute l'allocation quotidienne de la formule pendant 30 jours à partir du paiement, en plus de la vôtre. Payez en USDT (BEP20).", "يضيف الحد اليومي للباقة لمدة 30 يومًا من الدفع، فوق ما لديك. ادفع بعملة USDT على شبكة BEP20." } },
{ "Adds budget to this passcode once the payment arrives. Pay in USDT on BNB Smart Chain (BEP20).", new[] { "Ajoute du budget à ce code d'accès dès l'arrivée du paiement. Payez en USDT sur BNB Smart Chain (BEP20).", "يضيف ميزانية إلى رمز المرور هذا فور وصول الدفعة. ادفع بعملة USDT على شبكة BNB Smart Chain (BEP20)." } },
{ "Pay in USDT on BNB Smart Chain (BEP20). The passcode works on one device and is saved on this PC.", new[] { "Payez en USDT sur BNB Smart Chain (BEP20). Le code d'accès fonctionne sur un seul appareil et est enregistré sur ce PC.", "ادفع بعملة USDT على شبكة BNB Smart Chain (BEP20). يعمل رمز المرور على جهاز واحد ويُحفظ على هذا الكمبيوتر." } },
{ "CHOOSE A PLAN", new[] { "CHOISISSEZ UNE FORMULE", "اختر باقة" } },
{ "NAME", new[] { "NOM", "الاسم" } },
{ "Pay {0} USDT", new[] { "Payer {0} USDT", "ادفع {0} USDT" } },
{ "{0} plan, renewing \"{1}\"", new[] { "Formule {0}, renouvellement de « {1} »", "باقة {0}، تجديد «{1}»" } },
{ "{0} plan, new passcode \"{1}\"", new[] { "Formule {0}, nouveau code « {1} »", "باقة {0}، رمز مرور جديد «{1}»" } },
{ "AMOUNT", new[] { "MONTANT", "المبلغ" } },
{ "NETWORK", new[] { "RÉSEAU", "الشبكة" } },
{ "TIME LEFT", new[] { "TEMPS RESTANT", "الوقت المتبقي" } },
{ "ADDRESS", new[] { "ADRESSE", "العنوان" } },
{ "Payment received. Confirming on the network...", new[] { "Paiement reçu. Confirmation sur le réseau...", "تم استلام الدفعة. جارٍ التأكيد على الشبكة..." } },
{ "Time is up. Checking for a last payment...", new[] { "Le temps est écoulé. Recherche d'un dernier paiement...", "انتهى الوقت. جارٍ التحقق من وصول دفعة أخيرة..." } },
{ "Received {0} of {1} USDT. Send the remaining {2} USDT to the same address.", new[] { "{0} USDT reçus sur {1}. Envoyez les {2} USDT restants à la même adresse.", "تم استلام {0} من {1} USDT. أرسل المبلغ المتبقي {2} USDT إلى العنوان نفسه." } },
{ "Waiting for your payment...", new[] { "En attente de votre paiement...", "في انتظار دفعتك..." } },
{ "Send exactly this amount of USDT on BNB Smart Chain (BEP20). Other tokens or networks can't be recovered.", new[] { "Envoyez exactement ce montant en USDT sur BNB Smart Chain (BEP20). Les autres jetons ou réseaux ne peuvent pas être récupérés.", "أرسل هذا المبلغ بالضبط بعملة USDT على شبكة BNB Smart Chain (BEP20). لا يمكن استرداد العملات أو الشبكات الأخرى." } },
{ "You can close this window: the order stays open, and Nyxo Code shows it again next time.", new[] { "Vous pouvez fermer cette fenêtre : la commande reste ouverte et Nyxo Code l'affichera à nouveau.", "يمكنك إغلاق هذه النافذة: يبقى الطلب مفتوحًا وسيعرضه Nyxo Code مجددًا في المرة القادمة." } },
{ "Passcode renewed", new[] { "Code d'accès renouvelé", "تم تجديد رمز المرور" } },
{ "Not renewed", new[] { "Non renouvelé", "لم يتم التجديد" } },
{ "This passcode was revoked for usage-policy violations, so nothing was added. No refund is issued.", new[] { "Ce code d'accès a été révoqué pour violations de la politique d'utilisation : rien n'a été ajouté. Aucun remboursement n'est effectué.", "أُلغي رمز المرور هذا بسبب مخالفات سياسة الاستخدام، لذلك لم يُضف شيء. لا يُصدر أي استرداد." } },
{ "The passcode could not be renewed, so a new one was issued for this plan.", new[] { "Le code d'accès n'a pas pu être renouvelé : un nouveau code a été émis pour cette formule.", "تعذّر تجديد رمز المرور، لذلك صدر رمز جديد لهذه الباقة." } },
{ "Your passcode is ready", new[] { "Votre code d'accès est prêt", "رمز المرور الخاص بك جاهز" } },
{ "Payment complete", new[] { "Paiement terminé", "اكتمل الدفع" } },
{ "The {0} plan was added to \"{1}\".", new[] { "La formule {0} a été ajoutée à « {1} ».", "أُضيفت باقة {0} إلى «{1}»." } },
{ "Saved on this PC as \"{0}\". It works on one device only.", new[] { "Enregistré sur ce PC sous « {0} ». Il ne fonctionne que sur un seul appareil.", "حُفظ على هذا الكمبيوتر باسم «{0}». يعمل على جهاز واحد فقط." } },
{ "The passcode is no longer shown here. Contact support at nyxo.space.", new[] { "Le code d'accès n'est plus affiché ici. Contactez le support sur nyxo.space.", "لم يعد رمز المرور معروضًا هنا. تواصل مع الدعم عبر nyxo.space." } },
{ "Keep a copy somewhere safe too.", new[] { "Gardez-en aussi une copie en lieu sûr.", "احتفظ بنسخة منه في مكان آمن أيضًا." } },
{ "It works on one device only.", new[] { "Il ne fonctionne que sur un seul appareil.", "يعمل على جهاز واحد فقط." } },
{ "Copied. It is removed from the clipboard in 1 minute.", new[] { "Copié. Il sera retiré du presse-papiers dans 1 minute.", "تم النسخ. سيُزال من الحافظة بعد دقيقة واحدة." } },
{ "Cancelling...", new[] { "Annulation...", "جارٍ الإلغاء..." } },
{ "Nyxo Code ran into a problem and has to close:", new[] { "Nyxo Code a rencontré un problème et doit se fermer :", "واجه Nyxo Code مشكلة ويجب إغلاقه:" } },
{ "Language: {0}", new[] { "Langue : {0}", "اللغة: {0}" } },
{ "Press", new[] { "Appuyer", "اضغط" } },
{ "Toggle", new[] { "Basculer", "تبديل" } },
{ "Open", new[] { "Ouvrir", "فتح" } },
{ "Nyxo Code {0} is ready. It installs when you close the purchase window.", new[] { "Nyxo Code {0} est prêt. Il s'installera à la fermeture de la fenêtre d'achat.", "Nyxo Code {0} جاهز. سيُثبَّت عند إغلاق نافذة الشراء." } },
{ "Update failed, and Nyxo Code's file could not be put back.", new[] { "La mise à jour a échoué et le fichier de Nyxo Code n'a pas pu être remis en place.", "فشل التحديث وتعذّرت استعادة ملف Nyxo Code." } },
{ "Update failed", new[] { "Échec de la mise à jour", "فشل التحديث" } },
{ "The update failed, and Nyxo Code's file could not be put back. The previous version is saved as:", new[] { "La mise à jour a échoué et le fichier de Nyxo Code n'a pas pu être remis en place. La version précédente est enregistrée ici :", "فشل التحديث وتعذّرت استعادة ملف Nyxo Code. الإصدار السابق محفوظ باسم:" } },
{ "Rename that file to \"{0}\" to use Nyxo Code again.", new[] { "Renommez ce fichier en « {0} » pour utiliser à nouveau Nyxo Code.", "أعد تسمية هذا الملف إلى «{0}» لاستخدام Nyxo Code مجددًا." } },
{ "The server no longer accepts this session. The passcode may be paused or revoked.", new[] { "Le serveur n'accepte plus cette session. Le code d'accès est peut-être suspendu ou révoqué.", "لم يعد الخادم يقبل هذه الجلسة. ربما أُوقف رمز المرور أو أُلغي." } },
{ "Checkout is not available right now.", new[] { "Le paiement n'est pas disponible pour le moment.", "الدفع غير متاح حاليًا." } },
{ "Checkout is busy. Try again in a few minutes.", new[] { "Le paiement est très sollicité. Réessayez dans quelques minutes.", "الدفع مشغول حاليًا. حاول مرة أخرى بعد بضع دقائق." } },
{ "Checkout is busy. Try again later.", new[] { "Le paiement est très sollicité. Réessayez plus tard.", "الدفع مشغول حاليًا. حاول مرة أخرى لاحقًا." } },
{ "You already have open orders. Pay or cancel them first.", new[] { "Vous avez déjà des commandes ouvertes. Payez-les ou annulez-les d'abord.", "لديك طلبات مفتوحة بالفعل. ادفعها أو ألغِها أولًا." } },
{ "Too many orders. Try again later.", new[] { "Trop de commandes. Réessayez plus tard.", "طلبات كثيرة جدًا. حاول مرة أخرى لاحقًا." } },
{ "This passcode can't be renewed. It may have expired or been revoked.", new[] { "Ce code d'accès ne peut pas être renouvelé. Il a peut-être expiré ou été révoqué.", "لا يمكن تجديد رمز المرور هذا. ربما انتهت صلاحيته أو أُلغي." } },
{ "This passcode was revoked for usage-policy violations and can't be renewed.", new[] { "Ce code d'accès a été révoqué pour violation des règles d'utilisation et ne peut pas être renouvelé.", "أُلغي رمز المرور هذا بسبب مخالفات سياسة الاستخدام ولا يمكن تجديده." } },
{ "This passcode is paused by the admin and can't be renewed right now.", new[] { "Ce code d'accès est suspendu par l'administrateur et ne peut pas être renouvelé pour le moment.", "رمز المرور هذا موقوف من قِبل المسؤول ولا يمكن تجديده حاليًا." } },
{ "Top up a pay-as-you-go passcode with a pay-as-you-go amount.", new[] { "Rechargez un code à l'usage avec un montant à l'usage.", "اشحن رمز الدفع حسب الاستخدام بمبلغ دفع حسب الاستخدام." } },
{ "Adds {0} to the balance, usable for 12 months", new[] { "Ajoute {0} au solde, utilisable 12 mois", "يضيف {0} إلى الرصيد، صالح لمدة 12 شهرًا" } },
{ "{0} balance, use it any time for 12 months", new[] { "Solde de {0}, à utiliser quand vous voulez pendant 12 mois", "رصيد {0}، استخدمه متى شئت لمدة 12 شهرًا" } },
{ "Adds the amount to this passcode's balance once the payment arrives. Pay in USDT (BEP20).", new[] { "Ajoute le montant au solde de ce code d'accès dès réception du paiement. Paiement en USDT (BEP20).", "يُضاف المبلغ إلى رصيد رمز المرور هذا فور وصول الدفعة. الدفع بـ USDT (BEP20)." } },
{ "Balance: {0} of {1} used. {2} left.", new[] { "Solde : {0} utilisés sur {1}. Il reste {2}.", "الرصيد: تم استخدام {0} من {1}. المتبقي {2}." } },
{ "The balance is used up. Top it up with Renew.", new[] { "Le solde est épuisé. Rechargez-le avec Renouveler.", "نفد الرصيد. اشحنه عبر زر تجديد." } },
{ "At least 80 percent of the balance used.", new[] { "Au moins 80 % du solde utilisé.", "تم استخدام 80 بالمئة على الأقل من الرصيد." } },
{ "Input, cache and output all come out of the balance.", new[] { "L'entrée, le cache et la sortie sont tous décomptés du solde.", "الإدخال والذاكرة المؤقتة والإخراج كلها تُخصم من الرصيد." } },
{ "Balance", new[] { "Solde", "الرصيد" } },
{ "Choose your balance", new[] { "Choisissez votre solde", "اختر رصيدك" } },
{ "Pay as you go", new[] { "Paiement à l'usage", "الدفع حسب الاستخدام" } },
{ "No daily limit", new[] { "Sans limite quotidienne", "بلا حد يومي" } },
{ "Use it at your own pace. Top it up any time with Renew; unused balance stays on the passcode.", new[] { "Utilisez-le à votre rythme. Rechargez-le quand vous voulez avec Renouveler ; le solde non utilisé reste sur le code.", "استخدمه على راحتك. اشحنه متى شئت عبر زر تجديد؛ يبقى الرصيد غير المستخدم على رمز المرور." } },
{ "Over 80% of the balance is used.", new[] { "Plus de 80 % du solde est utilisé.", "تم استخدام أكثر من 80% من الرصيد." } },
{ "Opus 5.5 - $4 / 1M input, $20 / 1M output, all from the balance", new[] { "Opus 5.5 - 4 $ / 1M entrée, 20 $ / 1M sortie, tout sur le solde", "النموذج Opus 5.5: إدخال 4$ وإخراج 20$ لكل مليون رمز، من الرصيد" } },
{ "Top up a monthly passcode with a monthly plan.", new[] { "Rechargez un code mensuel avec une formule mensuelle.", "اشحن رمز المرور الشهري بباقة شهرية." } },
{ "This passcode already has as much time as it can hold. Renew it later.", new[] { "Ce code d'accès a déjà le maximum de temps possible. Renouvelez-le plus tard.", "رمز المرور هذا لديه أقصى مدة ممكنة بالفعل. جدّده لاحقًا." } },
{ "Top up this passcode with a plan that has input and output budgets.", new[] { "Rechargez ce code d'accès avec une formule comprenant des budgets d'entrée et de sortie.", "اشحن رمز المرور هذا بباقة تتضمن ميزانيتي إدخال وإخراج." } },
{ "Could not create the order. Try again in a moment.", new[] { "Impossible de créer la commande. Réessayez dans un instant.", "تعذّر إنشاء الطلب. حاول مرة أخرى بعد لحظة." } },
{ "A payment was already detected, so this order cannot be cancelled.", new[] { "Un paiement a déjà été détecté : cette commande ne peut pas être annulée.", "تم رصد دفعة بالفعل، لذا لا يمكن إلغاء هذا الطلب." } },
{ "This order is being checked. Try again in a moment.", new[] { "Cette commande est en cours de vérification. Réessayez dans un instant.", "يجري التحقق من هذا الطلب. حاول مرة أخرى بعد لحظة." } },
{ "Could not cancel the order.", new[] { "Impossible d'annuler la commande.", "تعذّر إلغاء الطلب." } },
{ "Order not found.", new[] { "Commande introuvable.", "الطلب غير موجود." } },
{ "Could not read the order.", new[] { "Impossible de lire la commande.", "تعذّرت قراءة الطلب." } },
};
}
App.csالنافذة الرئيسية، وبث الاستهلاك المباشر، وتسجيل الخروج عند الإغلاق.868 سطر
// The main window: sign in, pick a folder, open Claude Code, and watch usage live.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Media;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
using System.Windows.Forms;
class MainForm : Form
{
readonly LoginPage login = new LoginPage { Dock = DockStyle.Fill };
readonly HomePage home = new HomePage { Dock = DockStyle.Fill, Visible = false };
readonly List<Process> running = new List<Process>();
readonly Dictionary<Process, string> procToken = new Dictionary<Process, string>(); // which session each Claude Code window uses
readonly ContextMenuStrip savedMenu;
RowList.Item menuItem;
string token;
string passcode, label; // the signed-in passcode and its name, kept in memory only to renew it
volatile string claude; volatile bool claudeSearched;
volatile int gen; volatile HttpWebRequest current;
int alertIn, alertOut, alertDay, alertTotal;
// Usage-policy state last shown, so a popup appears only when it gets worse (a new strike, a pause, a revocation).
int lastStrikes; bool lastPaused, lastRevoked, popupOpen;
readonly Queue<string[]> popups = new Queue<string[]>();
double refreshedFor;
volatile bool closing;
// Updates: the bar at the top, the newest version found, and whether an update is downloading or installing.
readonly UpdateBar bar = new UpdateBar { Dock = DockStyle.Top };
Updater.Info update;
Version dismissed;
bool updating; volatile bool exiting;
string pendingFile; Updater.Info pendingInfo; // an update that waits for the purchase window to close
int minH0;
Action barAgain; // shows the bar's current message again, in a newly chosen language
[StructLayout(LayoutKind.Sequential)] struct FLASHWINFO { public uint cbSize; public IntPtr hwnd; public uint dwFlags; public uint uCount; public uint dwTimeout; }
[DllImport("user32.dll")] static extern bool FlashWindowEx(ref FLASHWINFO f);
public MainForm()
{
Text = "Nyxo Code"; AutoScaleMode = AutoScaleMode.None; FormBorderStyle = FormBorderStyle.Sizable; MaximizeBox = true;
StartPosition = FormStartPosition.CenterScreen; BackColor = T.Bg; ForeColor = T.Text; Font = T.Body;
try { Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); } catch { }
ClientSize = new Size(T.P(800), T.P(560));
MinimumSize = Size;
// Fit small screens: never open larger than the work area.
var wa = Screen.PrimaryScreen.WorkingArea;
if (Width > wa.Width || Height > wa.Height) { MinimumSize = new Size(Math.Min(Width, wa.Width), Math.Min(Height, wa.Height)); Size = MinimumSize; }
// The pages keep their full size inside a scrolling viewport, so nothing is cut off on a small screen.
var viewport = new Panel { Dock = DockStyle.Fill, AutoScroll = true, BackColor = T.Bg };
var canvas = new Panel { BackColor = T.Bg };
Action sizeCanvas = () => canvas.Size = new Size(Math.Max(T.P(800), viewport.ClientSize.Width), Math.Max(T.P(560), viewport.ClientSize.Height));
canvas.Controls.Add(home); canvas.Controls.Add(login);
viewport.Controls.Add(canvas);
Controls.Add(viewport);
Controls.Add(bar); // added after the viewport, so it docks first and takes the top
minH0 = MinimumSize.Height;
bar.VisibleChanged += (s, e) =>
{
// Keep the pages at full size under the bar: the window grows by the bar's height, within the screen.
int h = bar.Visible ? Math.Min(minH0 + bar.Height, Screen.FromControl(this).WorkingArea.Height) : minH0;
MinimumSize = new Size(MinimumSize.Width, Math.Max(minH0, h));
};
bar.UpdateBtn.Click += (s, e) => DoUpdate();
bar.Notes.Click += (s, e) => ShowNotes();
bar.Later.Click += (s, e) => { if (update != null) dismissed = update.Version; bar.Visible = false; };
L.Changed += Relabel;
var updTimer = new System.Windows.Forms.Timer { Interval = 6 * 60 * 60 * 1000 };
updTimer.Tick += (s, e) => CheckForUpdate();
updTimer.Start();
viewport.Resize += (s, e) => sizeCanvas();
sizeCanvas();
login.Connect.Click += (s, e) => DoLogin(null);
login.SavedList.Pick += it => DoLogin((Nyxo.Saved)it.Tag);
login.SavedList.Remove += it => RemoveSaved((Nyxo.Saved)it.Tag);
login.SavedList.Menu += (it, p) => { menuItem = it; savedMenu.Show(login.SavedList, p); };
savedMenu = new ContextMenuStrip { Renderer = new ToolStripProfessionalRenderer(new DarkColors()), ShowImageMargin = false, Font = T.Body };
foreach (var name in new[] { "Connect", "Rename...", "Remove from this PC" })
{
var mi = new ToolStripMenuItem(L.T(name)) { Tag = name, ForeColor = name.StartsWith("Remove") ? T.Danger : T.Text, Padding = new Padding(T.P(4)) };
mi.Click += SavedMenuClick;
savedMenu.Items.Add(mi);
}
savedMenu.RightToLeft = T.Rtl ? RightToLeft.Yes : RightToLeft.No;
home.SignOut.Click += (s, e) => ShowLogin(L.T("Signed out."), T.Dim);
home.OnTop.Changed += (s, e) => TopMost = home.OnTop.Checked;
home.History.Click += (s, e) => ShowHistory();
home.Renew.Click += (s, e) => ShowBuy(true);
login.Buy.Click += (s, e) => ShowBuy(false);
home.Browse.Click += (s, e) => BrowseFolder();
home.Open.Click += (s, e) => OpenClaude();
home.Install.Click += (s, e) => { try { Process.Start(new ProcessStartInfo(Nyxo.InstallUrl) { UseShellExecute = true }); } catch { } };
home.Recent.Pick += it =>
{
home.Folder.Text = (string)it.Tag; home.Recent.Selected = home.Recent.Items.IndexOf(it); home.Recent.Invalidate(); home.Open.Focus();
CheckRecent((string)it.Tag);
};
home.Recent.Remove += it => DropRecent((string)it.Tag);
home.Folder.Box.TextChanged += (s, e) =>
{
int i = home.Recent.Items.FindIndex(it => ((string)it.Tag).Equals(home.Folder.Text.Trim().Trim('"'), StringComparison.OrdinalIgnoreCase));
if (i != home.Recent.Selected) { home.Recent.Selected = i; home.Recent.Invalidate(); }
};
var tick = new System.Windows.Forms.Timer { Interval = 1000 };
tick.Tick += (s, e) => { if (home.Visible) { home.Usage.Invalidate(); home.TickPolicy(); RefreshAtPolicyEdge(); } };
tick.Start();
// Looking for Claude Code can touch slow disks, so it happens in the background.
Task.Run(() => { claude = Nyxo.FindClaude(); claudeSearched = true; Post(UpdateClaude); });
ShowLogin(null, T.Dim);
}
protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); T.DarkTitle(Handle); }
// A new language was picked: every text on screen follows at once.
void Relabel()
{
foreach (ToolStripMenuItem mi in savedMenu.Items) mi.Text = L.T((string)mi.Tag);
savedMenu.RightToLeft = T.Rtl ? RightToLeft.Yes : RightToLeft.No;
bar.Relabel();
if (barAgain != null) barAgain();
login.Relabel(); home.Relabel();
if (history != null) history.Relabel();
ShowSaved();
if (home.Visible) UpdateClaude();
Invalidate(true);
}
void ShowBar(Action show) { barAgain = show; show(); }
HistoryForm history;
void ShowHistory()
{
string tok = token;
if (tok == null || history != null) return;
using (history = new HistoryForm(n => Nyxo.History(tok, n)))
{
history.ShowDialog(this);
}
history = null;
}
// Buy a passcode (from the sign-in page) or renew the signed-in one (from the home page).
BuyForm buy;
void ShowBuy(bool renew)
{
if (buy != null || (renew && (token == null || passcode == null))) return;
var f = buy = new BuyForm(renew ? passcode : null, renew ? label : null, renew ? home.Usage.Data : null);
try { f.ShowDialog(this); }
finally { buy = null; f.Dispose(); }
if (IsDisposed) return;
if (pendingFile != null && updating)
{
string file = pendingFile; var info = pendingInfo;
pendingFile = null; pendingInfo = null;
FinishUpdate(file, info);
if (exiting) return;
}
if (login.Visible) ShowSaved();
if (f.Renewed && token != null)
{
string tok = token; int my = gen;
Task.Run(() =>
{
var u = Nyxo.Usage(tok);
if (u != null) Post(() => { if (my == gen && tok == token) OnUsage(u); });
});
}
if (f.UsePasscode != null)
{
if (home.Visible) ShowLogin(null, T.Dim);
if (!login.Connect.Enabled) return;
login.Pass.Text = f.UsePasscode;
DoLogin(null);
}
}
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
if (login.Visible) { if (login.SavedList.Items.Count > 0) login.Connect.Focus(); else login.Pass.Box.Focus(); }
if (claudeSearched) UpdateClaude();
Updater.CleanUp();
var from = Updater.UpdatedFrom();
if (from != null)
{
Updater.RefreshIcon(Updater.ExePath);
ShowBar(() => bar.Set(L.F("Nyxo Code was updated to {0} (from {1}). Sign in again to continue.", Nyxo.Version, from), T.Text, "\uE73E", false, null));
bar.Visible = true;
}
// The first check waits a moment, so it never slows down opening the app.
var first = new System.Windows.Forms.Timer { Interval = 3000 };
first.Tick += (s, ev) => { first.Stop(); first.Dispose(); CheckForUpdate(); };
first.Start();
}
// ---------- updates ----------
void CheckForUpdate()
{
if (updating) return;
Task.Run(() =>
{
var info = Updater.Check();
if (info != null) Post(() => ShowUpdate(info));
});
}
void ShowUpdate(Updater.Info info)
{
if (updating || IsDisposed) return;
if (dismissed != null && info.Version <= dismissed) return;
update = info;
bar.UpdateBtn.Enabled = bar.Later.Enabled = true;
ShowBar(() =>
{
bar.Set(L.F("Nyxo Code {0} is available. You have {1}.", info.Version.ToString(3), Nyxo.Version), T.Text, "\uE896", true, L.T("Update"));
if (Updater.NotesFor(info) != null) bar.ShowNotes();
});
bar.Visible = true;
}
void ShowNotes()
{
var info = update; var notes = Updater.NotesFor(info);
if (notes == null || updating) return;
string text = string.Join("\n", notes.ConvertAll(n => "\u2022 " + n));
if (Dlg.Ask(this, L.F("What's new in Nyxo Code {0}", info.Version.ToString(3)), text, null, L.T("Update"), L.T("Close"), false) != null) DoUpdate();
}
void DoUpdate()
{
var info = update;
if (info == null || updating) return;
if (login.Visible && !login.Connect.Enabled)
{
ShowBar(() => bar.Set(L.T("Wait for the sign-in to finish, then click Update."), T.Text, "\uE896", true, L.T("Update")));
return;
}
running.RemoveAll(Exited);
if (running.Count > 0)
{
var n = running.Count;
if (Dlg.Ask(this, L.T("Update Nyxo Code?"), L.F(n == 1 ? "Claude Code is open in {0} window. It keeps working, but its usage is not shown here until you sign in again after the update." : "Claude Code is open in {0} windows. It keeps working, but its usage is not shown here until you sign in again after the update.", n), null, L.T("Update"), L.T("Cancel"), false) == null) return;
}
if (!Updater.CanReplace())
{
if (Dlg.Ask(this, L.T("Update Nyxo Code"), L.T("Nyxo Code can't update itself in this folder:") + "\n" + Path.GetDirectoryName(Updater.ExePath) +
"\n\n" + L.T("Your browser will download the new version. Close Nyxo Code, then use the new file instead of this one."), null, L.T("Download"), L.T("Cancel"), false) != null)
Updater.OpenUrl(info.Url);
return;
}
updating = true;
bar.UpdateBtn.Enabled = bar.Later.Enabled = false;
string v = info.Version.ToString(3);
ShowBar(() => bar.Set(L.F("Downloading Nyxo Code {0}...", v), T.Text, "\uE896", true, L.T("Updating...")));
Task.Run(() =>
{
try
{
var file = Updater.Download(info, p => Post(() => { if (updating) ShowBar(() => bar.Set(L.F("Downloading Nyxo Code {0}...", v) + " " + p + "%", T.Text, "\uE896", true, L.T("Updating..."))); }), CancellationToken.None);
Post(() => FinishUpdate(file, info));
}
catch (Exception ex) { Post(() => UpdateFailed(ex)); }
});
}
void FinishUpdate(string file, Updater.Info info)
{
// An open purchase window (maybe with an unpaid order) is never closed by the restart: the update waits for it.
if (buy != null)
{
pendingFile = file; pendingInfo = info;
ShowBar(() => bar.Set(L.F("Nyxo Code {0} is ready. It installs when you close the purchase window.", info.Version.ToString(3)), T.Text, "\uE896", true, L.T("Updating...")));
return;
}
ShowBar(() => bar.Set(L.F("Installing Nyxo Code {0}...", info.Version.ToString(3)), T.Text, "\uE896", true, L.T("Updating...")));
bar.Refresh();
try { Updater.Install(file, Updater.Current); }
catch (Updater.RestartFailed)
{
updating = false; update = null;
ShowBar(() => bar.Set(L.F("Nyxo Code was updated to {0}. Close it and start it again to use the new version.", info.Version.ToString(3)), T.Warn, "\uE7BA", false, null));
return;
}
catch (Exception ex) { UpdateFailed(ex, true); return; }
exiting = true; Close(); // the new version is starting; this one signs out and exits
}
void UpdateFailed(Exception ex, bool installing = false)
{
updating = false;
try { File.Delete(Updater.ExePath + ".download"); } catch { }
bar.UpdateBtn.Enabled = bar.Later.Enabled = true;
var missing = ex as Updater.ExeMissing;
if (missing != null)
{
// The app's file could not be put back: say plainly where the previous version is, instead of "nothing changed".
update = null;
ShowBar(() => bar.Set(L.T("Update failed, and Nyxo Code's file could not be put back."), T.Danger, "\uE7BA", false, null));
Dlg.Ask(this, L.T("Update failed"), L.T("The update failed, and Nyxo Code's file could not be put back. The previous version is saved as:") + "\n" + missing.Backup +
"\n\n" + L.F("Rename that file to \"{0}\" to use Nyxo Code again.", Path.GetFileName(Updater.ExePath)), null, L.T("OK"), null, true);
return;
}
string why = ex is InvalidDataException ? ex.Message
: ex is UnauthorizedAccessException ? "Nyxo Code can't replace its file in this folder."
: installing && ex is IOException ? "The file is in use. Wait a moment and try again."
: ex is WebException || ex is IOException ? "The download didn't finish. Check your connection."
: "Something went wrong.";
ShowBar(() => bar.Set(L.F("Update failed: {0} Nothing was changed.", L.T(why)), T.Danger, "\uE7BA", true, L.T("Try again")));
}
void Post(Action a) { TryPost(a); }
bool TryPost(Action a)
{
try { if (IsHandleCreated && !IsDisposed) { BeginInvoke(a); return true; } } catch { }
return false;
}
// ---------- signing in ----------
void ShowSaved()
{
login.SetSaved(Nyxo.LoadSaved().Select(s => new RowList.Item
{
Title = s.Name, Sub = s.LastUsed > 0 ? L.F("Last used {0}", Nyxo.Rel(s.LastUsed)) : L.T("Not used yet"), Glyph = "\uE8D7", Tag = s
}).ToList());
}
void ShowLogin(string msg, Color c)
{
if (history != null) history.Close();
if (buy != null) buy.Close();
passcode = label = null;
StopStream();
EndSession();
ShowSaved();
login.SetBusy(false);
login.SetStatus(msg, c);
home.Visible = false; login.Visible = true;
AcceptButton = login.Connect;
if (login.SavedList.Items.Count == 0) login.Pass.Box.Focus(); else login.Connect.Focus();
}
async void DoLogin(Nyxo.Saved entry)
{
string pass;
if (entry != null)
{
pass = Nyxo.Unprotect(entry.Secret);
if (pass == null) { login.SetStatus(L.F("\"{0}\" cannot be read by this Windows account. Remove it and add it again.", entry.Name), T.Warn); return; }
}
else
{
pass = login.Pass.Text.Trim();
if (pass == "")
{
// Enter with an empty box and saved passcodes: use the most recent one.
if (login.SavedList.Items.Count > 0) { DoLogin((Nyxo.Saved)login.SavedList.Items[0].Tag); return; }
login.SetStatus(L.T("Enter your passcode."), T.Warn); login.Pass.Box.Focus(); return;
}
}
login.SetBusy(true);
login.SetStatus(entry != null ? L.F("Connecting as \"{0}\"...", entry.Name) : L.T("Connecting..."), T.Dim);
var r = await Task.Run(() => Nyxo.TryLogin(pass));
if (IsDisposed || Disposing)
{
// The window closed meanwhile: end the session that was just created.
string orphan = Nyxo.Str(r.Body, "token");
if (!string.IsNullOrEmpty(orphan)) await Task.Run(() => Nyxo.Logout(orphan));
return;
}
login.SetBusy(false);
if (r.Status == 200 && Nyxo.Str(r.Body, "token") != null)
{
bool remember = login.Remember.Checked;
try
{
Nyxo.EditSaved(saved =>
{
var same = entry != null ? saved.FirstOrDefault(s => s.Secret == entry.Secret) : saved.FirstOrDefault(s => Nyxo.Unprotect(s.Secret) == pass);
if (same != null) { same.LastUsed = Nyxo.NowMs(); return true; }
if (entry != null || !remember) return false;
var name = Nyxo.Str(r.Body, "label");
if (string.IsNullOrWhiteSpace(name)) name = "Nyxo";
saved.Add(new Nyxo.Saved { Name = Nyxo.UniqueName(saved, name.Trim(), null), Secret = Nyxo.Protect(pass), LastUsed = Nyxo.NowMs() });
return true;
});
}
catch { }
login.Pass.Text = "";
passcode = pass; label = Nyxo.Str(r.Body, "label");
EnterSession(r.Body);
return;
}
// The server answers in English; known messages are shown in the chosen language.
string said = r.Message != null ? L.Server(r.Message) : null;
if (r.Status == 401)
{
if (entry != null)
{
login.SetStatus(said ?? L.F("\"{0}\" was not accepted.", entry.Name), T.Warn);
if (Dlg.Ask(this, L.T("Passcode not accepted"), (said ?? L.F("\"{0}\" was not accepted. It may have expired, been revoked or changed.", entry.Name)) + "\n\n" + L.T("Remove it from this PC?"), null, L.T("Remove"), L.T("Keep"), true) != null)
{
if (!Save(saved => saved.RemoveAll(s => s.Secret == entry.Secret) > 0)) return;
ShowSaved();
login.SetStatus(L.F("Removed \"{0}\".", entry.Name), T.Dim);
}
}
else { login.SetStatus(said ?? L.T("Passcode not accepted."), T.Danger); login.Pass.Box.Focus(); login.Pass.Box.SelectAll(); }
}
else if (r.Status == 403)
{
login.SetStatus(said ?? L.T("This passcode is paused by the admin. Try again once it is resumed."), T.Warn);
// A usage-policy pause or revocation is spelled out in a popup too.
if (r.Message != null && r.Message.IndexOf("usage-policy", StringComparison.OrdinalIgnoreCase) >= 0)
{
Flash(true);
Dlg.Ask(this, r.Message.Contains("revoked") ? L.T("Passcode revoked") : L.T("Passcode paused"), said, null, L.T("OK"), null, true);
}
}
else if (r.Status == 429) login.SetStatus(L.T("Too many attempts. Try again in 30 minutes."), T.Danger);
else if (r.Status >= 500) login.SetStatus(L.T("Nyxo is temporarily unavailable. Please try again shortly."), T.Warn);
else if (r.Status >= 200) login.SetStatus(L.T("Nyxo returned an unexpected response. Please try again shortly."), T.Warn);
else login.SetStatus(L.T("Could not reach the Nyxo server. Check your internet connection."), T.Danger);
}
void SavedMenuClick(object sender, EventArgs e)
{
var it = menuItem; if (it == null) return;
var entry = (Nyxo.Saved)it.Tag;
var text = (string)((ToolStripMenuItem)sender).Tag;
if (text == "Connect") DoLogin(entry);
else if (text.StartsWith("Remove")) RemoveSaved(entry);
else
{
var name = Dlg.Ask(this, L.T("Rename passcode"), L.T("Name for this saved passcode:"), entry.Name, L.T("Save"), L.T("Cancel"), false);
if (string.IsNullOrWhiteSpace(name)) return;
if (!Save(saved =>
{
var me = saved.FirstOrDefault(s => s.Secret == entry.Secret);
if (me == null) return false;
me.Name = Nyxo.UniqueName(saved, name, me);
return true;
})) return;
ShowSaved();
}
}
void RemoveSaved(Nyxo.Saved entry)
{
if (Dlg.Ask(this, L.T("Remove saved passcode"), L.F("Remove \"{0}\" from this PC?", entry.Name) + "\n\n" + L.T("The passcode keeps working; you will just need to type it again."), null, L.T("Remove"), L.T("Cancel"), true) == null) return;
if (!Save(saved => saved.RemoveAll(s => s.Secret == entry.Secret) > 0)) return;
ShowSaved();
login.SetStatus(L.F("Removed \"{0}\".", entry.Name), T.Dim);
}
// Changes the saved passcode list; on a locked or unwritable file the user is told instead of a crash dialog.
bool Save(Func<List<Nyxo.Saved>, bool> edit)
{
try { Nyxo.EditSaved(edit); return true; }
catch { login.SetStatus(L.T("Couldn't save the change on this PC. Try again."), T.Warn); return false; }
}
// ---------- signed in ----------
void EnterSession(Dictionary<string, object> body)
{
token = Nyxo.Str(body, "token");
alertIn = alertOut = alertDay = alertTotal = 0;
lastStrikes = 0; lastPaused = lastRevoked = false; refreshedFor = 0;
home.SetUser(Nyxo.Str(body, "label"));
home.Usage.Show(body);
home.SetPolicy(body);
home.SetBusy(false);
ShowRecent(true);
login.Visible = false; home.Visible = true;
AcceptButton = home.Open;
UpdateClaude();
home.Open.Focus();
StartStream(token);
int my = gen;
Post(() => { if (my == gen) CheckPolicy(body); }); // strikes already counted show at sign-in, once the screen is up
}
void ShowRecent(bool fillFolder)
{
var list = Nyxo.LoadRecent();
home.SetRecent(list.Select(f => new RowList.Item { Title = Path.GetFileName(f.TrimEnd('\\')) == "" ? f : Path.GetFileName(f.TrimEnd('\\')), Sub = f, Glyph = "\uE8B7", Tag = f }).ToList());
if (fillFolder && home.Folder.Text.Trim() == "" && list.Count > 0) home.Folder.Text = list[0];
int i = home.Recent.Items.FindIndex(it => ((string)it.Tag).Equals(home.Folder.Text.Trim(), StringComparison.OrdinalIgnoreCase));
home.Recent.Selected = i; home.Recent.Invalidate();
}
void UpdateClaude()
{
if (!home.Visible) return;
bool missing = claudeSearched && claude == null;
home.ClaudeMissing = missing;
home.Open.Text = missing ? L.T("Locate Claude Code...") : L.T("Open Claude Code");
home.Open.Glyph = missing ? "\uE721" : "\uE756";
home.Install.Visible = missing;
if (missing) home.SetStatus(L.T("Claude Code was not found on this PC."), T.Warn);
else UpdateRunning(null, T.Dim);
}
void UpdateRunning(string msg, Color c)
{
running.RemoveAll(p => { try { return p.HasExited; } catch { return true; } });
if (msg != null) { home.SetStatus(msg, c); return; }
int n = running.Count;
home.SetStatus(n == 0 ? "" : L.F(n == 1 ? "Claude Code is running in {0} window." : "Claude Code is running in {0} windows.", n), T.Ok);
}
// Folders and Claude Code may sit on a slow or unreachable network share, so they are checked in the background.
// Results are dropped if the window closed or the session changed meanwhile (gen moves on both).
bool Stale(int my) { return IsDisposed || Disposing || my != gen; }
static bool FolderExists(string f) { try { return Directory.Exists(f); } catch { return false; } }
bool InRecent(string f) { return home.Recent.Items.Any(it => ((string)it.Tag).Equals(f, StringComparison.OrdinalIgnoreCase)); }
void DropRecent(string f)
{
Nyxo.SaveRecent(Nyxo.LoadRecent().Where(x => !x.Equals(f, StringComparison.OrdinalIgnoreCase)));
ShowRecent(false);
}
async void CheckRecent(string f)
{
int my = gen;
bool ok = await Task.Run(() => FolderExists(f));
if (ok || Stale(my) || !InRecent(f)) return;
DropRecent(f);
home.SetStatus(L.T("That folder no longer exists. Removed it from Recent."), T.Warn);
}
async Task<string> PickFolder(int my)
{
var cur = home.Folder.Text.Trim().Trim('"');
var desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
var start = cur == "" ? desktop : await Task.Run(() => FolderExists(cur) ? cur : desktop);
if (Stale(my)) return null;
return FolderPicker.Pick(Handle, L.T("Choose the folder to open Claude Code in"), start);
}
async void BrowseFolder()
{
if (token == null || home.Busy) return;
int my = gen;
home.SetBusy(true);
string f;
try { f = await PickFolder(my); }
finally { if (!Stale(my)) home.SetBusy(false); }
if (f != null && !Stale(my)) { home.Folder.Text = f; home.Open.Focus(); }
}
void LocateClaude()
{
using (var d = new OpenFileDialog { Title = L.T("Find Claude Code (claude.exe)"), Filter = "Claude Code|claude.exe;claude.cmd;claude.bat|" + L.T("Programs") + "|*.exe;*.cmd;*.bat", CheckFileExists = true })
{
if (d.ShowDialog(this) != DialogResult.OK) return;
var c = Nyxo.ClaudeAt(d.FileName);
if (c == null) { home.SetStatus(L.T("That is not Claude Code. Choose claude.exe."), T.Danger); return; }
Nyxo.RememberClaude(c);
claude = c;
UpdateClaude();
home.SetStatus(L.T("Found Claude Code. Saved for next time."), T.Ok);
}
}
async void OpenClaude()
{
if (token == null || home.Busy) return;
int my = gen;
home.SetBusy(true);
try { await OpenClaude(my); }
finally { if (!Stale(my)) home.SetBusy(false); }
}
async Task OpenClaude(int my)
{
string known = claudeSearched ? claude : null;
string c = await Task.Run(() => known != null && File.Exists(known) ? known : Nyxo.FindClaude());
if (Stale(my)) return;
claude = c; claudeSearched = true;
UpdateClaude();
if (c == null) { LocateClaude(); return; }
// Claude Code can't work while the passcode is paused or revoked for usage-policy reasons: say so here instead.
var pd = home.Usage.Data;
double pu = Policy.PausedUntil(pd);
if (Policy.Revoked(pd)) { home.SetStatus(L.T("This passcode was revoked for usage-policy violations."), T.Danger); return; }
if (pu > 0) { home.SetStatus(Policy.PausedText(pu) + ".", T.Danger); return; }
var folder = home.Folder.Text.Trim().Trim('"');
if (folder == "")
{
folder = await PickFolder(my);
if (folder == null || Stale(my)) return;
home.Folder.Text = folder;
}
// "\\?\C:\..." is an ordinary local folder; .NET Framework paths do not accept that prefix.
if (folder.StartsWith(@"\\?\", StringComparison.Ordinal) && folder.Length >= 7 && char.IsLetter(folder[4]) && folder[5] == ':' && folder[6] == '\\')
folder = folder.Substring(4);
bool exists = await Task.Run(() => FolderExists(folder));
if (Stale(my)) return;
if (!exists)
{
if (InRecent(folder)) { DropRecent(folder); home.SetStatus(L.T("That folder no longer exists. Removed it from Recent."), T.Danger); }
else home.SetStatus(L.T("That folder does not exist."), T.Danger);
home.Folder.Box.Focus();
return;
}
folder = Path.GetFullPath(folder);
if (folder.StartsWith(@"\\") && !c.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{ home.SetStatus(L.T("Network folders need claude.exe. Map the share to a drive letter first."), T.Danger); return; }
try
{
string settingsFile;
var p = Nyxo.StartClaude(c, folder, token, out settingsFile);
var started = DateTime.Now;
string tok = token;
// Track it and subscribe before enabling events, so a very fast exit is never missed.
running.Add(p);
lock (procToken) procToken[p] = tok;
p.Exited += (s, e) =>
{
Nyxo.DeleteLaunchFile(settingsFile);
// Once the window is gone (the app is quitting or updating) its session is ended from here instead.
if (!closing && !exiting && TryPost(() => ClaudeExited(p, started))) return;
ExitedWithoutUi(p, tok);
};
p.EnableRaisingEvents = true;
Nyxo.AddRecent(folder);
home.Folder.Text = folder;
ShowRecent(false);
UpdateRunning(null, T.Ok);
}
catch (Exception ex) { home.SetStatus(L.T("Could not start Claude Code:") + " " + ex.Message, T.Danger); }
}
void ClaudeExited(Process p, DateTime started)
{
int code = 0;
try { code = p.ExitCode; } catch { }
running.Remove(p);
string used;
lock (procToken)
{
if (procToken.TryGetValue(p, out used))
{
procToken.Remove(p);
// A session left open for this window after signing out ends with its last window.
if (used != token && !procToken.Values.Contains(used)) Nyxo.Background(() => Nyxo.Logout(used));
}
}
p.Dispose();
if (!home.Visible) return;
if (code != 0 && (DateTime.Now - started).TotalSeconds < 5) UpdateRunning(L.F("Claude Code closed right away (exit code {0}).", code), T.Danger);
else UpdateRunning(null, T.Dim);
}
// Runs on the thread that saw the exit: ends the window's session unless another open window still uses it.
void ExitedWithoutUi(Process p, string tok)
{
bool last;
lock (procToken)
{
if (!procToken.Remove(p)) return; // already handled
last = !procToken.Values.Contains(tok);
}
if (last && tok != null && tok != token) Nyxo.Background(() => Nyxo.Logout(tok));
}
// ---------- live usage: the server pushes new totals as they change, and a keep-alive every 3 s ----------
void StartStream(string tok)
{
int my = ++gen;
new Thread(() => StreamLoop(tok, my)) { IsBackground = true, Name = "usage" }.Start();
}
void StopStream()
{
gen++;
var r = current;
if (r != null) try { r.Abort(); } catch { }
}
void StreamLoop(string tok, int my)
{
var json = new JavaScriptSerializer();
Dictionary<string, object> last = null;
int wait = 2000; // doubles after each failed attempt, up to a minute; back to 2 s once usage arrives again
while (my == gen)
{
bool ended = false, gotData = false;
string refused = null; // the server's reason when it refuses the session (paused or revoked)
HttpWebResponse resp = null;
try
{
var req = (HttpWebRequest)WebRequest.Create(Nyxo.Server + "/usage/stream");
req.AllowAutoRedirect = false;
req.Headers["x-api-key"] = tok;
req.Accept = "text/event-stream";
req.Timeout = 15000; req.ReadWriteTimeout = 15000; // silence for 15 s means a dead connection
current = req;
if (my != gen) return;
resp = (HttpWebResponse)req.GetResponse();
// Anything but an event stream (a captive portal page, a proxy error) counts as a failed attempt.
if ((resp.ContentType ?? "").StartsWith("text/event-stream", StringComparison.OrdinalIgnoreCase))
using (var rd = new StreamReader(resp.GetResponseStream(), Encoding.UTF8))
{
string line;
while (my == gen && (line = rd.ReadLine()) != null)
{
if (line.StartsWith("event: end")) { ended = true; break; }
if (line.StartsWith("data: ")) { var d = Nyxo.Obj(json.DeserializeObject(line.Substring(6))); if (d != null) { last = d; gotData = true; } }
if (line.Length == 0 && last != null) { var snap = last; Post(() => { if (my == gen) OnUsage(snap); }); }
}
}
}
catch (WebException e)
{
var r = e.Response as HttpWebResponse;
if (r != null)
{
int code = (int)r.StatusCode;
if (code == 401) ended = true;
else if (code == 403)
{
// Paused or revoked: retrying can't help, so this is final too.
try { refused = Nyxo.Str(Nyxo.Obj(Nyxo.Get(Nyxo.ReadJson(r), "error")), "message"); } catch { }
refused = refused != null ? L.Server(refused) : L.T("The server no longer accepts this session. The passcode may be paused or revoked.");
}
try { r.Close(); } catch { }
}
}
catch { }
finally { if (resp != null) try { resp.Close(); } catch { } }
if (my != gen) return;
if (ended) { Post(() => { if (my == gen) ShowLogin(L.T("Your session ended. Connect again to continue."), T.Warn); }); return; }
if (refused != null) { Post(() => { if (my == gen) ShowLogin(refused, T.Warn); }); return; }
if (gotData) wait = 2000;
Post(() => { if (my == gen) home.Usage.SetNote(L.T("Reconnecting...")); });
for (int slept = 0; slept < wait && my == gen; slept += 500) Thread.Sleep(500);
wait = Math.Min(wait * 2, 60000);
}
}
void OnUsage(Dictionary<string, object> d)
{
home.Usage.Show(d);
home.SetPolicy(d);
if (history != null && Nyxo.Get(d, "usedTodayUsd") != null) history.SetToday(Nyxo.Num(d, "usedTodayUsd"));
Alert(ref alertIn, Pct(d, "usedInUsd", "budgetInUsd"));
Alert(ref alertOut, Pct(d, "usedOutUsd", "budgetOutUsd"));
Alert(ref alertDay, Pct(d, "usedTodayUsd", "dailyUsd"));
Alert(ref alertTotal, Pct(d, "usedUsd", "totalUsd"));
CheckPolicy(d);
}
// A popup (with a sound and a taskbar flash) when a request was refused by the model's safety rules and the
// passcode got a strike, was paused or was revoked. Older servers send no policy status: nothing is shown then.
void CheckPolicy(Dictionary<string, object> d)
{
if (!Policy.Known(d)) return;
bool revoked = Policy.Revoked(d);
double until = Policy.PausedUntil(d);
bool paused = until > 0;
int n = Policy.Strikes(d);
string title = null, msg = null;
if (revoked && !lastRevoked) { title = L.T("Passcode revoked"); msg = Policy.RevokedText; }
else if (paused && !lastPaused && !revoked)
{
title = L.T("Passcode paused for 1 hour");
msg = L.T("Another request was refused by the model's safety rules within 24 hours, so this passcode is paused.") + "\n\n" + Policy.PausedText(until) +
". " + L.T("It resumes on its own.") + "\n\n" + L.T("One more refusal within 24 hours revokes this passcode permanently, with no refund.");
}
else if (n > lastStrikes && !paused && !revoked)
{
title = L.T("Usage-policy warning");
msg = L.T("A request was refused by the model's safety rules.") + "\n\n" + Policy.StrikesText(d) + ".\n\n" +
(n >= Policy.Max(d) - 1 ? L.T("One more refusal within 24 hours revokes this passcode permanently, with no refund.")
: L.T("A 2nd refusal within 24 hours pauses this passcode for 1 hour; a 3rd revokes it with no refund."));
}
lastRevoked = revoked; lastPaused = paused; lastStrikes = n;
if (title == null) return;
Flash(revoked || paused);
popups.Enqueue(new[] { title, msg, revoked || paused ? "1" : "" });
if (popupOpen) return;
popupOpen = true;
try
{
while (popups.Count > 0 && !IsDisposed && home.Visible)
{
var p = popups.Dequeue();
Dlg.Ask(this, p[0], p[1], null, L.T("OK"), null, p[2] == "1");
}
popups.Clear();
}
finally { popupOpen = false; }
}
// The server doesn't push when a policy pause ends or a strike leaves the 24-hour window: fetch the status then.
void RefreshAtPolicyEdge()
{
var d = home.Usage.Data;
var p = Nyxo.Obj(Nyxo.Get(d, "policy"));
if (token == null || p == null) return;
double now = Nyxo.NowMs(), edge = 0;
foreach (var k in new[] { "pausedUntil", "windowEndsAt" })
{
double t = Nyxo.Num(p, k);
if (t > 0 && t <= now - 2000 && t != refreshedFor && (edge == 0 || t < edge)) edge = t;
}
if (edge == 0) return;
refreshedFor = edge;
string tok = token; int my = gen;
Task.Run(() =>
{
var u = Nyxo.Usage(tok);
if (u != null) Post(() => { if (my == gen && tok == token) OnUsage(u); });
});
}
void Flash(bool severe)
{
try { if (severe) SystemSounds.Hand.Play(); else SystemSounds.Exclamation.Play(); } catch { }
if (ContainsFocus || !IsHandleCreated) return;
var f = new FLASHWINFO { hwnd = Handle, dwFlags = 2 | 12, uCount = 5 };
f.cbSize = (uint)Marshal.SizeOf(f);
FlashWindowEx(ref f);
}
static double Pct(Dictionary<string, object> d, string used, string budget)
{
double b = Nyxo.Num(d, budget);
return b <= 0 ? -1 : Nyxo.Num(d, used) / b * 100;
}
// A sound and a taskbar flash once when a budget passes 80% and again when it is used up. A reset re-arms it.
void Alert(ref int alerted, double pct)
{
int level = pct >= 100 ? 2 : pct >= 80 ? 1 : 0;
if (level > alerted) Flash(level == 2);
alerted = level;
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
// Second pass, once the logout below has finished or timed out.
if (closing) { base.OnFormClosing(e); return; }
// A sign-in in progress would leave its new session open on the server.
if (e.CloseReason == CloseReason.UserClosing && !exiting && login.Visible && !login.Connect.Enabled)
{
e.Cancel = true;
login.SetStatus(L.T("Please wait for the connection attempt to finish."), T.Dim);
return;
}
running.RemoveAll(p => { try { return p.HasExited; } catch { return true; } });
if (running.Count > 0 && !updating && e.CloseReason != CloseReason.WindowsShutDown && e.CloseReason != CloseReason.TaskManagerClosing)
{
var n = running.Count;
if (Dlg.Ask(this, L.T("Quit Nyxo Code?"), L.F(n == 1 ? "Claude Code is still open in {0} window. It keeps working after you quit, but its usage will not be shown here." : "Claude Code is still open in {0} windows. It keeps working after you quit, but its usage will not be shown here.", n), null, L.T("Quit"), L.T("Cancel"), false) == null)
{ e.Cancel = true; return; }
}
StopStream();
var logout = EndSession();
// The window goes away at once; the app itself exits when the logout is done, or after 2 s at most.
if (logout != null && !logout.IsCompleted && e.CloseReason != CloseReason.WindowsShutDown)
{
e.Cancel = true;
closing = true;
Hide();
CloseAfter(logout);
return;
}
// Windows is shutting down and must not be held up: give the logout a short, bounded chance.
if (logout != null) try { logout.Wait(2000); } catch { }
base.OnFormClosing(e);
}
async void CloseAfter(Task logout)
{
await Task.WhenAny(logout, Task.Delay(2000));
if (!IsDisposed) Close();
}
// Sessions only end after 24 hours unused, so end them here. Claude Code windows opened from the app share the
// token; while any is still open the session stays, and the server ends it once it sits unused.
// Returns the logout running in the background, or null when there is nothing to end.
static bool Exited(Process p) { try { return p.HasExited; } catch { return true; } }
Task EndSession()
{
var tok = token;
token = null;
if (tok == null) return null;
lock (procToken) if (procToken.Any(kv => kv.Value == tok && !Exited(kv.Key))) return null;
return Task.Run(() => Nyxo.Logout(tok));
}
}
Update.csالتحقق من التحديثات، والتنزيل الموثَّق، وشريط التحديث.291 سطر
// Updates: the app asks nyxo.space for the newest Nyxo Code and, when there is one, shows a bar at the top of the
// window. Nothing is downloaded or installed until the user clicks Update. The download must match the size, SHA-256
// and version the server announced; then the running .exe is renamed out of the way, the new one takes its name,
// and the app restarts. A folder the app can't write to (e.g. Program Files) falls back to a browser download.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
static class Updater
{
const string LatestUrl = "https://nyxo.space/api/code/latest";
const string Site = "https://nyxo.space/";
public static readonly Version Current = Norm(new Version(Nyxo.Version));
static Version Norm(Version v) { return new Version(v.Major, v.Minor, Math.Max(0, v.Build)); }
// Notes: what's new in this version, by language ("en", "fr", "ar"); null when the server has none.
public class Info { public Version Version; public string Url, Sha; public long Size; public Dictionary<string, List<string>> Notes; }
// The newest version on the server when it is newer than this app; null when up to date or on any error.
public static Info Check()
{
try
{
var req = (HttpWebRequest)WebRequest.Create(LatestUrl);
req.Timeout = 15000; req.ReadWriteTimeout = 15000; req.AllowAutoRedirect = false;
req.UserAgent = "NyxoCode/" + Nyxo.Version;
req.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore);
Dictionary<string, object> d;
using (var resp = (HttpWebResponse)req.GetResponse()) d = Nyxo.ReadJson(resp);
string ver = Nyxo.Str(d, "version"), url = Nyxo.Str(d, "url"), sha = Nyxo.Str(d, "sha256");
double size = Nyxo.Num(d, "size");
if (ver == null || !Regex.IsMatch(ver, @"^\d{1,4}\.\d{1,4}\.\d{1,4}$")) return null;
if (sha == null || !Regex.IsMatch(sha, "^[0-9A-Fa-f]{64}$")) return null;
// Only ever download from, or link to, nyxo.space over HTTPS.
if (url == null || !url.StartsWith(Site + "media/", StringComparison.Ordinal)) return null;
if (size < 16 * 1024 || size > 64 * 1024 * 1024) return null;
var v = new Version(ver);
if (v <= Current) return null;
return new Info { Version = v, Url = url, Sha = sha.ToUpperInvariant(), Size = (long)size, Notes = ReadNotes(Nyxo.Get(d, "notes")) };
}
catch (WebException e) { if (e.Response != null) e.Response.Close(); return null; }
catch { return null; }
}
static Dictionary<string, List<string>> ReadNotes(object raw)
{
var o = Nyxo.Obj(raw);
if (o == null) return null;
var notes = new Dictionary<string, List<string>>();
foreach (var lang in new[] { "en", "fr", "ar" })
{
var list = Nyxo.Get(o, lang) as System.Collections.IEnumerable;
if (list == null || list is string) continue;
var lines = new List<string>();
foreach (var x in list)
{
var s = x as string;
if (s == null) continue;
s = Regex.Replace(s, @"[\p{C}]+", " ").Trim();
if (s.Length > 200) s = s.Substring(0, 200);
if (s.Length > 0 && lines.Count < 8) lines.Add(s);
}
if (lines.Count > 0) notes[lang] = lines;
}
return notes.ContainsKey("en") ? notes : null;
}
// The notes in the app's language, or in English when they are not translated.
public static List<string> NotesFor(Info info)
{
if (info == null || info.Notes == null) return null;
List<string> l;
return info.Notes.TryGetValue(L.Lang, out l) ? l : info.Notes["en"];
}
public static string ExePath { get { return Application.ExecutablePath; } }
static string Partial { get { return ExePath + ".download"; } }
// True when the app may rename itself and write next to its .exe.
public static bool CanReplace()
{
try
{
var probe = ExePath + ".write-test";
using (new FileStream(probe, FileMode.Create, FileAccess.Write, FileShare.None, 1, FileOptions.DeleteOnClose)) { }
return true;
}
catch { return false; }
}
// Downloads the update next to the .exe and checks it. Returns the checked file; throws with a short reason otherwise.
public static string Download(Info info, Action<int> progress, CancellationToken cancel)
{
string tmp = Partial;
try { File.Delete(tmp); } catch { }
var req = (HttpWebRequest)WebRequest.Create(info.Url);
req.Timeout = 30000; req.ReadWriteTimeout = 30000; req.AllowAutoRedirect = false;
req.UserAgent = "NyxoCode/" + Nyxo.Version;
try
{
using (var resp = (HttpWebResponse)req.GetResponse())
using (var src = resp.GetResponseStream())
using (var dst = new FileStream(tmp, FileMode.Create, FileAccess.Write, FileShare.None))
using (var sha = SHA256.Create())
{
var buf = new byte[64 * 1024];
long total = 0; int n, last = -1;
while ((n = src.Read(buf, 0, buf.Length)) > 0)
{
cancel.ThrowIfCancellationRequested();
total += n;
if (total > info.Size) throw new InvalidDataException("The download is larger than announced.");
sha.TransformBlock(buf, 0, n, null, 0);
dst.Write(buf, 0, n);
int pct = (int)(total * 100 / info.Size);
if (pct != last) { last = pct; progress(pct); }
}
sha.TransformFinalBlock(buf, 0, 0);
if (total != info.Size) throw new InvalidDataException("The download is incomplete.");
if (BitConverter.ToString(sha.Hash).Replace("-", "") != info.Sha) throw new InvalidDataException("The download failed its security check (SHA-256).");
}
var fv = FileVersionInfo.GetVersionInfo(tmp);
if (Norm(new Version(fv.FileMajorPart, fv.FileMinorPart, fv.FileBuildPart)) != info.Version)
throw new InvalidDataException("The download is not the announced version.");
return tmp;
}
catch (Exception e)
{
var we = e as WebException;
if (we != null && we.Response != null) we.Response.Close();
try { File.Delete(tmp); } catch { }
throw;
}
}
// Puts the checked file in place of the running .exe (Windows allows renaming a running .exe) and starts it.
// File.Replace swaps them in one step (the running .exe becomes the .old backup); if it can't, two renames do it.
// After any failure the .exe must still be there: it is put back from the backup, and if even that fails,
// ExeMissing says where the previous version was left.
public static void Install(string file, Version from)
{
string exe = ExePath, old = exe + ".old";
try { File.Delete(old); } catch { old = exe + "." + DateTime.UtcNow.Ticks.ToString(Nyxo.Inv) + ".old"; }
try
{
try
{
File.Replace(file, exe, old, true);
// File.Replace also copies the old file's "downloaded from the internet" mark onto the new one; the
// new file was checked when it was downloaded and never had that mark, so it is removed again (as with a plain rename).
try { DeleteFileW(exe + ":Zone.Identifier"); } catch { }
}
catch
{
// Fall back to the renames only when nothing was moved yet.
if (!File.Exists(exe) || !File.Exists(file) || File.Exists(old)) throw;
File.Move(exe, old);
File.Move(file, exe);
}
}
catch (Exception e)
{
if (!File.Exists(exe)) try { if (File.Exists(old)) File.Move(old, exe); } catch { }
if (!File.Exists(exe)) throw new ExeMissing(old, e);
throw;
}
RefreshIcon(exe);
try { Process.Start(new ProcessStartInfo(exe, "--updated-from " + from.ToString(3)) { UseShellExecute = false, WorkingDirectory = Path.GetDirectoryName(exe) }); }
catch (Exception ex) { throw new RestartFailed(ex); }
}
[System.Runtime.InteropServices.DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
static extern bool DeleteFileW(string path);
// The update is installed, but the new .exe could not be started.
public class RestartFailed : Exception { public RestartFailed(Exception inner) : base(inner.Message, inner) { } }
// The update failed and the app's .exe could not be put back; the previous version is still at Backup.
public class ExeMissing : Exception
{
public readonly string Backup;
public ExeMissing(string backup, Exception inner) : base(inner.Message, inner) { Backup = backup; }
}
// Windows caches the icon of an .exe and of its shortcuts; tell Explorer the file changed so the new icon shows.
[System.Runtime.InteropServices.DllImport("shell32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
static extern void SHChangeNotify(int eventId, uint flags, string item1, IntPtr item2);
public static void RefreshIcon(string exe)
{
try
{
SHChangeNotify(0x00002000, 0x0005 | 0x1000, exe, IntPtr.Zero); // SHCNE_UPDATEITEM, SHCNF_PATHW | SHCNF_FLUSHNOWAIT
SHChangeNotify(0x08000000, 0x1000, null, IntPtr.Zero); // SHCNE_ASSOCCHANGED: refresh cached icons
}
catch { }
}
// Leftovers of an update: the previous .exe (still running for a moment after the restart) and unfinished downloads.
public static void CleanUp()
{
new Thread(() =>
{
for (int i = 0; i < 20; i++)
{
bool left = false;
try
{
string dir = Path.GetDirectoryName(ExePath), name = Path.GetFileName(ExePath);
foreach (var f in Directory.GetFiles(dir, name + ".*"))
{
if (!f.EndsWith(".old", StringComparison.OrdinalIgnoreCase) && !f.EndsWith(".download", StringComparison.OrdinalIgnoreCase)) continue;
try { File.Delete(f); } catch { left = true; }
}
}
catch { return; }
if (!left) return;
Thread.Sleep(1500);
}
}) { IsBackground = true, Name = "update-cleanup" }.Start();
}
// The version this app was updated from, when it was just restarted by an update.
public static string UpdatedFrom()
{
var a = Environment.GetCommandLineArgs();
for (int i = 1; i + 1 < a.Length; i++)
if (a[i] == "--updated-from" && Regex.IsMatch(a[i + 1], @"^\d{1,4}\.\d{1,4}\.\d{1,4}$")) return a[i + 1];
return null;
}
public static void OpenUrl(string url)
{
if (url == null || !url.StartsWith(Site, StringComparison.Ordinal)) return;
try { Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } catch { }
}
}
// The bar at the top of the window: "a new version is available" with Update / close, the download
// progress, an error, or "updated to ..." right after an update.
class UpdateBar : Painted
{
public readonly Btn UpdateBtn = new Btn("", Btn.Kind.Primary);
public readonly Btn Later = new Btn("", Btn.Kind.Ghost) { Glyph = "\uE711" };
public readonly Btn Notes = new Btn("", Btn.Kind.Secondary) { Visible = false };
string message = ""; Color color = T.Text; string glyph = "\uE896"; bool showUpd = true, showNotes;
public UpdateBar()
{
BackColor = T.Elev; Height = T.P(50); Visible = false; TabStop = false;
Controls.Add(UpdateBtn); Controls.Add(Notes); Controls.Add(Later);
AccessibleRole = AccessibleRole.StatusBar;
Relabel();
}
public void Relabel()
{
UpdateBtn.Text = L.T("Update"); Notes.Text = L.T("What's new"); Later.AccessibleName = L.T("Hide this notice");
PerformLayout();
}
public void Set(string text, Color c, string icon, bool showUpdate, string updateText)
{
message = text; color = c; glyph = icon; AccessibleName = text;
showUpd = showUpdate; UpdateBtn.Visible = showUpdate; if (updateText != null) UpdateBtn.Text = updateText;
showNotes = false; Notes.Visible = false;
PerformLayout(); Invalidate();
}
// Shows "What's new" next to Update; any later Set hides it again.
public void ShowNotes() { showNotes = true; Notes.Visible = true; PerformLayout(); Invalidate(); }
protected override void OnLayout(LayoutEventArgs e)
{
base.OnLayout(e);
int h = T.P(34), y = (Height - h) / 2, x = Width - T.P(12);
Later.SetBounds(x - T.P(30), y, T.P(30), h); x = Later.Left - T.P(8);
if (showUpd) { int w = Math.Max(T.P(96), T.W(UpdateBtn.Text, T.Bold) + T.P(32)); UpdateBtn.SetBounds(x - w, y, w, h); x = UpdateBtn.Left - T.P(8); }
if (showNotes) { int w = T.W(Notes.Text, T.Bold) + T.P(32); Notes.SetBounds(x - w, y, w, h); x = Notes.Left - T.P(8); }
T.Mirror(this, Later, UpdateBtn, Notes);
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics; g.Clear(BackColor); T.Begin(g, Width);
using (var pen = new Pen(T.Border)) g.DrawLine(pen, 0, Height - 1, Width, Height - 1);
using (var b = new SolidBrush(T.Accent)) g.FillRectangle(b, 0, 0, T.P(3), Height);
var first = showNotes ? Notes : showUpd ? UpdateBtn : Later;
int right = (T.Rtl ? Width - first.Right : first.Left) - T.P(12);
T.Txt(g, glyph, T.Glyph, new Rectangle(T.P(16), 0, T.P(24), Height), color == T.Text ? T.Accent : color, T.L);
T.Txt(g, message, T.Small, new Rectangle(T.P(48), 0, Math.Max(0, right - T.P(48)), Height), color, T.L);
}
}
Pages.csشاشتا تسجيل الدخول والصفحة الرئيسية.546 سطر
// The live usage card and the two screens (sign in, and the main screen after signing in).
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
// Usage-policy strikes, from the server's "policy" status: requests refused by the model's safety rules within 24 h.
static class Policy
{
public static string Rule { get { return L.T("Requests refused by the model's safety rules count as strikes: 2 in 24 h = 1-hour pause, 3 = passcode revoked, no refund."); } }
public static string RevokedText { get { return L.T("This passcode was revoked for repeated usage-policy violations. As stated in the terms, no refund is issued."); } }
static Dictionary<string, object> Of(Dictionary<string, object> d) { return Nyxo.Obj(Nyxo.Get(d, "policy")); }
public static bool Known(Dictionary<string, object> d) { return Of(d) != null; }
public static bool Revoked(Dictionary<string, object> d) { var v = Nyxo.Get(Of(d), "revoked"); return v is bool && (bool)v; }
// When the policy pause ends (ms), or 0 when not paused. The countdown runs here between updates.
public static double PausedUntil(Dictionary<string, object> d) { double u = Nyxo.Num(Of(d), "pausedUntil"); return u > Nyxo.NowMs() ? u : 0; }
public static int Strikes(Dictionary<string, object> d) { return Math.Max(0, (int)Nyxo.Num(Of(d), "strikes")); }
public static int Max(Dictionary<string, object> d) { int m = (int)Nyxo.Num(Of(d), "max"); return m > 0 ? m : 3; }
public static double WindowEndsAt(Dictionary<string, object> d) { return Nyxo.Num(Of(d), "windowEndsAt"); }
public static string Span(double ms)
{
return L.Span((int)Math.Ceiling(ms / 60000));
}
public static string PausedText(double until) { return L.F("Paused until {0} ({1} left)", Nyxo.When(until).ToString("HH:mm", Nyxo.Inv), Span(until - Nyxo.NowMs())); }
public static string StrikesText(Dictionary<string, object> d)
{
int n = Strikes(d);
string s = L.F("Policy strikes: {0} of {1}", n.ToString(Nyxo.Inv), Max(d).ToString(Nyxo.Inv));
double end = WindowEndsAt(d);
return n > 0 && end > 0 ? s + " " + L.F("(resets in {0})", Span(end - Nyxo.NowMs())) : s;
}
// One short line for the current state, and its colour.
public static string Status(Dictionary<string, object> d, out Color c)
{
double until = PausedUntil(d);
if (Revoked(d)) { c = T.Danger; return L.T("Passcode revoked (usage-policy violations)"); }
if (until > 0) { c = T.Danger; return PausedText(until); }
c = Strikes(d) > 0 ? T.Warn : T.Dim;
return StrikesText(d);
}
// What happens on the next refusal, given the strikes counted now.
public static string Next(Dictionary<string, object> d)
{
return Strikes(d) >= Max(d) - 1 ? L.T("One more refusal in 24 h = revoked, no refund.") : L.T("A 2nd refusal in 24 h = 1-hour pause.");
}
}
class UsageView : Painted
{
public Dictionary<string, object> Data;
public string Note;
DateTime updated;
readonly Timer clock = new Timer { Interval = 30000 };
// Read-only: kept out of the Tab order (it has nothing to act on); screen readers reach it as text in the UIA tree.
public UsageView()
{
TabStop = false; SetStyle(ControlStyles.Selectable, false); AccessibleRole = AccessibleRole.StaticText; Announce();
// Keeps the daily countdown current, and shows the new day's allowance at 00:00 UTC before the next update arrives.
clock.Tick += (s, e) => { if (Daily(Data)) { Invalidate(); Announce(); } };
clock.Start();
}
protected override void Dispose(bool disposing) { if (disposing) clock.Dispose(); base.Dispose(disposing); }
// Monthly passcodes: one allowance per UTC day for input, cache and output together.
static bool Daily(Dictionary<string, object> d) { return d != null && Nyxo.Num(d, "dailyUsd") > 0; }
// Pay-as-you-go passcodes: one balance for input, cache and output, with no daily limit.
static bool Balance(Dictionary<string, object> d) { return d != null && !Daily(d) && Nyxo.Num(d, "totalUsd") > 0; }
static string SpokenBalance(Dictionary<string, object> d)
{
double used = Nyxo.Num(d, "usedUsd"), total = Nyxo.Num(d, "totalUsd"), left = Math.Max(0, Nyxo.Num(d, "leftTotalUsd"));
string result = L.F("Balance: {0} of {1} used. {2} left.", Nyxo.Usd(used), Nyxo.Usd(total), Nyxo.Usd(left)) + " ";
if (used >= total) result += L.T("The balance is used up. Top it up with Renew.") + " ";
else if (used >= total * 0.8) result += L.T("At least 80 percent of the balance used.") + " ";
return result;
}
static bool NewDay(Dictionary<string, object> d) { return Nyxo.Has(d, "dayResetsAt") && Nyxo.NowMs() >= Nyxo.Num(d, "dayResetsAt"); }
static double TodayUsed(Dictionary<string, object> d) { return NewDay(d) ? 0 : Nyxo.Num(d, "usedTodayUsd"); }
static DateTime NextRenewal(Dictionary<string, object> d)
{
DateTime next = DateTime.UtcNow.Date.AddDays(1);
return DateTime.SpecifyKind(next, DateTimeKind.Utc).ToLocalTime();
}
static string Until(DateTime t)
{
return L.Span((int)Math.Ceiling((t - DateTime.Now).TotalMinutes));
}
// Set when a renewal's 30 days end before the passcode does; the allowance then drops at 00:00 UTC.
static string Drop(Dictionary<string, object> d)
{
if (!Nyxo.Has(d, "dailyDropsAt") || Nyxo.Num(d, "dailyNextUsd") <= 0) return null;
var at = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(Nyxo.Num(d, "dailyDropsAt"));
return L.F("Drops to {0} a day on {1} (UTC).", Nyxo.Usd(Nyxo.Num(d, "dailyNextUsd")), L.Date(at, false));
}
static string SpokenDaily(Dictionary<string, object> d)
{
double used = TodayUsed(d), daily = Nyxo.Num(d, "dailyUsd"), left = Math.Max(0, daily - used);
DateTime next = NextRenewal(d);
string result = L.F("Today: {0} of {1} used. {2} left.", Nyxo.Usd(used), Nyxo.Usd(daily), Nyxo.Usd(left)) + " ";
if (Drop(d) != null) result += Drop(d) + " ";
if (used >= daily) result += L.T("Today's allowance is used up.") + " ";
else if (used >= daily * 0.8) result += L.T("At least 80 percent of today's allowance used.") + " ";
return result + L.F("Renews at {0} your time, midnight UTC, in {1}.", next.ToString("HH:mm", Nyxo.Inv), Until(next)) + " ";
}
protected override UiaProvider CreateUiaProvider() { return new UiaProvider(this, UiaApi.Text, null); }
public void Show(Dictionary<string, object> d) { Data = d; Note = null; updated = DateTime.Now; Invalidate(); Announce(); }
public void SetNote(string n) { Note = n; Invalidate(); Announce(); }
public void Reset() { Data = null; Note = null; Invalidate(); Announce(); }
// Screen readers: the painted totals as one accessible summary.
static string SpokenStats(Dictionary<string, object> d)
{
if (d == null) return L.T("Not available.") + " ";
return L.F("{0} used. {1} input tokens. {2} output tokens. {3} cache read tokens. {4} cache write tokens. {5} requests.",
Nyxo.Usd(Nyxo.Num(d, "usedUsd")), Nyxo.Tok(Nyxo.Num(d, "inputTokens")), Nyxo.Tok(Nyxo.Num(d, "outputTokens")),
Nyxo.Tok(Nyxo.Num(d, "cacheReadTokens")), Nyxo.Tok(Nyxo.Num(d, "cacheWriteTokens")), ((long)Nyxo.Num(d, "requests")).ToString(Nyxo.Inv)) + " ";
}
static string SpokenBudget(Dictionary<string, object> d, string side)
{
double used = Nyxo.Num(d, "used" + side + "Usd"), budget = Nyxo.Num(d, "budget" + side + "Usd"), left = Math.Max(0, Nyxo.Num(d, "left" + side + "Usd"));
string name = side == "In" ? L.T("Input") : L.T("Output");
if (budget <= 0) return L.F("{0}: {1} used, no limit.", name, Nyxo.Usd(used)) + " ";
string result = L.F("{0}: {1} of {2} used. {3} left.", name, Nyxo.Usd(used), Nyxo.Usd(budget), Nyxo.Usd(left)) + " ";
if (used >= budget) result += L.T("Allowance used up. Buy a new passcode at nyxo.space.") + " ";
else if (used >= budget * 0.8) result += L.T("At least 80 percent used.") + " ";
return result;
}
void Announce()
{
string text = L.T("Live usage") + ". " + (Note == null ? "" : Note + ". ");
var d = Data;
if (d == null) text += L.T("Connecting.");
else
{
text += Daily(d) ? SpokenDaily(d) : Balance(d) ? SpokenBalance(d) : SpokenBudget(d, "In") + SpokenBudget(d, "Out");
if (Nyxo.Get(d, "paused") is bool && (bool)Nyxo.Get(d, "paused")) text += L.T("Paused by the admin.") + " ";
if (Policy.Known(d)) { Color pc; text += Policy.Status(d, out pc) + ". "; }
if (Nyxo.Num(d, "replying") > 0) text += L.T("Claude is replying.") + " ";
var s = Nyxo.Obj(Nyxo.Get(d, "session"));
text += L.T("This session.") + " " + SpokenStats(s);
if (s != null) text += L.F("Started {0}.", Nyxo.When(Nyxo.Num(s, "startedAt")).ToString("HH:mm", Nyxo.Inv)) + " ";
text += L.T("All sessions.") + " " + SpokenStats(d) + L.T("Session ends after 24 hours unused.") + " ";
text += (Nyxo.Has(d, "codeExpiresAt") ? L.F("Passcode expires {0}.", L.Date(Nyxo.When(Nyxo.Num(d, "codeExpiresAt")), true)) : L.T("Passcode never expires.")) + " ";
text += L.T("Opus 5.5. 4 dollars per million input tokens; 20 dollars per million output tokens.") + " " + (Daily(d) ? L.T("Input, cache and output all come out of the daily allowance.") : Balance(d) ? L.T("Input, cache and output all come out of the balance.") : L.T("Cache billed as input."));
}
if (AccessibleName == text) return;
AccessibleName = text;
AccessibilityNotifyClients(AccessibleEvents.NameChange, -1);
UiaChanged(UiaApi.NameProp, text);
}
static double Budget(Graphics g, int x, int y, int w, string name, double used, double budget, double left, double price, bool daily = false)
{
left = Math.Max(0, left);
T.Txt(g, name, T.Bold, new Rectangle(x, y, w / 3, T.P(20)), T.Text, T.L);
var bar = new RectangleF(x, y + T.P(27), w, T.P(8));
T.Fill(g, bar, T.P(4), T.Surface);
if (budget <= 0)
{
T.Txt(g, L.F("{0} used", Nyxo.Usd(used)), T.Small, new Rectangle(x + w / 3, y, w - w / 3, T.P(20)), T.Dim, T.R);
T.Txt(g, L.T("No limit on this passcode"), T.Small, new Rectangle(x, y + T.P(40), w, T.P(18)), T.Faint, T.L);
return -1;
}
double pct = Math.Min(100.0, used / budget * 100);
Color c = pct >= 100 ? T.Danger : pct >= 80 ? T.Warn : T.Dim;
T.Txt(g, L.F("{0} of {1}", Nyxo.Usd(used), Nyxo.Usd(budget)) + " " + Nyxo.Pct(pct), T.Small, new Rectangle(x + w / 3, y, w - w / 3, T.P(20)), pct >= 80 ? c : T.Dim, T.R);
if (pct > 0)
{
var fill = new RectangleF(bar.X, bar.Y, Math.Max(bar.Height, (float)(bar.Width * pct / 100)), bar.Height);
if (pct >= 80) T.Fill(g, fill, T.P(4), c);
else using (var b = T.Grad(bar, 0)) T.Fill(g, fill, T.P(4), b);
}
T.Txt(g, daily ? L.F("{0} left for input, cache and output", Nyxo.Usd(left)) : L.F("{0} left", Nyxo.Usd(left)) + " " + L.F("~{0} tokens", Nyxo.Tok(left / price * 1e6)), T.Small, new Rectangle(x, y + T.P(40), w, T.P(18)), pct >= 80 ? c : T.Faint, T.L);
return pct;
}
static void Line(Graphics g, string label, string value, int x, int y, int w)
{
// The value always shows in full; a long label (some languages) gives way.
int vw = Math.Min(w, T.W(value, T.Small) + T.P(4));
T.Txt(g, label, T.Small, new Rectangle(x, y, Math.Max(0, w - vw - T.P(8)), T.P(20)), T.Faint, T.L);
T.Txt(g, value, T.Small, new Rectangle(x + w - vw, y, vw, T.P(20)), T.Text, T.R);
}
static void Stats(Graphics g, string title, Dictionary<string, object> s, int x, int y, int w)
{
T.Txt(g, title, T.Tiny, new Rectangle(x, y, w, T.P(16)), T.Faint, T.L);
if (s == null) { T.Txt(g, "-", T.Big, new Rectangle(x, y + T.P(18), w, T.P(32)), T.Faint, T.L); return; }
T.Txt(g, Nyxo.Usd(Nyxo.Num(s, "usedUsd")), T.Big, new Rectangle(x, y + T.P(18), w, T.P(32)), T.Text, T.L);
Line(g, L.T("Tokens"), L.F("{0} in / {1} out", Nyxo.Tok(Nyxo.Num(s, "inputTokens")), Nyxo.Tok(Nyxo.Num(s, "outputTokens"))), x, y + T.P(54), w);
Line(g, L.T("Cache"), Nyxo.Tok(Nyxo.Num(s, "cacheReadTokens")) + " / " + Nyxo.Tok(Nyxo.Num(s, "cacheWriteTokens")), x, y + T.P(76), w);
Line(g, L.T("Requests"), ((long)Nyxo.Num(s, "requests")).ToString(Nyxo.Inv), x, y + T.P(98), w);
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics; g.Clear(Under); T.Smooth(g); T.Begin(g, Width);
var card = new RectangleF(0.5f, 0.5f, Width - 1.5f, Height - 1.5f);
T.Fill(g, card, T.P(14), T.Elev); T.Stroke(g, card, T.P(14), T.Border);
int pad = T.P(22), x = pad, w = Width - 2 * pad, y = T.P(18);
T.Txt(g, L.T("Live usage"), T.H2, new Rectangle(x, y, w, T.P(26)), T.Text, T.L);
string st; Color sc;
if (Note != null) { st = Note; sc = T.Warn; }
else if (Data == null) { st = L.T("Connecting..."); sc = T.Faint; }
else if (Nyxo.Num(Data, "replying") > 0) { st = L.T("Claude is replying..."); sc = T.Accent; }
else { st = L.T("Live") + " " + updated.ToString("HH:mm:ss", Nyxo.Inv); sc = T.Ok; }
int tw = T.W(st, T.Small);
T.Txt(g, st, T.Small, new Rectangle(x + w - tw - T.P(2), y, tw + T.P(2), T.P(26)), sc == T.Ok ? T.Dim : sc, T.R);
int dot = T.P(8);
using (var b = new SolidBrush(sc)) g.FillEllipse(b, x + w - tw - T.P(10) - dot, y + (T.P(26) - dot) / 2f, dot, dot);
y += T.P(42);
var d = Data;
if (d == null) { T.Txt(g, L.T("Waiting for the Nyxo server..."), T.Body, new Rectangle(x, y, w, T.P(24)), T.Faint, T.L); return; }
bool daily = Daily(d), balance = Balance(d);
double pIn = -1, pOut = -1, pDay = -1, pBal = -1;
DateTime renew = NextRenewal(d);
if (daily)
{
double dUsd = Nyxo.Num(d, "dailyUsd"), used = TodayUsed(d);
pDay = Budget(g, x, y, w, L.T("Today"), used, dUsd, dUsd - used, 4, true);
y += T.P(64);
T.Txt(g, L.T("Renews"), T.Bold, new Rectangle(x, y, w / 3, T.P(20)), T.Text, T.L);
T.Txt(g, L.F("in {0}", Until(renew)), T.Small, new Rectangle(x + w / 3, y, w - w / 3, T.P(20)), T.Dim, T.R);
string drop = Drop(d), every = L.F("Every day at 00:00 UTC ({0} your time).", renew.ToString("HH:mm", Nyxo.Inv));
T.Txt(g, drop != null ? every + " " + drop : L.F("Every day at 00:00 UTC ({0} your time). Unused allowance does not carry over.", renew.ToString("HH:mm", Nyxo.Inv)), T.Small, new Rectangle(x, y + T.P(24), w, T.P(36)), T.Faint, TextFormatFlags.Left | TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding);
y += T.P(66);
}
else if (balance)
{
double tUsd = Nyxo.Num(d, "totalUsd");
pBal = Budget(g, x, y, w, L.T("Balance"), Nyxo.Num(d, "usedUsd"), tUsd, Nyxo.Num(d, "leftTotalUsd"), 4, true);
y += T.P(64);
T.Txt(g, L.T("Pay as you go"), T.Bold, new Rectangle(x, y, w / 2, T.P(20)), T.Text, T.L);
T.Txt(g, L.T("No daily limit"), T.Small, new Rectangle(x + w / 2, y, w - w / 2, T.P(20)), T.Dim, T.R);
T.Txt(g, L.T("Use it at your own pace. Top it up any time with Renew; unused balance stays on the passcode."), T.Small, new Rectangle(x, y + T.P(24), w, T.P(36)), T.Faint, TextFormatFlags.Left | TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding);
y += T.P(66);
}
else
{
pIn = Budget(g, x, y, w, L.T("Input"), Nyxo.Num(d, "usedInUsd"), Nyxo.Num(d, "budgetInUsd"), Nyxo.Num(d, "leftInUsd"), 4);
y += T.P(64);
pOut = Budget(g, x, y, w, L.T("Output"), Nyxo.Num(d, "usedOutUsd"), Nyxo.Num(d, "budgetOutUsd"), Nyxo.Num(d, "leftOutUsd"), 20);
y += T.P(66);
}
string notice = null; Color nc = T.Warn;
double policyUntil = Policy.PausedUntil(d);
if (Policy.Revoked(d)) { notice = L.T("Passcode revoked for repeated usage-policy violations. No refund is issued."); nc = T.Danger; }
else if (policyUntil > 0) { notice = Policy.PausedText(policyUntil) + ". " + Policy.Next(d); nc = T.Danger; }
else if (Nyxo.Get(d, "paused") is bool && (bool)Nyxo.Get(d, "paused")) notice = L.T("Paused by the admin. Requests wait until it is resumed.");
else if (pDay >= 100) { notice = L.F("Today's allowance is used up. It renews at {0} (00:00 UTC).", renew.ToString("HH:mm", Nyxo.Inv)); nc = T.Danger; }
else if (pBal >= 100) { notice = L.T("The balance is used up. Top it up with Renew."); nc = T.Danger; }
else if (pIn >= 100) { notice = L.T("Input allowance used up. Buy a new passcode at nyxo.space."); nc = T.Danger; }
else if (pOut >= 100) { notice = L.T("Output allowance used up. Buy a new passcode at nyxo.space."); nc = T.Danger; }
else if (Policy.Strikes(d) > 0) notice = Policy.StrikesText(d) + ". " + Policy.Next(d);
else if (pDay >= 80) notice = L.T("Over 80% of today's allowance is used.");
else if (pBal >= 80) notice = L.T("Over 80% of the balance is used.");
else if (pIn >= 80 || pOut >= 80) notice = pIn >= pOut ? L.T("Over 80% of the input budget is used.") : L.T("Over 80% of the output budget is used.");
// The notice wraps onto more lines when the card is narrow; the gaps below give up the extra height.
int nh = T.P(32);
if (notice != null)
{
const TextFormatFlags wrap = TextFormatFlags.Left | TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding;
int ntw = w - T.P(24), nth = TextRenderer.MeasureText(g, notice, T.Small, new Size(ntw, 0), wrap).Height;
nh = Math.Max(nh, nth + T.P(14));
var nr = new RectangleF(x, y, w, nh);
T.Fill(g, nr, T.P(8), T.Mix(nc, T.Elev, 0.86f)); T.Stroke(g, nr, T.P(8), T.Mix(nc, T.Elev, 0.6f));
T.Txt(g, notice, T.Small, new Rectangle(x + T.P(12), y + (nh - nth) / 2, ntw, nth), nc, wrap);
}
y += nh + T.P(12);
int squeeze = Math.Max(0, Math.Min(T.P(20), y + T.P(14 + 130 + 12 + 37) - (Height - T.P(30))));
int sStats = Math.Min(squeeze, T.P(10)), sTop = Math.Min(squeeze - sStats, T.P(6)), sBottom = squeeze - sStats - sTop;
using (var pen = new Pen(T.Border)) g.DrawLine(pen, x, y, x + w, y);
y += T.P(14) - sTop;
int cw = (w - T.P(24)) / 2, x2 = x + cw + T.P(24);
var s = Nyxo.Obj(Nyxo.Get(d, "session"));
Stats(g, s != null ? L.T("THIS SESSION") + " - " + L.F("since {0}", Nyxo.When(Nyxo.Num(s, "startedAt")).ToString("HH:mm", Nyxo.Inv)) : L.T("THIS SESSION"), s, x, y, cw);
Stats(g, L.T("ALL SESSIONS"), d, x2, y, cw);
y += T.P(130) - sStats;
using (var pen = new Pen(T.Border)) g.DrawLine(pen, x, y, x + w, y);
y += T.P(12) - sBottom;
T.Txt(g, L.T("SESSION"), T.Tiny, new Rectangle(x, y, cw, T.P(16)), T.Faint, T.L);
T.Txt(g, L.T("Ends after 24 h unused"), T.Small, new Rectangle(x, y + T.P(17), cw, T.P(20)), T.Text, T.L);
T.Txt(g, L.T("PASSCODE"), T.Tiny, new Rectangle(x2, y, cw, T.P(16)), T.Faint, T.L);
T.Txt(g, Nyxo.Has(d, "codeExpiresAt") ? L.F("Expires {0}", L.Date(Nyxo.When(Nyxo.Num(d, "codeExpiresAt")), true)) : L.T("Never expires"), T.Small, new Rectangle(x2, y + T.P(17), cw, T.P(20)), T.Text, T.L);
T.Txt(g, daily ? L.T("Opus 5.5 - $4 / 1M input, $20 / 1M output, all from the daily allowance") : balance ? L.T("Opus 5.5 - $4 / 1M input, $20 / 1M output, all from the balance") : L.T("Opus 5.5 - $4 / 1M input, $20 / 1M output, cache billed as input"), T.Tiny, new Rectangle(x, Height - T.P(34), w, T.P(18)), T.Faint, T.L);
}
}
class LoginPage : Panel
{
public readonly RowList SavedList = new RowList { MaxRows = 3, RowH = 50 };
public readonly Field Pass = new Field("", true);
public readonly Check Remember = new Check("", true);
public readonly Btn Connect = new Btn("", Btn.Kind.Primary);
public readonly Btn Lang = L.Button();
public readonly Btn Buy = new Btn("", Btn.Kind.Ghost) { Glyph = "\uE7BF" };
string status = ""; Color statusColor = T.Dim; bool hasSaved;
Rectangle logoR, titleR, subR, savedR, divR, statusR;
public LoginPage()
{
BackColor = T.Bg;
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true);
Controls.Add(SavedList); Controls.Add(Pass); Controls.Add(Remember); Controls.Add(Connect); Controls.Add(Lang); Controls.Add(Buy);
Relabel();
// The highlight only marks the keyboard position; an empty Enter still uses the most recent passcode.
SavedList.LostFocus += (s, e) => { SavedList.Selected = -1; SavedList.Invalidate(); };
}
public void SetSaved(List<RowList.Item> items)
{
SavedList.Set(items); hasSaved = items.Count > 0; SavedList.Visible = hasSaved;
PerformLayout(); Invalidate();
}
// Texts set once on controls; painted texts follow the language on their own.
public void Relabel()
{
Pass.SetCue(L.T("Passcode")); Remember.Text = L.T("Remember on this PC");
Connect.Text = Connect.Enabled ? L.T("Connect") : L.T("Connecting...");
SavedList.AccessibleName = L.T("Saved passcodes");
Buy.Text = L.T("Buy a passcode");
L.Label(Lang);
PerformLayout(); Invalidate();
}
public void SetStatus(string s, Color c) { status = s ?? ""; statusColor = c; Invalidate(); }
public void SetBusy(bool busy)
{
Connect.Enabled = !busy; SavedList.Enabled = !busy; Buy.Enabled = !busy; Pass.Box.ReadOnly = busy; Remember.Enabled = !busy;
Connect.Text = busy ? L.T("Connecting...") : L.T("Connect");
Cursor = busy ? Cursors.AppStarting : Cursors.Default;
}
// The status (for example the one-device explanation) word-wraps onto up to two lines, centred under Connect.
const TextFormatFlags StatusFlags = TextFormatFlags.HorizontalCenter | TextFormatFlags.Top | TextFormatFlags.WordBreak |
TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis;
static int StatusHeight() { return Math.Max(T.P(22), 2 * TextRenderer.MeasureText("Ag", T.Small, Size.Empty, TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix).Height); }
protected override void OnLayout(LayoutEventArgs e)
{
base.OnLayout(e);
int W = T.P(370), x = (ClientSize.Width - W) / 2, statusH = StatusHeight(), subGap = T.P(22);
int h = T.P(56 + 12 + 36 + 22) + subGap + (hasSaved ? T.P(22 + 14 + 18 + 14) + SavedList.Wanted : 0) + T.P(42 + 12 + 22 + 18 + 42 + 10) + statusH;
// Short of room (three saved passcodes at the minimum window size): tighten the gap under the subtitle.
int cut = Math.Max(0, Math.Min(T.P(12), h + 2 * T.P(8) - ClientSize.Height));
subGap -= cut; h -= cut;
int y = Math.Max(T.P(8), (ClientSize.Height - h) / 2);
logoR = new Rectangle(x + (W - T.P(56)) / 2, y, T.P(56), T.P(56)); y += T.P(56 + 12);
titleR = new Rectangle(x, y, W, T.P(36)); y += T.P(36);
subR = new Rectangle(x, y, W, T.P(22)); y += T.P(22) + subGap;
if (hasSaved)
{
savedR = new Rectangle(x, y, W, T.P(16)); y += T.P(22);
SavedList.SetBounds(x, y, W, SavedList.Wanted); y += SavedList.Wanted + T.P(14);
divR = new Rectangle(x, y, W, T.P(18)); y += T.P(18 + 14);
}
Pass.SetBounds(x, y, W, T.P(42)); y += T.P(42 + 12);
Remember.SetBounds(x, y, Remember.Wanted, T.P(22)); y += T.P(22 + 18);
Connect.SetBounds(x, y, W, T.P(42)); y += T.P(42 + 10);
statusR = new Rectangle(x - T.P(60), y, W + T.P(120), statusH);
int lw = L.ButtonWidth(Lang);
Lang.SetBounds(ClientSize.Width - T.P(14) - lw, T.P(12), lw, T.P(32));
Buy.SetBounds(T.P(14), T.P(12), T.W(Buy.Text, Buy.Font) + T.P(22 + 24), T.P(32));
if (hasSaved) T.Mirror(this, SavedList);
T.Mirror(this, Pass, Remember, Connect, Lang, Buy);
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var g = e.Graphics; T.Smooth(g); T.Begin(g, Width);
T.Img(g, T.Logo(logoR.Width), logoR.Location);
T.Txt(g, "Nyxo Code", T.Title, titleR, T.Text, T.C);
T.Txt(g, L.F("Version {0}", Nyxo.Version), T.Tiny, new Rectangle(0, ClientSize.Height - T.P(30), ClientSize.Width, T.P(20)), T.Faint, T.C);
T.Txt(g, L.T("Claude Code on your Nyxo passcode"), T.Body, subR, T.Dim, T.C);
if (hasSaved)
{
T.Txt(g, L.T("SAVED PASSCODES"), T.Tiny, savedR, T.Faint, T.L);
T.Txt(g, L.T("Right-click to rename"), T.Tiny, savedR, T.Faint, T.R);
string or = L.T("or enter a passcode");
int tw = T.W(or, T.Small), mid = divR.Y + divR.Height / 2;
using (var pen = new Pen(T.Border))
{
g.DrawLine(pen, divR.X, mid, divR.X + (divR.Width - tw) / 2 - T.P(12), mid);
g.DrawLine(pen, divR.X + (divR.Width + tw) / 2 + T.P(12), mid, divR.Right, mid);
}
T.Txt(g, or, T.Small, divR, T.Faint, T.C);
}
if (status != "")
{
// One line sits where it always did; a longer message takes the second line reserved below it.
int th = Math.Min(statusR.Height, TextRenderer.MeasureText(g, status, T.Small, new Size(statusR.Width, 0), StatusFlags).Height);
int top = th <= T.P(22) ? statusR.Y + (T.P(22) - th) / 2 : statusR.Y;
T.Txt(g, status, T.Small, new Rectangle(statusR.X, top, statusR.Width, statusR.Bottom - top), statusColor, StatusFlags);
}
}
}
class HomePage : Panel
{
public readonly Btn SignOut = new Btn("", Btn.Kind.Secondary);
public readonly Check OnTop = new Check("", false);
public readonly Btn Lang = L.Button();
public readonly Btn History = new Btn("", Btn.Kind.Secondary) { Glyph = "\uE9D2" };
public readonly Btn Renew = new Btn("", Btn.Kind.Secondary) { Glyph = "\uE7BF" };
public readonly Card Launcher = new Card();
public readonly Field Folder = new Field("", false);
public readonly Btn Browse = new Btn("", Btn.Kind.Secondary);
public readonly RowList Recent = new RowList { MaxRows = 3, RowH = 46, PathSub = true };
public readonly Btn Open = new Btn("", Btn.Kind.Primary) { Glyph = "\uE756" };
public readonly Btn Install = new Btn("", Btn.Kind.Ghost) { Visible = false };
public bool ClaudeMissing;
public readonly UsageView Usage = new UsageView();
string user = "", status = ""; Color statusColor = T.Dim;
// Usage-policy block above the Open button: the passcode's strike status and the rule, always shown.
Dictionary<string, object> policy;
Rectangle policyR;
const TextFormatFlags Wrap = TextFormatFlags.Left | TextFormatFlags.Top | TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding;
public void SetPolicy(Dictionary<string, object> d) { policy = d; Launcher.Invalidate(policyR); }
public void TickPolicy() { if (policy != null && (Policy.Strikes(policy) > 0 || Policy.PausedUntil(policy) > 0)) Launcher.Invalidate(policyR); }
public HomePage()
{
BackColor = T.Bg;
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true);
Launcher.Controls.Add(Folder); Launcher.Controls.Add(Browse); Launcher.Controls.Add(Recent); Launcher.Controls.Add(Install); Launcher.Controls.Add(Open);
Launcher.Paint += PaintCard;
Controls.Add(Launcher); Controls.Add(Usage); Controls.Add(OnTop); Controls.Add(SignOut); Controls.Add(Lang); Controls.Add(History); Controls.Add(Renew);
Install.VisibleChanged += (s, e) => PerformLayout();
Relabel();
}
public void Relabel()
{
SignOut.Text = L.T("Sign out"); OnTop.Text = L.T("Keep on top"); History.Text = L.T("History"); Renew.Text = L.T("Renew");
History.AccessibleName = History.Text; Renew.AccessibleName = Renew.Text;
Folder.SetCue(L.T("Choose a folder, or paste its path")); Browse.Text = L.T("Browse");
Open.Text = ClaudeMissing ? L.T("Locate Claude Code...") : L.T("Open Claude Code");
Install.Text = L.T("How to install Claude Code");
Recent.AccessibleName = L.T("Recent folders");
L.Label(Lang);
PerformLayout(); Invalidate(); Launcher.Invalidate(); Usage.Invalidate();
}
public void SetUser(string label) { user = label ?? ""; Invalidate(); }
public void SetStatus(string s, Color c)
{
status = s ?? ""; statusColor = c;
if (StatusHeight(Launcher.Width - 2 * T.P(22)) != statusH) PerformLayout();
Launcher.Invalidate();
}
// The launcher's status word-wraps (up to 3 lines) under the Open button, so a long error is never cut off.
int statusH;
const TextFormatFlags StatusFlags = TextFormatFlags.HorizontalCenter | TextFormatFlags.Top | TextFormatFlags.WordBreak |
TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis;
int StatusHeight(int width)
{
if (status == "" || width <= 0) return T.P(22);
int line = TextRenderer.MeasureText("Ag", T.Small, Size.Empty, TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix).Height;
int h = TextRenderer.MeasureText(status, T.Small, new Size(width, 0), StatusFlags).Height;
return Math.Max(T.P(22), Math.Min(3 * line + T.P(2), h + T.P(2)));
}
public void SetRecent(List<RowList.Item> items) { Recent.Set(items); PerformLayout(); Launcher.Invalidate(); }
// While a folder or Claude Code is being checked in the background, the launcher cannot start another check.
public bool Busy { get; private set; }
Control refocus;
public void SetBusy(bool busy)
{
if (busy == Busy) return;
Busy = busy;
if (busy) refocus = Open.Focused ? Open : Browse.Focused ? Browse : Recent.Focused ? (Control)Recent : null;
Open.Enabled = !busy; Browse.Enabled = !busy; Recent.Enabled = !busy; Folder.Box.ReadOnly = busy;
Cursor = Launcher.Cursor = busy ? Cursors.AppStarting : Cursors.Default;
if (!busy && refocus != null && !Folder.Box.Focused && refocus.CanFocus) refocus.Focus();
if (!busy) refocus = null;
}
protected override void OnLayout(LayoutEventArgs e)
{
base.OnLayout(e);
int W = ClientSize.Width, H = ClientSize.Height, m = T.P(24), top = T.P(78);
int sw = Math.Max(T.P(96), T.W(SignOut.Text, T.Bold) + T.P(32)), lgw = L.ButtonWidth(Lang);
SignOut.SetBounds(W - m - sw, T.P(22), sw, T.P(34));
Lang.SetBounds(SignOut.Left - T.P(10) - lgw, T.P(23), lgw, T.P(32));
OnTop.SetBounds(Lang.Left - T.P(14) - OnTop.Wanted, T.P(28), OnTop.Wanted, T.P(22));
// History and Renew show only their icons when the full labels would leave too little room for "Connected as".
int hw = T.W(History.Text, T.Bold) + T.P(22 + 30), rw = T.W(Renew.Text, T.Bold) + T.P(22 + 30);
int room = OnTop.Left - T.P(18) - T.P(88 + 230);
History.IconOnly = Renew.IconOnly = hw + T.P(10) + rw > room;
if (History.IconOnly) hw = rw = T.P(40);
History.SetBounds(OnTop.Left - T.P(18) - hw, T.P(22), hw, T.P(34));
Renew.SetBounds(History.Left - T.P(10) - rw, T.P(22), rw, T.P(34));
// The launcher grows with the window, up to a comfortable width; usage takes the rest.
int lw = Math.Max(T.P(360), Math.Min(T.P(620), (W - 2 * m - T.P(16)) * 9 / 20));
Launcher.SetBounds(m, top, lw, H - top - m);
Usage.SetBounds(m + lw + T.P(16), top, W - 2 * m - lw - T.P(16), H - top - m);
int pad = T.P(22), cw = lw - 2 * pad, ch = Launcher.Height;
int bw = Math.Max(T.P(90), T.W(Browse.Text, T.Bold) + T.P(28));
statusH = StatusHeight(cw);
Folder.SetBounds(pad, T.P(104), cw - bw - T.P(8), T.P(42));
Browse.SetBounds(pad + cw - bw, T.P(104), bw, T.P(42));
Open.SetBounds(pad, ch - pad - statusH - T.P(8) - T.P(46), cw, T.P(46));
Install.SetBounds(pad, Open.Top - T.P(36), cw, T.P(28));
int ruleH = TextRenderer.MeasureText(Policy.Rule, T.Tiny, new Size(cw, 0), Wrap).Height;
int ph = T.P(18 + 3) + ruleH;
policyR = new Rectangle(pad, (Install.Visible ? Install.Top : Open.Top) - T.P(10) - ph, cw, ph);
Recent.SetMaxRows((policyR.Top - T.P(8) - T.P(184) + T.P(Recent.Gap)) / (T.P(Recent.RowH) + T.P(Recent.Gap)));
Recent.SetBounds(pad, T.P(184), cw, Recent.Wanted);
T.Mirror(this, SignOut, Lang, OnTop, History, Renew, Launcher, Usage);
T.Mirror(Launcher, Folder, Browse, Open, Install, Recent);
Launcher.Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var g = e.Graphics; T.Smooth(g); T.Begin(g, Width);
T.Img(g, T.Logo(T.P(38)), new Point(T.P(24), T.P(20)));
T.Txt(g, "Nyxo Code", T.H2, new Rectangle(T.P(74), T.P(16), T.P(300), T.P(26)), T.Text, T.L);
int vx = T.P(74) + T.W("Nyxo Code", T.H2) + T.P(10);
T.Txt(g, L.F("Version {0}", Nyxo.Version), T.Tiny, new Rectangle(vx, T.P(16), T.P(120), T.P(26)), T.Faint, T.L);
int dot = T.P(7);
using (var b = new SolidBrush(T.Ok)) g.FillEllipse(b, T.P(75), T.P(46) + (T.P(18) - dot) / 2f, dot, dot);
int hl = T.Rtl ? Width - Renew.Right : Renew.Left; // the leftmost header button's edge before mirroring
T.Txt(g, user != "" ? L.F("Connected as \"{0}\"", user) : L.T("Connected"), T.Small, new Rectangle(T.P(88), T.P(46), Math.Min(T.P(320), hl - T.P(12) - T.P(88)), T.P(18)), T.Dim, T.L);
}
void PaintCard(object sender, PaintEventArgs e)
{
var g = e.Graphics; T.Begin(g, Launcher.Width);
int pad = T.P(22), cw = Launcher.Width - 2 * pad;
T.Txt(g, L.T("Open Claude Code"), T.H2, new Rectangle(pad, T.P(20), cw, T.P(26)), T.Text, T.L);
T.Txt(g, L.T("It opens in its own window, working in this folder."), T.Small, new Rectangle(pad, T.P(48), cw, T.P(18)), T.Dim, T.L);
T.Txt(g, L.T("FOLDER"), T.Tiny, new Rectangle(pad, T.P(84), cw, T.P(16)), T.Faint, T.L);
T.Txt(g, L.T("RECENT"), T.Tiny, new Rectangle(pad, T.P(164), cw, T.P(16)), T.Faint, T.L);
if (Recent.Items.Count == 0) T.Txt(g, L.T("Folders you open will show up here."), T.Small, new Rectangle(pad, T.P(184), cw, T.P(20)), T.Faint, T.L);
if (policyR.Height > 0)
{
Color pc = T.Dim;
string ps = policy != null && Policy.Known(policy) ? Policy.Status(policy, out pc) : L.T("Policy strikes: -");
T.Txt(g, ps, T.Small, new Rectangle(policyR.X, policyR.Y, policyR.Width, T.P(18)), pc, T.L);
T.Txt(g, Policy.Rule, T.Tiny, new Rectangle(policyR.X, policyR.Y + T.P(21), policyR.Width, policyR.Height - T.P(21)), T.Faint, Wrap);
}
if (status != "")
{
int sh = Math.Max(T.P(22), statusH);
int th = Math.Min(sh, TextRenderer.MeasureText(g, status, T.Small, new Size(cw, 0), StatusFlags).Height);
int top = Launcher.Height - pad - sh + (th < T.P(22) ? (T.P(22) - th) / 2 : 0);
T.Txt(g, status, T.Small, new Rectangle(pad, top, cw, Launcher.Height - pad - top), statusColor, StatusFlags);
}
}
}
History.csنافذة سجل الاستخدام ومخططها للإنفاق اليومي.351 سطر
// The usage history window: spend per UTC day as a bar chart, for the last 7, 30 or 90 days.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
class HistoryChart : Painted
{
public class Day { public DateTime Date; public double Usd, Requests, In, Out, Read, Write; }
public List<Day> Days = new List<Day>();
public double Daily;
public string Message = "";
int hover = -1, sel = -1;
bool keys; // the arrow keys were used: the selected day shows its details
RectangleF plot; float slot;
public HistoryChart()
{
TabStop = true; AccessibleRole = AccessibleRole.Chart;
Cursor = Cursors.Default;
}
protected override UiaProvider CreateUiaProvider() { return new UiaProvider(this, UiaApi.Text, null); }
public void SetMessage(string m) { Message = m; hover = sel = -1; Invalidate(); Announce(); }
public void Set(List<Day> days, double daily) { Days = days; Daily = daily; Message = null; hover = -1; if (sel >= days.Count) sel = -1; Invalidate(); Announce(); }
public void Changed() { Invalidate(); Announce(); }
public static string Label(Day d) { return d.Date.ToString("dddd", L.Lang == "en" ? Nyxo.Inv : L.Culture) + " " + L.Date(d.Date, false); }
// Spend counted before the daily details were kept (the day history started) has no request or token counts.
static bool NoDetails(Day d) { return d.Usd > 0 && d.Requests <= 0 && d.In <= 0 && d.Out <= 0; }
static string Details(Day d)
{
if (NoDetails(d)) return L.T("Details were not recorded for this day.");
return L.F("{0} requests", d.Requests.ToString("#,0", Nyxo.Inv)) + ". " +
L.F("Tokens: {0} in, {1} out", Nyxo.Tok(d.In), Nyxo.Tok(d.Out)) + ". " +
L.F("Cache: {0} read, {1} written", Nyxo.Tok(d.Read), Nyxo.Tok(d.Write)) + ".";
}
void Announce()
{
string text = L.T("Spend per day") + ". ";
if (Message != null) text += Message;
else
{
int i = sel >= 0 ? sel : Days.Count - 1;
if (i >= 0) text += Label(Days[i]) + ": " + Nyxo.Usd(Days[i].Usd) + ". " + Details(Days[i]) + " " + L.T("Use the arrow keys to move between days.");
}
if (AccessibleName == text) return;
AccessibleName = text;
AccessibilityNotifyClients(AccessibleEvents.NameChange, -1);
UiaChanged(UiaApi.NameProp, text);
}
// A round step (1, 2, 2.5 or 5 times a power of ten) and 3 to 5 grid steps that reach just above the top value.
static double Step(double top, out int count)
{
double target = Math.Max(top, 0.01) * 1.06, best = double.MaxValue, step = 1;
count = 4;
double mag0 = Math.Pow(10, Math.Floor(Math.Log10(target / 5)));
foreach (var mag in new[] { mag0, mag0 * 10 })
foreach (var m in new[] { 1, 2, 2.5, 5 })
{
double st = m * mag; int c = (int)Math.Ceiling(target / st - 1e-9);
if (c < 3 || c > 5 || c * st > best || (c * st == best && c < count)) continue;
best = c * st; step = st; count = c;
}
return step;
}
static string Axis(double v) { return v == 0 ? "$0" : "$" + v.ToString(v >= 100 ? "#,0" : v >= 1 ? "0.##" : "0.0##", Nyxo.Inv); }
string Tick(Day d)
{
var c = L.Lang == "en" ? Nyxo.Inv : L.Culture;
return Days.Count <= 7 ? d.Date.ToString("ddd", c) + " " + d.Date.Day.ToString(Nyxo.Inv) : d.Date.Day.ToString(Nyxo.Inv) + " " + d.Date.ToString("MMM", c);
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics; g.Clear(Under); T.Smooth(g); T.Begin(g, Width);
if (Message != null || Days.Count == 0)
{
T.Txt(g, Message ?? "", T.Body, ClientRectangle, T.Faint, T.C | TextFormatFlags.WordBreak);
return;
}
// The scale follows the spend, so small days stay readable; the daily allowance line shows when it is near.
double max = Days.Max(d => d.Usd);
bool line = Daily > 0 && Daily <= max * 1.6;
double top = line ? Math.Max(max, Daily) : max > 0 ? max : Daily > 0 ? Daily : 1;
int lines;
double step = Step(top, out lines), scale = step * lines;
int aw = Enumerable.Range(0, lines + 1).Max(i => T.W(Axis(step * i), T.Tiny)) + T.P(10);
plot = new RectangleF(aw, T.P(24), Width - aw - T.P(2), Height - T.P(24) - T.P(28));
int n = Days.Count;
slot = plot.Width / n;
using (var pen = new Pen(T.Border))
for (int i = 0; i <= lines; i++)
{
float y = plot.Bottom - plot.Height * i / lines;
g.DrawLine(pen, plot.X, y, plot.Right, y);
T.Txt(g, Axis(step * i), T.Tiny, new Rectangle(0, (int)y - T.P(9), aw - T.P(8), T.P(18)), T.Faint, T.R);
}
float bw = Math.Min(T.P(34), Math.Max(T.P(2), slot * (n > 40 ? 0.7f : 0.62f)));
int focus = hover >= 0 ? hover : Focused && keys ? sel : -1;
for (int i = 0; i < n; i++)
{
var d = Days[i];
float cx = plot.X + slot * i + slot / 2;
if (i == focus)
using (var b = new SolidBrush(Color.FromArgb(70, T.Surface))) T.Fill(g, new RectangleF(plot.X + slot * i, plot.Y, slot, plot.Height), T.P(4), b);
if (d.Usd <= 0) continue;
float h = Math.Max(T.P(2), (float)(d.Usd / scale * plot.Height));
var r = new RectangleF(cx - bw / 2, plot.Bottom - h, bw, h);
float rad = Math.Min(T.P(4), bw / 2);
bool over = Daily > 0 && d.Usd >= Daily, last = i == n - 1;
if (over) T.Fill(g, r, rad, i == focus ? T.Mix(T.Danger, Color.White, 0.2f) : T.Danger);
else if (last || i == focus) using (var b = T.Grad(r, i == focus ? 0.18f : 0f)) T.Fill(g, r, rad, b);
else T.Fill(g, r, rad, T.Mix(T.Accent, T.Elev, 0.45f));
}
if (Daily > 0)
{
float y = line ? plot.Bottom - (float)(Daily / scale * plot.Height) : plot.Y;
if (line) using (var pen = new Pen(T.Warn, Math.Max(1f, T.S)) { DashStyle = DashStyle.Dash }) g.DrawLine(pen, plot.X, y, plot.Right, y);
string dl = L.F("Daily allowance {0}", Nyxo.Usd(Daily));
int dw = T.W(dl, T.Tiny) + T.P(4);
T.Txt(g, dl, T.Tiny, new Rectangle((int)plot.Right - dw, (int)y - T.P(20), dw, T.P(16)), line ? T.Warn : T.Faint, T.R);
}
// Day labels, counted back from today so that today always has one.
int lw = Days.Max(d => T.W(Tick(d), T.Tiny)) + T.P(12);
int every = Math.Max(1, (int)Math.Ceiling(lw / slot));
for (int i = n - 1; i >= 0; i -= every)
{
float cx = plot.X + slot * i + slot / 2;
string t = i == n - 1 ? L.T("Today") : Tick(Days[i]);
int tw = T.W(t, T.Tiny) + T.P(4);
int x = (int)Math.Max(0, Math.Min(Width - tw, cx - tw / 2f));
T.Txt(g, t, T.Tiny, new Rectangle(x, (int)plot.Bottom + T.P(8), tw, T.P(16)), i == n - 1 ? T.Text : T.Faint, T.C);
}
if (focus >= 0) Tip(g, focus, scale);
}
void Tip(Graphics g, int i, double scale)
{
var d = Days[i];
string head = Label(d), usd = Nyxo.Usd(d.Usd);
var lines = NoDetails(d) ? new[] { L.T("Details were not recorded for this day.") } : new[] {
L.F("{0} requests", d.Requests.ToString("#,0", Nyxo.Inv)),
L.F("Tokens: {0} in, {1} out", Nyxo.Tok(d.In), Nyxo.Tok(d.Out)),
L.F("Cache: {0} read, {1} written", Nyxo.Tok(d.Read), Nyxo.Tok(d.Write)),
};
int pad = T.P(12), lh = T.P(19);
int w = Math.Max(T.W(head, T.Small), Math.Max(T.W(usd, T.Big), lines.Max(l => T.W(l, T.Small)))) + 2 * pad + T.P(4);
int h = pad + T.P(18) + T.P(30) + lines.Length * lh + pad - T.P(4);
float cx = plot.X + slot * i + slot / 2;
float barTop = plot.Bottom - (float)(d.Usd / scale * plot.Height);
float x = cx + slot / 2 + T.P(8);
if (x + w > Width) x = cx - slot / 2 - T.P(8) - w;
x = Math.Max(0, x);
float y = Math.Max(0, Math.Min(Height - h, Math.Min(barTop, plot.Bottom - h / 2f) - h / 2f));
var r = new RectangleF(x, y, w, h);
using (var b = new SolidBrush(Color.FromArgb(70, 0, 0, 0))) T.Fill(g, new RectangleF(r.X + T.P(2), r.Y + T.P(3), r.Width, r.Height), T.P(10), b);
T.Fill(g, r, T.P(10), T.Elev2); T.Stroke(g, r, T.P(10), T.Border);
int tx = (int)x + pad, ty = (int)y + pad - T.P(2), tw = w - 2 * pad;
T.Txt(g, head, T.Small, new Rectangle(tx, ty, tw, T.P(18)), T.Dim, T.L);
T.Txt(g, usd, T.Big, new Rectangle(tx, ty + T.P(18), tw, T.P(30)), Daily > 0 && d.Usd >= Daily ? T.Danger : T.Text, T.L);
for (int k = 0; k < lines.Length; k++)
T.Txt(g, lines[k], T.Small, new Rectangle(tx, ty + T.P(48) + k * lh, tw, lh), T.Faint, T.L);
}
int At(Point p)
{
if (Message != null || slot <= 0) return -1;
float x = T.Rtl ? Width - 1 - p.X : p.X;
if (x < plot.X || x >= plot.Right || p.Y < plot.Y || p.Y > plot.Bottom + T.P(26)) return -1;
return Math.Min(Days.Count - 1, (int)((x - plot.X) / slot));
}
protected override void OnMouseMove(MouseEventArgs e) { int i = At(e.Location); if (i != hover) { hover = i; Invalidate(); } base.OnMouseMove(e); }
protected override void OnMouseLeave(EventArgs e) { if (hover >= 0) { hover = -1; Invalidate(); } base.OnMouseLeave(e); }
protected override void OnMouseDown(MouseEventArgs e) { Focus(); int i = At(e.Location); if (i >= 0) { sel = i; Announce(); } base.OnMouseDown(e); }
protected override void OnGotFocus(EventArgs e) { if (sel < 0) sel = Days.Count - 1; Invalidate(); Announce(); base.OnGotFocus(e); }
protected override void OnLostFocus(EventArgs e) { Invalidate(); base.OnLostFocus(e); }
protected override bool IsInputKey(Keys k) { return k == Keys.Left || k == Keys.Right || k == Keys.Home || k == Keys.End || base.IsInputKey(k); }
protected override void OnKeyDown(KeyEventArgs e)
{
if (Message == null && Days.Count > 0)
{
int dir = T.Rtl ? -1 : 1, i = sel < 0 ? Days.Count - 1 : sel;
if (e.KeyCode == Keys.Left) i -= dir; else if (e.KeyCode == Keys.Right) i += dir;
else if (e.KeyCode == Keys.Home) i = 0; else if (e.KeyCode == Keys.End) i = Days.Count - 1;
i = Math.Max(0, Math.Min(Days.Count - 1, i));
if (e.KeyCode == Keys.Left || e.KeyCode == Keys.Right || e.KeyCode == Keys.Home || e.KeyCode == Keys.End) { keys = true; Invalidate(); }
if (i != sel) { sel = i; hover = -1; Invalidate(); Announce(); }
}
base.OnKeyDown(e);
}
}
class HistoryForm : Form
{
static readonly int[] Spans = { 7, 30, 90 };
readonly Func<int, Dictionary<string, object>> load;
readonly Btn[] range = new Btn[Spans.Length];
readonly Btn close = new Btn("", Btn.Kind.Secondary);
readonly Card card = new Card();
readonly HistoryChart chart = new HistoryChart();
int days = 30, gen;
DateTime since = DateTime.MinValue;
Rectangle[] stats = new Rectangle[4];
Rectangle subR, noteR;
const TextFormatFlags Wrap = TextFormatFlags.Left | TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding;
public HistoryForm(Func<int, Dictionary<string, object>> load)
{
this.load = load;
AutoScaleMode = AutoScaleMode.None; FormBorderStyle = FormBorderStyle.FixedDialog; DoubleBuffered = true;
MaximizeBox = MinimizeBox = false; ShowInTaskbar = false; ShowIcon = false; BackColor = T.Bg; Font = T.Body;
StartPosition = FormStartPosition.CenterParent;
if (T.Rtl) RightToLeft = RightToLeft.Yes;
ClientSize = new Size(T.P(780), T.P(540));
for (int i = 0; i < Spans.Length; i++)
{
int n = Spans[i];
range[i] = new Btn("", Btn.Kind.Secondary);
range[i].Click += (s, e) => { if (n != days || chart.Message != null) Fetch(n); };
Controls.Add(range[i]);
}
close.Click += (s, e) => Close();
CancelButton = close;
card.Controls.Add(chart);
Controls.Add(card); Controls.Add(close);
Relabel();
Fetch(days);
}
protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); T.DarkTitle(Handle); }
protected override void OnShown(EventArgs e) { base.OnShown(e); chart.Focus(); }
public void Relabel()
{
Text = L.T("Usage history");
for (int i = 0; i < Spans.Length; i++) range[i].Text = L.F("{0} days", Spans[i]);
close.Text = L.T("Close");
RightToLeft = T.Rtl ? RightToLeft.Yes : RightToLeft.No;
PerformLayout(); Invalidate(); chart.Changed();
}
protected override void OnLayout(LayoutEventArgs e)
{
base.OnLayout(e);
if (range[range.Length - 1] == null) return; // still being built
int W = ClientSize.Width, H = ClientSize.Height, pad = T.P(24);
int x = W - pad;
for (int i = Spans.Length - 1; i >= 0; i--)
{
int bw = T.W(range[i].Text, T.Bold) + T.P(28);
x -= bw;
range[i].SetBounds(x, pad, bw, T.P(34));
x -= T.P(8);
}
subR = new Rectangle(pad, pad, x - pad - T.P(8), T.P(34));
int sy = pad + T.P(50), gap = T.P(12), sw = (W - 2 * pad - 3 * gap) / 4;
for (int i = 0; i < 4; i++) stats[i] = new Rectangle(pad + i * (sw + gap), sy, sw, T.P(76));
int cy = sy + T.P(76) + T.P(14), bh = T.P(38);
int cwid = Math.Max(T.P(96), T.W(close.Text, T.Bold) + T.P(36));
close.SetBounds(W - pad - cwid, H - pad - bh, cwid, bh);
noteR = new Rectangle(pad, close.Top, close.Left - pad - T.P(12), bh);
card.SetBounds(pad, cy, W - 2 * pad, close.Top - T.P(14) - cy);
chart.SetBounds(T.P(14), T.P(14), card.Width - T.P(28), card.Height - T.P(24));
T.Mirror(this, range); T.Mirror(this, close, card);
}
async void Fetch(int n)
{
days = n;
int my = ++gen;
for (int i = 0; i < Spans.Length; i++) { range[i].Style = Spans[i] == n ? Btn.Kind.Primary : Btn.Kind.Secondary; range[i].Invalidate(); }
chart.SetMessage(L.T("Loading..."));
Invalidate();
var d = await Task.Run(() => load(n));
if (my != gen || IsDisposed) return;
var list = new List<HistoryChart.Day>();
var raw = Nyxo.Get(d, "days") as IEnumerable;
if (raw != null)
foreach (var o in raw)
{
var x = Nyxo.Obj(o);
DateTime date;
if (x == null || !DateTime.TryParseExact(Nyxo.Str(x, "date"), "yyyy-MM-dd", Nyxo.Inv, System.Globalization.DateTimeStyles.None, out date)) continue;
list.Add(new HistoryChart.Day
{
Date = date, Usd = Nyxo.Num(x, "usd"), Requests = Nyxo.Num(x, "requests"), In = Nyxo.Num(x, "inputTokens"),
Out = Nyxo.Num(x, "outputTokens"), Read = Nyxo.Num(x, "cacheReadTokens"), Write = Nyxo.Num(x, "cacheWriteTokens"),
});
}
if (list.Count == 0) { chart.SetMessage(L.T("Couldn't load the history. Check your connection and try again.")); Invalidate(); return; }
if (!DateTime.TryParseExact(Nyxo.Str(d, "since") ?? "", "yyyy-MM-dd", Nyxo.Inv, System.Globalization.DateTimeStyles.None, out since)) since = DateTime.MinValue;
chart.Set(list, Nyxo.Num(d, "dailyUsd"));
if (list.All(x => x.Usd <= 0 && x.Requests <= 0)) chart.SetMessage(L.T("No usage in this period yet."));
Invalidate();
}
// Today's spend from the live usage feed, so the last bar moves while Claude Code works.
DateTime refetchedFor;
public void SetToday(double usd)
{
var l = chart.Days;
if (l.Count == 0 || gen == 0) return;
if (DateTime.UtcNow.Date > l[l.Count - 1].Date) // a new UTC day began: reload once, not on every update
{
if (refetchedFor != DateTime.UtcNow.Date) { refetchedFor = DateTime.UtcNow.Date; Fetch(days); }
return;
}
if (l[l.Count - 1].Date != DateTime.UtcNow.Date || Math.Abs(l[l.Count - 1].Usd - usd) < 1e-9) return;
l[l.Count - 1].Usd = usd;
if (chart.Message == L.T("No usage in this period yet.") && usd > 0) chart.Set(l, chart.Daily);
chart.Changed(); Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var g = e.Graphics; T.Smooth(g); T.Begin(g, ClientSize.Width);
string sub = L.T("Spend per day for this passcode. Days run from 00:00 to 24:00 UTC.");
int sh = Math.Min(subR.Height, TextRenderer.MeasureText(g, sub, T.Small, new Size(subR.Width, 0), Wrap).Height);
T.Txt(g, sub, T.Small, new Rectangle(subR.X, subR.Y + (subR.Height - sh) / 2, subR.Width, sh + 1), T.Dim, Wrap);
var l = chart.Days;
bool have = l.Count > 0 && (chart.Message == null || chart.Message == L.T("No usage in this period yet."));
double total = have ? l.Sum(x => x.Usd) : 0, req = have ? l.Sum(x => x.Requests) : 0;
var top = have ? l.OrderByDescending(x => x.Usd).First() : null;
int active = have ? l.Count(x => x.Usd > 0) : 0;
Stat(g, stats[0], L.T("TOTAL"), have ? Nyxo.Usd(total) : "-", have ? L.F("last {0} days", days) : "");
Stat(g, stats[1], L.T("AVERAGE PER DAY"), have ? Nyxo.Usd(total / l.Count) : "-", have ? L.F("{0} of {1} days used", active, l.Count) : "");
Stat(g, stats[2], L.T("BUSIEST DAY"), have && top.Usd > 0 ? Nyxo.Usd(top.Usd) : "-", have && top.Usd > 0 ? L.Date(top.Date, false) : "");
Stat(g, stats[3], L.T("REQUESTS"), have ? req.ToString("#,0", Nyxo.Inv) : "-", "");
string note = L.F("Hover a bar for details. Kept for {0} days.", 90);
if (have && since > l[0].Date) note = L.F("Recorded from {0}. Kept for {1} days.", L.Date(since, false), 90);
T.Txt(g, note, T.Small, noteR, T.Faint, T.L);
}
static void Stat(Graphics g, Rectangle r, string label, string value, string sub)
{
var rf = new RectangleF(r.X + 0.5f, r.Y + 0.5f, r.Width - 1, r.Height - 1);
T.Fill(g, rf, T.P(12), T.Elev); T.Stroke(g, rf, T.P(12), T.Border);
int p = T.P(14), w = r.Width - 2 * p;
T.Txt(g, label, T.Tiny, new Rectangle(r.X + p, r.Y + T.P(10), w, T.P(16)), T.Faint, T.L);
T.Txt(g, value, T.Big, new Rectangle(r.X + p, r.Y + T.P(26), w, T.P(28)), T.Text, T.L);
if (sub != "") T.Txt(g, sub, T.Tiny, new Rectangle(r.X + p, r.Y + T.P(54), w, T.P(16)), T.Dim, T.L);
}
}
Buy.csشراء رمز مرور أو تجديده: الباقات وعنوان الدفع ورمز QR وحالة الطلب.688 سطر
// Buying a passcode, or renewing the signed-in one, from inside the app: pick a plan, pay in USDT on BNB Smart Chain
// (BEP20) and get the passcode. It uses the same checkout as nyxo.space. The open order is kept in %APPDATA%\Nyxo
// (encrypted), so closing this window or the app never loses a payment: the order shows again next time.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
class BuyForm : Form
{
enum Step { Plans, Pay, Done }
Step step = Step.Plans;
// The passcode to renew, its name and its usage (plans that fit it are listed); null when buying a new one.
string renewPass, renewLabel;
// Sent with a new order; reused when the same order is retried, so a lost response never opens a second order.
string orderKey, orderKeyFor;
readonly Dictionary<string, object> usage;
public string UsePasscode; // the new passcode, when "Use it" was clicked
public bool Renewed;
readonly RowList plans = new RowList { MaxRows = 4, RowH = 56 };
// Pay-as-you-go amounts are one row in the list; the slider under it picks the amount (as on nyxo.space).
readonly Steps slider = new Steps();
List<Dictionary<string, object>> balances;
RowList.Item payg;
readonly Field name = new Field("", false), addr = new Field("", false), code = new Field("", false);
readonly Btn ok = new Btn("", Btn.Kind.Primary), cancel = new Btn("", Btn.Kind.Secondary), other = new Btn("", Btn.Kind.Ghost);
readonly Btn copyAddr = new Btn("", Btn.Kind.Secondary) { Glyph = "\uE8C8" }, copyAmount = new Btn("", Btn.Kind.Secondary) { Glyph = "\uE8C8", IconOnly = true },
copyCode = new Btn("", Btn.Kind.Secondary) { Glyph = "\uE8C8" };
readonly ToolTip tip = new ToolTip();
List<Dictionary<string, object>> planList;
Dictionary<string, object> order;
string orderId, orderName, orderLabel, passcode;
bool orderRenew, refused, loading, failedPoll;
// A cancel asked for while a status check is still running waits for it, then goes ahead (cancelPending).
bool cancelPending, cancelling;
// The name the new passcode was saved under on this PC; null when it could not be saved (keptOrder).
string savedName; bool keptOrder;
Bitmap qr; string qrSrc;
string note = ""; Color noteColor = T.Dim;
long deadline;
int gen;
readonly Timer poll = new Timer { Interval = 4000 }, tick = new Timer { Interval = 1000 };
const TextFormatFlags Wrap = TextFormatFlags.Left | TextFormatFlags.Top | TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding;
const TextFormatFlags WrapC = TextFormatFlags.HorizontalCenter | TextFormatFlags.Top | TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding;
static readonly Regex NameOk = new Regex(@"^[\p{L}\p{N} ._-]{1,40}$");
public BuyForm(string renewPass, string renewLabel, Dictionary<string, object> usage)
{
this.renewPass = renewPass; this.renewLabel = renewLabel; this.usage = usage;
AutoScaleMode = AutoScaleMode.None; FormBorderStyle = FormBorderStyle.FixedDialog; DoubleBuffered = true;
MaximizeBox = MinimizeBox = false; ShowInTaskbar = false; ShowIcon = false; BackColor = T.Bg; Font = T.Body;
StartPosition = FormStartPosition.CenterParent;
if (T.Rtl) RightToLeft = RightToLeft.Yes;
ClientSize = new Size(T.P(540), T.P(560));
addr.Box.ReadOnly = code.Box.ReadOnly = true;
addr.Box.RightToLeft = code.Box.RightToLeft = RightToLeft.No;
name.Box.MaxLength = 40;
foreach (Control c in new Control[] { plans, slider, name, addr, code, copyAddr, copyAmount, copyCode, other, cancel, ok }) Controls.Add(c);
plans.Pick += it => { plans.Selected = plans.Items.IndexOf(it); plans.Invalidate(); slider.Active = it == payg; if (Plan(it) != null && Nyxo.Get(Plan(it), "soldOut") is bool && (bool)Nyxo.Get(Plan(it), "soldOut")) Note(L.T("This plan is sold out."), T.Warn); else Note("", T.Dim); };
slider.Changed += SliderMoved;
ok.Click += (s, e) => OkClick();
cancel.Click += (s, e) => CancelClick();
other.Click += (s, e) => { this.renewPass = null; LoadPlans("", T.Dim); };
copyAddr.Click += (s, e) => Copy(Nyxo.Str(order, "address"));
copyAmount.Click += (s, e) => Copy(Nyxo.Str(order, "amount"));
copyCode.Click += (s, e) => Copy(passcode, true);
poll.Tick += (s, e) => { if (step == Step.Pay && !loading) Fetch(); };
tick.Tick += (s, e) => { if (step == Step.Pay) Invalidate(); };
tick.Start();
var saved = Nyxo.LoadOrder();
if (saved != null && !string.IsNullOrEmpty(Nyxo.Str(saved, "id")))
{
orderId = Nyxo.Str(saved, "id"); orderName = Nyxo.Str(saved, "name"); orderLabel = Nyxo.Str(saved, "label");
orderRenew = Nyxo.Get(saved, "renew") is bool && (bool)Nyxo.Get(saved, "renew");
Go(Step.Pay);
Note(L.T("Loading your order..."), T.Dim);
Fetch();
}
else LoadPlans("", T.Dim);
}
protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); T.DarkTitle(Handle); }
protected override void OnFormClosed(FormClosedEventArgs e)
{
gen++; poll.Stop(); tick.Stop(); poll.Dispose(); tick.Dispose(); tip.Dispose();
if (qr != null) qr.Dispose();
base.OnFormClosed(e);
}
bool Renewing { get { return renewPass != null; } }
static Dictionary<string, object> Plan(RowList.Item it) { return it == null ? null : it.Tag as Dictionary<string, object>; }
void Note(string s, Color c) { note = s ?? ""; noteColor = c; Invalidate(); }
void Copy(string s, bool secret = false)
{
if (string.IsNullOrEmpty(s)) return;
try
{
// Keep the passcode out of Windows clipboard history and cloud clipboard sync.
var d = new DataObject();
d.SetData(DataFormats.UnicodeText, s);
d.SetData("CanIncludeInClipboardHistory", new MemoryStream(BitConverter.GetBytes(0)));
d.SetData("CanUploadToCloudClipboard", new MemoryStream(BitConverter.GetBytes(0)));
d.SetData("ExcludeClipboardContentFromMonitorProcessing", new MemoryStream(new byte[0]));
Clipboard.SetDataObject(d, true);
if (secret) ClearClipboardLater(s);
Note(secret ? L.T("Copied. It is removed from the clipboard in 1 minute.") : L.T("Copied."), T.Ok);
}
catch { Note(L.T("Couldn't copy. Select the text and copy it instead."), T.Warn); }
}
// A copied passcode is taken off the clipboard after a minute (or when the app closes), unless something else
// was copied since. The timer belongs to no window, so it still runs after this one closes.
static Timer clipTimer; static string clipText;
static void ClearClipboardLater(string s)
{
clipText = s;
if (clipTimer == null)
{
clipTimer = new Timer { Interval = 60000 };
clipTimer.Tick += (o, e) => { clipTimer.Stop(); ForgetClipboard(); };
}
clipTimer.Stop(); clipTimer.Start();
}
public static void ForgetClipboard()
{
string s = clipText;
clipText = null;
if (clipTimer != null) clipTimer.Stop();
if (s == null) return;
try { if (Clipboard.ContainsText(TextDataFormat.UnicodeText) && Clipboard.GetText(TextDataFormat.UnicodeText) == s) Clipboard.Clear(); } catch { }
}
static string Error(Nyxo.ApiResult r)
{
if (r.Status == 0) return L.T("Could not reach nyxo.space. Check your internet connection.");
if (r.Error != null) return L.Server(r.Error);
return L.T("Something went wrong. Please try again.");
}
// ---------- steps ----------
void Go(Step s)
{
step = s;
bool plansStep = s == Step.Plans, pay = s == Step.Pay, done = s == Step.Done;
bool newCode = done && !orderRenew && passcode != null;
plans.Visible = plansStep;
slider.Visible = plansStep && balances != null && balances.Count > 0;
name.Visible = plansStep && !Renewing;
// Room for the slider under four plan rows (new passcodes); renewals list fewer rows, so they fit as they are.
bool roomy = plansStep && !Renewing;
FitHeight(T.P(roomy ? 660 : 560));
other.Visible = plansStep && Renewing;
addr.Visible = copyAddr.Visible = copyAmount.Visible = pay;
code.Visible = copyCode.Visible = newCode;
cancel.Visible = !done || newCode;
if (pay) poll.Start(); else poll.Stop();
Relabel();
}
void Relabel()
{
Text = step == Step.Plans ? (Renewing ? L.T("Renew passcode") : L.T("Buy a passcode")) : step == Step.Pay ? L.T("Pay with USDT") : L.T("Nyxo Code");
name.SetCue(L.T("Name for the passcode"));
other.Text = L.T("Buy a new passcode instead");
copyAddr.Text = copyCode.Text = L.T("Copy");
copyAmount.Text = L.T("Copy amount"); copyAmount.AccessibleName = copyAmount.Text;
tip.SetToolTip(copyAmount, copyAmount.Text);
plans.AccessibleName = L.T("Plans");
addr.SetCue(L.T("Address")); code.SetCue(L.T("Passcode"));
bool newCode = step == Step.Done && !orderRenew && passcode != null;
if (step == Step.Plans) { ok.Text = planList == null && !loading ? L.T("Try again") : L.T("Continue"); ok.Style = Btn.Kind.Primary; cancel.Text = L.T("Close"); AcceptButton = ok; CancelButton = cancel; }
else if (step == Step.Pay) { ok.Text = L.T("Close"); ok.Style = Btn.Kind.Secondary; cancel.Text = cancelPending || cancelling ? L.T("Cancelling...") : L.T("Cancel order"); AcceptButton = null; CancelButton = ok; }
else if (newCode) { ok.Text = L.T("Use it"); ok.Style = Btn.Kind.Primary; cancel.Text = L.T("Close"); AcceptButton = ok; CancelButton = cancel; }
else { ok.Text = L.T("Close"); ok.Style = Btn.Kind.Primary; AcceptButton = ok; CancelButton = ok; }
ok.Invalidate();
PerformLayout(); Invalidate();
}
// ---------- plans ----------
bool Fits(Dictionary<string, object> p)
{
if (!Renewing) return true;
double daily = Nyxo.Num(usage, "dailyUsd"), pd = Nyxo.Num(p, "dailyUsd"), total = Nyxo.Num(usage, "totalUsd"), pt = Nyxo.Num(p, "totalUsd");
double bIn = Nyxo.Num(usage, "budgetInUsd"), bOut = Nyxo.Num(usage, "budgetOutUsd");
// Same rules as the site: a pay-as-you-go balance is topped up only with a pay-as-you-go amount, and so on.
if (total > 0 && daily <= 0 && bIn <= 0 && bOut <= 0) return pt > 0;
if (daily > 0 && Nyxo.Has(usage, "codeExpiresAt")) return pd > 0;
if (daily <= 0 && total <= 0 && bIn > 0 && bOut > 0) return pd <= 0 && pt <= 0;
return false;
}
void FitHeight(int h)
{
int d = h - ClientSize.Height;
if (d == 0) return;
ClientSize = new Size(ClientSize.Width, h);
if (Visible) Top = Math.Max(Screen.FromControl(this).WorkingArea.Top, Top - d / 2);
}
static string Whole(double v) { return "$" + v.ToString("0.##", Nyxo.Inv); }
static bool IsBalance(Dictionary<string, object> p) { return Nyxo.Num(p, "totalUsd") > 0 && Nyxo.Num(p, "dailyUsd") <= 0; }
string StepText(Dictionary<string, object> p)
{
return Whole(Nyxo.Num(p, "totalUsd")) + " · " + (SoldOut(p) ? L.T("Sold out") : Nyxo.Str(p, "price") + " USDT");
}
void SliderMoved()
{
if (payg == null || balances == null || slider.Index < 0 || slider.Index >= balances.Count) return;
var p = balances[slider.Index];
payg.Tag = p; payg.Title = PlanTitle(p); payg.Sub = PlanSub(p);
slider.Value = StepText(p); slider.ValueColor = SoldOut(p) ? T.Warn : T.Accent; slider.Active = true;
plans.SelectItem(plans.Items.IndexOf(payg));
if (step == Step.Plans && !loading) Note(SoldOut(p) ? L.T("This plan is sold out.") : "", SoldOut(p) ? T.Warn : T.Dim);
}
async void LoadPlans(string msg, Color c)
{
int my = ++gen;
plans.Enabled = true; name.Box.ReadOnly = false; Cursor = Cursors.Default;
orderId = null; order = null; passcode = null;
planList = null; loading = true; balances = null; payg = null;
plans.Set(new List<RowList.Item>());
Go(Step.Plans);
ok.Enabled = false;
Note(L.T("Loading plans..."), T.Dim);
var r = await Task.Run(() => Nyxo.Api("GET", "/plans?v=2", null));
if (my != gen || IsDisposed) return;
loading = false;
ok.Enabled = true;
var raw = r.Status == 200 ? Nyxo.Get(r.Body, "plans") as IEnumerable : null;
if (raw == null) { Relabel(); Note(Error(r.Status == 200 ? new Nyxo.ApiResult() { Status = 502 } : r), T.Danger); return; }
planList = raw.Cast<object>().Select(Nyxo.Obj).Where(p => p != null && Nyxo.Str(p, "id") != null).ToList();
var fit = planList.Where(Fits).ToList();
// New passcodes: monthly plans and pay-as-you-go only. Older plans stay available to renew passcodes that use them.
var items = fit.Where(p => !IsBalance(p) && (Renewing || Nyxo.Num(p, "dailyUsd") > 0))
.Select(p => new RowList.Item { Title = PlanTitle(p), Sub = PlanSub(p), Glyph = Nyxo.Num(p, "dailyUsd") > 0 ? "\uE787" : "\uE8C7", Tag = p }).ToList();
balances = fit.Where(IsBalance).OrderBy(p => Nyxo.Num(p, "totalUsd")).ToList();
if (balances.Count > 0)
{
int pick = balances.FindIndex(p => Nyxo.Num(p, "totalUsd") == 25 && !SoldOut(p));
if (pick < 0) pick = Math.Max(0, balances.FindIndex(p => !SoldOut(p)));
payg = new RowList.Item { Glyph = "\uE825", Tag = balances[pick] };
payg.Title = PlanTitle(balances[pick]); payg.Sub = PlanSub(balances[pick]);
items.Add(payg);
slider.Head = L.T("Choose your balance");
slider.MinText = Whole(Nyxo.Num(balances[0], "totalUsd")); slider.MaxText = Whole(Nyxo.Num(balances[balances.Count - 1], "totalUsd"));
slider.AccessibleName = L.T("Pay as you go");
slider.SetCount(balances.Count, pick);
slider.Value = StepText(balances[pick]); slider.ValueColor = SoldOut(balances[pick]) ? T.Warn : T.Accent;
}
plans.Set(items);
int first = items.FindIndex(it => !SoldOut(Plan(it)));
plans.Selected = first; plans.Invalidate();
slider.Active = first >= 0 && items[first] == payg;
Go(Step.Plans);
bool open = !(Nyxo.Get(r.Body, "checkout") is bool) || (bool)Nyxo.Get(r.Body, "checkout");
if (!open) { ok.Enabled = false; Note(L.T("Checkout is not available right now. Try again in a minute."), T.Warn); }
else if (items.Count == 0) { ok.Enabled = false; Note(Renewing ? L.T("This passcode can't be renewed in the app. Buy a new passcode instead.") : L.T("No plans are available right now."), T.Warn); }
else if (first < 0) { ok.Enabled = false; Note(L.T("All plans are sold out right now."), T.Warn); }
else Note(msg, c);
if (step == Step.Plans) { if (name.Visible) name.Box.Focus(); else plans.Focus(); }
}
static bool SoldOut(Dictionary<string, object> p) { return Nyxo.Get(p, "soldOut") is bool && (bool)Nyxo.Get(p, "soldOut"); }
string PlanTitle(Dictionary<string, object> p)
{
string t = (IsBalance(p) ? L.T("Pay as you go") : Nyxo.Str(p, "name")) + " - " + Nyxo.Str(p, "price") + " USDT";
if (Nyxo.Get(p, "best") is bool && (bool)Nyxo.Get(p, "best")) t += " - " + L.T("Best value");
return t;
}
string PlanSub(Dictionary<string, object> p)
{
if (SoldOut(p)) return L.T("Sold out");
double daily = Nyxo.Num(p, "dailyUsd");
int days = (int)Math.Max(1, Nyxo.Num(p, "days"));
double total = Nyxo.Num(p, "totalUsd");
if (total > 0) return Renewing ? L.F("Adds {0} to the balance, usable for 12 months", Nyxo.Usd(total)) : L.F("{0} balance, use it any time for 12 months", Nyxo.Usd(total));
if (daily > 0) return Renewing ? L.F("Adds {0} a day for {1} days", Nyxo.Usd(daily), days) : L.F("{0} a day for {1} days", Nyxo.Usd(daily), days);
string bIn = Nyxo.Usd(Nyxo.Num(p, "budgetInUsd")), bOut = Nyxo.Usd(Nyxo.Num(p, "budgetOutUsd"));
return Renewing ? L.F("Adds {0} input and {1} output budget", bIn, bOut) : L.F("{0} input and {1} output budget, never expires", bIn, bOut);
}
// ---------- buttons ----------
void OkClick()
{
if (step == Step.Plans)
{
if (planList == null) { if (!loading) LoadPlans("", T.Dim); return; }
Create();
}
else if (step == Step.Pay) Close();
else
{
if (!orderRenew && passcode != null) UsePasscode = passcode;
DialogResult = DialogResult.OK; Close();
}
}
void CancelClick()
{
if (step != Step.Pay) { Close(); return; }
if (orderId == null || cancelPending || cancelling) return;
if (Dlg.Ask(this, L.T("Cancel this order?"), L.T("Don't cancel if you already sent the payment: it would not be matched to this order."), null, L.T("Cancel order"), L.T("Keep order"), true) == null) return;
if (IsDisposed || step != Step.Pay || orderId == null) return;
if (loading)
{
// A status check is running: the cancel goes right after it.
cancelPending = true; cancel.Enabled = false;
Relabel();
return;
}
CancelOrder();
}
// Runs a cancel that waited for a status check, if the order is still open.
void RunPendingCancel()
{
if (!cancelPending) return;
cancelPending = false;
if (step == Step.Pay && orderId != null && !IsDisposed) CancelOrder();
else { cancel.Enabled = true; Relabel(); }
}
async void Create()
{
var p = Plan(plans.Selected >= 0 && plans.Selected < plans.Items.Count ? plans.Items[plans.Selected] : null);
if (p == null) { Note(L.T("Choose a plan."), T.Warn); plans.Focus(); return; }
if (SoldOut(p)) { Note(L.T("This plan is sold out."), T.Warn); return; }
string nm = Regex.Replace(name.Text.Trim(), @"\s+", " ");
if (!Renewing && !NameOk.IsMatch(nm))
{
Note(nm == "" ? L.T("Enter a name for the passcode, so you can tell it apart.") : L.T("Use 1 to 40 letters, numbers, spaces, dots, dashes or underscores."), T.Warn);
name.Box.Focus(); return;
}
int my = ++gen;
loading = true; ok.Enabled = false; other.Enabled = false; plans.Enabled = false; name.Box.ReadOnly = true; Cursor = Cursors.AppStarting;
Note(L.T("Creating your order..."), T.Dim);
var body = new Dictionary<string, object> { { "plan", Nyxo.Str(p, "id") }, { "name", Renewing ? "Renewal" : nm } };
if (Renewing) body["renew"] = renewPass;
string sig = Nyxo.Str(p, "id") + "\n" + nm + "\n" + (renewPass ?? "");
if (orderKey == null || orderKeyFor != sig) { orderKey = Guid.NewGuid().ToString("N"); orderKeyFor = sig; }
body["key"] = orderKey;
bool renewing = Renewing;
string newName = renewing ? renewLabel : nm, label = renewLabel;
var r = await Task.Run(() =>
{
Nyxo.ApiResult a;
for (int i = 0; ; i++)
{
a = Nyxo.Api("POST", "/orders?qr=png", body);
if (i >= 2 || !(a.Status == 0 || (a.Status >= 500 && a.Error == null))) break;
System.Threading.Thread.Sleep(2000);
}
// Kept right away, even when this window was closed meanwhile, so the order shows again next time.
if (a.Status == 200 && Nyxo.Str(a.Body, "id") != null)
Nyxo.SaveOrder(new Dictionary<string, object> { { "id", Nyxo.Str(a.Body, "id") }, { "name", newName ?? "" }, { "label", label ?? "" }, { "renew", renewing } });
return a;
});
if (my != gen || IsDisposed) return;
loading = false; ok.Enabled = true; other.Enabled = true; plans.Enabled = true; name.Box.ReadOnly = false; Cursor = Cursors.Default;
if (r.Status != 200 || Nyxo.Str(r.Body, "id") == null) { Note(Error(r), T.Danger); return; }
orderKey = null;
orderId = Nyxo.Str(r.Body, "id"); orderRenew = renewing; orderName = newName; orderLabel = label;
Note("", T.Dim);
ShowOrder(r.Body);
}
async void Fetch()
{
if (orderId == null) return;
int my = gen;
loading = true;
var id = orderId;
var r = await Task.Run(() => Nyxo.Api("GET", "/orders/" + Uri.EscapeDataString(id) + "?qr=png", null));
if (my != gen || IsDisposed) return;
loading = false;
if (r.Status == 404 && r.Error != null) { cancelPending = false; Nyxo.ClearOrder(); LoadPlans(L.T("Your last order is no longer available."), T.Warn); return; }
if (r.Status != 200 || r.Body == null) { failedPoll = true; if (order == null) Note(Error(r), T.Warn); Invalidate(); }
else { failedPoll = false; ShowOrder(r.Body); }
RunPendingCancel();
}
async void CancelOrder()
{
int my = ++gen;
loading = true; cancelling = true; cancel.Enabled = false;
Relabel();
var id = orderId;
var r = await Task.Run(() => Nyxo.Api("POST", "/orders/" + Uri.EscapeDataString(id) + "/cancel", null));
if (my != gen || IsDisposed) return;
loading = false; cancelling = false; cancel.Enabled = true;
Relabel();
if (r.Status == 404 && r.Error != null) { Nyxo.ClearOrder(); LoadPlans(L.T("Order cancelled."), T.Dim); return; }
if (r.Status != 200 || r.Body == null) { Note(Error(r), T.Danger); return; }
ShowOrder(r.Body);
}
void ShowOrder(Dictionary<string, object> o)
{
order = o;
string st = Nyxo.Str(o, "status");
if (st == "ready")
{
bool renewOrder = orderRenew || Nyxo.Get(o, "renew") is bool;
refused = renewOrder && Nyxo.Str(o, "refused") == "revoked";
keptOrder = false; savedName = null;
if (renewOrder && (Nyxo.Get(o, "renewed") is bool || refused)) { orderRenew = true; Renewed = !refused; }
else
{
// A renewal the server could not apply (the passcode expired or changed meanwhile) comes as a new passcode.
orderRenew = false;
passcode = Nyxo.Str(o, "passcode");
if (passcode != null) { savedName = SaveOnPc(passcode, orderName); keptOrder = savedName == null; code.Text = passcode; }
}
// The order is only forgotten once its passcode is safely saved; otherwise it can be reopened later.
if (!keptOrder) Nyxo.ClearOrder();
Go(Step.Done);
if (keptOrder) Note(L.T("Couldn't save the passcode on this PC. Copy it now and keep it safe."), T.Warn);
else Note(renewOrder && !orderRenew && passcode != null ? L.T("The passcode could not be renewed, so a new one was issued for this plan.") : "", T.Dim);
if (ok.CanFocus) ok.Focus();
return;
}
if (st == "expired")
{
Nyxo.ClearOrder();
string rec = Nyxo.Str(o, "received") ?? "0";
double got; double.TryParse(rec, System.Globalization.NumberStyles.Float, Nyxo.Inv, out got);
if (Nyxo.Get(o, "cancelled") is bool) LoadPlans(L.T("Order cancelled."), T.Dim);
else if (got > 0) LoadPlans(L.F("The order closed with {0} USDT received, short of the full amount. Contact support at nyxo.space with the order address: {1}", rec, Nyxo.Str(o, "address")), T.Warn);
else LoadPlans(L.T("The order expired before a payment arrived."), T.Dim);
return;
}
deadline = Nyxo.NowMs() + (long)Nyxo.Num(o, "expiresIn");
string src = Nyxo.Str(o, "qrPng");
if (src != null && src != qrSrc)
{
qrSrc = src;
if (qr != null) { qr.Dispose(); qr = null; }
try
{
int comma = src.IndexOf(',');
using (var ms = new MemoryStream(Convert.FromBase64String(src.Substring(comma + 1))))
using (var img = Image.FromStream(ms))
{
int s = T.P(184);
qr = new Bitmap(s, s);
using (var g = Graphics.FromImage(qr)) { g.InterpolationMode = InterpolationMode.NearestNeighbor; g.PixelOffsetMode = PixelOffsetMode.Half; g.DrawImage(img, 0, 0, s, s); }
}
}
catch { qr = null; }
}
addr.Text = Nyxo.Str(o, "address") ?? "";
if (step != Step.Pay) Go(Step.Pay); else { PerformLayout(); Invalidate(); }
}
// A passcode bought here works on this PC only, so it is saved right away; it is never lost if the window closes.
// Returns the name it is saved under (an existing entry's name if it was already saved), or null if it couldn't be saved.
static string SaveOnPc(string pass, string label)
{
try
{
string used = null;
Nyxo.EditSaved(saved =>
{
var have = saved.FirstOrDefault(s => Nyxo.Unprotect(s.Secret) == pass);
if (have != null) { used = have.Name; return false; }
used = Nyxo.UniqueName(saved, string.IsNullOrWhiteSpace(label) ? "Nyxo" : label.Trim(), null);
saved.Add(new Nyxo.Saved { Name = used, Secret = Nyxo.Protect(pass), LastUsed = 0 });
return true;
});
return used;
}
catch { return null; }
}
// ---------- layout and painting ----------
int pad { get { return T.P(24); } }
int Qs { get { return T.P(184); } }
protected override void OnLayout(LayoutEventArgs e)
{
base.OnLayout(e);
if (ok == null || other == null) return; // still being built
int W = ClientSize.Width, H = ClientSize.Height, p = pad, bh = T.P(38), by = H - p - bh;
int ow = Math.Max(T.P(96), T.W(ok.Text, T.Bold) + T.P(36)), cw = Math.Max(T.P(96), T.W(cancel.Text, T.Bold) + T.P(36));
ok.SetBounds(W - p - ow, by, ow, bh);
cancel.SetBounds(step == Step.Pay ? p : ok.Left - T.P(10) - cw, by, cw, bh);
int gw = T.W(other.Text, other.Font) + T.P(22 + 20);
other.SetBounds(p - T.P(8), by, Math.Min(gw, cancel.Left - p), bh);
plans.SetBounds(p, T.P(128), W - 2 * p, plans.Wanted);
slider.SetBounds(p, plans.Top + plans.Wanted + T.P(14), W - 2 * p, T.P(72));
name.SetBounds(p, NameTop + T.P(22), W - 2 * p, T.P(42));
int rx = p + Qs + T.P(22);
int aw = T.W((Nyxo.Str(order, "amount") ?? "") + " USDT", T.Big);
copyAmount.SetBounds(Math.Min(W - p - T.P(36), rx + aw + T.P(10)), T.P(110), T.P(36), T.P(32));
int cpw = T.W(copyAddr.Text, T.Bold) + T.P(22 + 32);
addr.SetBounds(p, T.P(314), W - 2 * p - cpw - T.P(8), T.P(42));
copyAddr.SetBounds(W - p - cpw, T.P(314), cpw, T.P(42));
code.SetBounds(p, T.P(236), W - 2 * p - cpw - T.P(8), T.P(42));
copyCode.SetBounds(W - p - cpw, T.P(236), cpw, T.P(42));
T.Mirror(this, ok, cancel, other, plans, slider, name, copyAmount, addr, copyAddr, code, copyCode);
}
// Top of the NAME label: under the slider when it shows, else where it always was (under four plan rows).
int NameTop { get { return slider.Visible ? T.P(128) + plans.Wanted + T.P(14 + 72 + 18) : T.P(388); } }
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var g = e.Graphics; T.Smooth(g); T.Begin(g, ClientSize.Width);
int W = ClientSize.Width, p = pad, w = W - 2 * p;
if (step == Step.Plans) PaintPlans(g, p, w);
else if (step == Step.Pay) PaintPay(g, p, w);
else PaintDone(g, p, w);
}
void NoteAt(Graphics g, Rectangle r, TextFormatFlags fl)
{
if (note == "") return;
int h = Math.Min(r.Height, TextRenderer.MeasureText(g, note, T.Small, new Size(r.Width, 0), fl).Height);
T.Txt(g, note, T.Small, new Rectangle(r.X, r.Y, r.Width, h + 1), noteColor, fl);
}
void PaintPlans(Graphics g, int p, int w)
{
T.Txt(g, Renewing ? L.F("Renew \"{0}\"", renewLabel ?? "") : L.T("Buy a passcode"), T.H2, new Rectangle(p, T.P(22), w, T.P(30)), T.Text, T.L);
string sub = Renewing
? (Nyxo.Num(usage, "dailyUsd") > 0 ? L.T("Adds the plan's daily allowance for 30 days from payment, on top of what you have. Pay in USDT (BEP20).")
: Nyxo.Num(usage, "totalUsd") > 0 ? L.T("Adds the amount to this passcode's balance once the payment arrives. Pay in USDT (BEP20).")
: L.T("Adds budget to this passcode once the payment arrives. Pay in USDT on BNB Smart Chain (BEP20)."))
: L.T("Pay in USDT on BNB Smart Chain (BEP20). The passcode works on one device and is saved on this PC.");
T.Txt(g, sub, T.Small, new Rectangle(p, T.P(58), w, T.P(44)), T.Dim, Wrap);
T.Txt(g, L.T("CHOOSE A PLAN"), T.Tiny, new Rectangle(p, T.P(106), w, T.P(16)), T.Faint, T.L);
if (!Renewing) T.Txt(g, L.T("NAME"), T.Tiny, new Rectangle(p, NameTop, w, T.P(16)), T.Faint, T.L);
int ny = Renewing ? (slider.Visible ? slider.Bottom : plans.Bottom) + T.P(16) : NameTop + T.P(76);
NoteAt(g, new Rectangle(p, ny, w, ok.Top - T.P(8) - ny), Wrap);
}
void PaintPay(Graphics g, int p, int w)
{
string amount = Nyxo.Str(order, "amount") ?? "-";
T.Txt(g, order == null ? L.T("Pay with USDT") : L.F("Pay {0} USDT", amount), T.H2, new Rectangle(p, T.P(22), w, T.P(30)), T.Text, T.L);
if (order != null)
{
string plan = Nyxo.Str(order, "planName") ?? "";
string sub = orderRenew ? L.F("{0} plan, renewing \"{1}\"", plan, orderLabel ?? "") : L.F("{0} plan, new passcode \"{1}\"", plan, orderName ?? "");
T.Txt(g, sub, T.Small, new Rectangle(p, T.P(56), w, T.P(20)), T.Dim, T.L);
}
var qrR = new Rectangle(p, T.P(92), Qs, Qs);
T.Fill(g, qrR, T.P(10), Color.White);
if (qr != null) T.Img(g, qr, qrR.Location);
else T.Txt(g, order == null ? L.T("Loading...") : "", T.Small, qrR, T.Faint, T.C);
int rx = qrR.Right + T.P(22), rw = W0 - rx - p;
T.Txt(g, L.T("AMOUNT"), T.Tiny, new Rectangle(rx, T.P(92), rw, T.P(16)), T.Faint, T.L);
T.Txt(g, amount + " USDT", T.Big, new Rectangle(rx, T.P(110), Math.Max(0, copyAmountLeft() - rx), T.P(32)), T.Text, T.L);
T.Txt(g, L.T("NETWORK"), T.Tiny, new Rectangle(rx, T.P(152), rw, T.P(16)), T.Faint, T.L);
T.Txt(g, "BNB Smart Chain (BEP20)", T.Small, new Rectangle(rx, T.P(170), rw, T.P(20)), T.Text, T.L);
T.Txt(g, L.T("TIME LEFT"), T.Tiny, new Rectangle(rx, T.P(204), rw, T.P(16)), T.Faint, T.L);
long left = Math.Max(0, deadline - Nyxo.NowMs());
string time = order == null ? "-" : (left / 60000).ToString(Nyxo.Inv) + ":" + (left / 1000 % 60).ToString("00", Nyxo.Inv);
T.Txt(g, time, T.Big, new Rectangle(rx, T.P(222), rw, T.P(32)), left < 120000 && order != null ? T.Warn : T.Text, T.L);
T.Txt(g, L.T("ADDRESS"), T.Tiny, new Rectangle(p, T.P(294), w, T.P(16)), T.Faint, T.L);
string st = Nyxo.Str(order, "status");
double got, amt;
double.TryParse(Nyxo.Str(order, "received") ?? "0", System.Globalization.NumberStyles.Float, Nyxo.Inv, out got);
double.TryParse(amount, System.Globalization.NumberStyles.Float, Nyxo.Inv, out amt);
string status; Color sc;
if (order == null) { status = ""; sc = T.Dim; }
else if (failedPoll) { status = L.T("Reconnecting..."); sc = T.Warn; }
else if (st == "confirming") { status = L.T("Payment received. Confirming on the network..."); sc = T.Ok; }
else if (Nyxo.Get(order, "closing") is bool) { status = L.T("Time is up. Checking for a last payment..."); sc = T.Dim; }
else if (got > 0) { status = L.F("Received {0} of {1} USDT. Send the remaining {2} USDT to the same address.", Fmt(got), amount, Fmt(Math.Max(0, amt - got))); sc = T.Warn; }
else { status = L.T("Waiting for your payment..."); sc = T.Accent; }
T.Txt(g, status, T.Bold, new Rectangle(p, T.P(368), w, T.P(42)), sc, Wrap);
T.Txt(g, L.T("Send exactly this amount of USDT on BNB Smart Chain (BEP20). Other tokens or networks can't be recovered."), T.Tiny, new Rectangle(p, T.P(414), w, T.P(34)), T.Faint, Wrap);
string n = note != "" ? note : L.T("You can close this window: the order stays open, and Nyxo Code shows it again next time.");
Color nc = note != "" ? noteColor : T.Dim;
T.Txt(g, n, T.Small, new Rectangle(p, T.P(452), w, ok.Top - T.P(8) - T.P(452)), nc, Wrap);
}
int W0 { get { return ClientSize.Width; } }
int copyAmountLeft() { return T.Rtl ? W0 - copyAmount.Right - T.P(6) : copyAmount.Left - T.P(6); }
static string Fmt(double v) { return v.ToString("0.######", Nyxo.Inv); }
void PaintDone(Graphics g, int p, int w)
{
bool fresh = !orderRenew && passcode != null;
var circle = new Rectangle((W0 - T.P(64)) / 2, T.P(36), T.P(64), T.P(64));
Color mark = refused ? T.Danger : T.Ok;
T.Fill(g, circle, T.P(32), T.Mix(mark, T.Bg, 0.75f));
using (var f = new Font(T.Glyph.FontFamily, T.Glyph.Size * 2f)) T.Txt(g, refused ? "\uE711" : "\uE73E", f, circle, mark, T.C);
string title = refused ? L.T("Not renewed") : orderRenew ? L.T("Passcode renewed") : fresh ? L.T("Your passcode is ready") : L.T("Payment complete");
T.Txt(g, title, T.H2, new Rectangle(p, T.P(116), w, T.P(30)), T.Text, T.C);
string sub = refused ? L.T("This passcode was revoked for usage-policy violations, so nothing was added. No refund is issued.")
: orderRenew ? L.F("The {0} plan was added to \"{1}\".", Nyxo.Str(order, "planName") ?? "", orderLabel ?? "")
: fresh && !keptOrder ? L.F("Saved on this PC as \"{0}\". It works on one device only.", savedName ?? "")
: fresh ? L.T("It works on one device only.")
: L.T("The passcode is no longer shown here. Contact support at nyxo.space.");
T.Txt(g, sub, T.Small, new Rectangle(p, T.P(156), w, T.P(44)), T.Dim, WrapC);
if (fresh) T.Txt(g, L.T("Keep a copy somewhere safe too."), T.Tiny, new Rectangle(p, T.P(288), w, T.P(18)), T.Faint, T.L);
NoteAt(g, new Rectangle(p, T.P(316), w, T.P(40)), WrapC);
}
}
// A stepped slider: a title and the picked value on top, the track, and the lowest and highest values under it.
// Mouse (click or drag), arrows, Page Up/Down and Home/End move it; arrows follow the reading direction.
class Steps : Painted
{
public string Head = "", Value = "", MinText = "", MaxText = "";
public Color ValueColor = T.Accent;
public event Action Changed;
int count = 1, index, hover = -1; bool drag, active = true;
public int Index { get { return index; } }
public bool Active { get { return active; } set { if (active != value) { active = value; Invalidate(); } } }
public Steps() { TabStop = true; Cursor = Cursors.Hand; }
public void SetCount(int n, int at) { count = Math.Max(1, n); index = Math.Max(0, Math.Min(count - 1, at)); Invalidate(); }
void SetIndex(int i)
{
i = Math.Max(0, Math.Min(count - 1, i));
if (i == index) { if (!active && Changed != null) Changed(); return; }
index = i; Invalidate();
AccessibilityNotifyClients(AccessibleEvents.ValueChange, -1);
if (Changed != null) Changed();
}
int X0 { get { return T.P(12); } }
int TrackY { get { return T.P(40); } }
float StepX(int i) { return count < 2 ? X0 : X0 + (Width - 2f * X0) * i / (count - 1); }
int At(int x)
{
if (T.Rtl) x = Width - 1 - x;
if (count < 2) return 0;
return (int)Math.Round((x - X0) * (count - 1) / Math.Max(1f, Width - 2f * X0));
}
protected override bool IsInputKey(Keys k)
{
k &= Keys.KeyCode;
return k == Keys.Left || k == Keys.Right || k == Keys.Up || k == Keys.Down || base.IsInputKey(k);
}
protected override void OnKeyDown(KeyEventArgs e)
{
int fwd = T.Rtl ? -1 : 1;
if (e.KeyCode == Keys.Right) SetIndex(index + fwd);
else if (e.KeyCode == Keys.Left) SetIndex(index - fwd);
else if (e.KeyCode == Keys.Up || e.KeyCode == Keys.PageUp) SetIndex(index + (e.KeyCode == Keys.PageUp ? 3 : 1));
else if (e.KeyCode == Keys.Down || e.KeyCode == Keys.PageDown) SetIndex(index - (e.KeyCode == Keys.PageDown ? 3 : 1));
else if (e.KeyCode == Keys.Home) SetIndex(0);
else if (e.KeyCode == Keys.End) SetIndex(count - 1);
else { base.OnKeyDown(e); return; }
e.Handled = e.SuppressKeyPress = true;
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
if (e.Button != MouseButtons.Left || !Enabled) return;
Focus(); drag = true; Capture = true; SetIndex(At(e.X));
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (drag) SetIndex(At(e.X));
int h = e.Y >= TrackY - T.P(16) && e.Y <= TrackY + T.P(16) ? At(e.X) : -1;
if (h != hover) { hover = h; Invalidate(); }
}
protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); drag = false; Capture = false; Invalidate(); }
protected override void OnMouseLeave(EventArgs e) { hover = -1; Invalidate(); base.OnMouseLeave(e); }
protected override void OnGotFocus(EventArgs e) { Invalidate(); base.OnGotFocus(e); }
protected override void OnLostFocus(EventArgs e) { Invalidate(); base.OnLostFocus(e); }
protected override AccessibleObject CreateAccessibilityInstance() { return new StepsAccess(this); }
class StepsAccess : Control.ControlAccessibleObject
{
readonly Steps owner;
public StepsAccess(Steps owner) : base(owner) { this.owner = owner; }
public override AccessibleRole Role { get { return AccessibleRole.Slider; } }
public override string Value { get { return owner.Value; } set { } }
public override string Description { get { return owner.Head; } }
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics; g.Clear(Under); T.Smooth(g); T.Begin(g, Width);
Color acc = active ? T.Accent : T.Mix(T.Accent, Under, 0.55f);
T.Txt(g, Head, T.Small, new Rectangle(0, 0, Width / 2, T.P(22)), T.Dim, T.L);
T.Txt(g, Value, T.Bold, new Rectangle(Width / 3, 0, Width - Width / 3, T.P(22)), active ? ValueColor : T.Dim, T.R);
float y = TrackY, th = T.P(6), x1 = StepX(count - 1), xi = StepX(index);
T.Fill(g, new RectangleF(X0, y - th / 2, x1 - X0, th), th / 2, T.Elev2);
T.Stroke(g, new RectangleF(X0, y - th / 2, x1 - X0, th), th / 2, T.Border);
if (xi > X0) T.Fill(g, new RectangleF(X0, y - th / 2, xi - X0, th), th / 2, acc);
float dot = T.P(4);
for (int i = 0; i < count; i++)
{
float x = StepX(i);
Color dc = i < index ? T.Mix(acc, T.OnAccent, 0.5f) : i == hover && i != index ? T.Text : T.Faint;
using (var b = new SolidBrush(dc)) g.FillEllipse(b, x - dot / 2, y + th / 2 + T.P(5), dot, dot);
}
float r = T.P(drag || hover == index ? 11 : 10);
if (Focused && ShowFocusCues) using (var b = new SolidBrush(Color.FromArgb(70, T.Accent))) g.FillEllipse(b, xi - r - T.P(4), y - r - T.P(4), 2 * r + T.P(8), 2 * r + T.P(8));
using (var b = new SolidBrush(acc)) g.FillEllipse(b, xi - r, y - r, 2 * r, 2 * r);
using (var b = new SolidBrush(T.Bg)) g.FillEllipse(b, xi - r * 0.42f, y - r * 0.42f, r * 0.84f, r * 0.84f);
T.Txt(g, MinText, T.Tiny, new Rectangle(0, T.P(54), Width / 2, T.P(16)), T.Faint, T.L);
T.Txt(g, MaxText, T.Tiny, new Rectangle(Width / 2, T.P(54), Width - Width / 2, T.P(16)), T.Faint, T.R);
if (!Enabled) using (var b = new SolidBrush(Color.FromArgb(120, Under))) g.FillRectangle(b, ClientRectangle);
}
}
Ui.csالألوان والخطوط وعناصر التحكم المخصّصة.867 سطر
// Theme and the small set of custom-drawn controls the app is built from.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;
static class T
{
public static float S = 1f;
public static int P(float v) { return (int)Math.Round(v * S); }
static Color Hex(string h) { return ColorTranslator.FromHtml("#" + h); }
public static readonly Color Bg = Hex("0b0d10"), Elev = Hex("12151a"), Elev2 = Hex("171b21"), Surface = Hex("1b2028"),
Border = Hex("262c36"), Text = Hex("e7ebf0"), Dim = Hex("9aa4b2"), Faint = Hex("6b7482"),
Accent = Hex("22d3ee"), Accent2 = Hex("22d3ee"), OnAccent = Hex("041a20"), Danger = Hex("ff6b6b"), Ok = Hex("3fb950"), Warn = Hex("e3b341");
public static Font Body, Small, Tiny, Bold, Title, H2, Big, Glyph, GlyphSmall;
public const TextFormatFlags L = TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPadding;
public const TextFormatFlags R = L | TextFormatFlags.Right;
public const TextFormatFlags C = L | TextFormatFlags.HorizontalCenter;
public static void Init()
{
// Z enlarges text and spacing together, on top of the Windows display scale.
const float Z = 1.2f;
using (var g = Graphics.FromHwnd(IntPtr.Zero)) S = g.DpiX / 96f * Z;
Body = new Font("Segoe UI", 10f * Z); Small = new Font("Segoe UI", 9f * Z); Tiny = new Font("Segoe UI", 8f * Z);
Bold = new Font("Segoe UI Semibold", 10f * Z); Title = new Font("Segoe UI Semibold", 19f * Z);
H2 = new Font("Segoe UI Semibold", 12.5f * Z); Big = new Font("Segoe UI Semibold", 16f * Z);
var fam = FontFamily.Families.Any(f => f.Name == "Segoe Fluent Icons") ? "Segoe Fluent Icons" : "Segoe MDL2 Assets";
Glyph = new Font(fam, 12f * Z); GlyphSmall = new Font(fam, 8f * Z);
}
public static Color Mix(Color a, Color b, float t)
{
return Color.FromArgb((int)(a.R + (b.R - a.R) * t), (int)(a.G + (b.G - a.G) * t), (int)(a.B + (b.B - a.B) * t));
}
public static GraphicsPath Round(RectangleF r, float rad)
{
var p = new GraphicsPath();
float d = Math.Min(rad * 2, Math.Min(r.Width, r.Height));
if (d <= 0) { p.AddRectangle(r); return p; }
p.AddArc(r.X, r.Y, d, d, 180, 90); p.AddArc(r.Right - d, r.Y, d, d, 270, 90);
p.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90); p.AddArc(r.X, r.Bottom - d, d, d, 90, 90);
p.CloseFigure();
return p;
}
public static void Fill(Graphics g, RectangleF r, float rad, Brush b) { using (var p = Round(r, rad)) g.FillPath(b, p); }
public static void Fill(Graphics g, RectangleF r, float rad, Color c) { using (var b = new SolidBrush(c)) Fill(g, r, rad, b); }
public static void Stroke(Graphics g, RectangleF r, float rad, Color c) { using (var p = Round(r, rad)) using (var pen = new Pen(c, Math.Max(1f, S))) g.DrawPath(pen, p); }
public static Brush Grad(RectangleF r, float lighten)
{
return new LinearGradientBrush(new RectangleF(r.X - 1, r.Y - 1, r.Width + 2, r.Height + 2), Mix(Accent, Color.White, lighten), Mix(Accent2, Color.White, lighten), LinearGradientMode.ForwardDiagonal);
}
// Right-to-left (Arabic): each OnPaint calls Begin, which mirrors shapes with a transform. Text goes through GDI, which
// ignores that transform, so Txt mirrors its rectangle and alignment itself; Img keeps pictures the right way round.
public static bool Rtl;
static Graphics mg; static int mw;
public static void Begin(Graphics g, int width)
{
mg = g; mw = width;
g.ResetTransform();
if (Rtl) { g.TranslateTransform(width, 0); g.ScaleTransform(-1, 1); }
}
static bool Mirrored(Graphics g) { return Rtl && g == mg; }
public static int X(int x) { return Rtl ? mw - x : x; }
public static void Txt(Graphics g, string s, Font f, Rectangle r, Color c, TextFormatFlags fl)
{
if (Mirrored(g))
{
r = new Rectangle(mw - r.Right, r.Y, r.Width, r.Height);
if ((fl & TextFormatFlags.Right) != 0) fl &= ~TextFormatFlags.Right;
else if ((fl & TextFormatFlags.HorizontalCenter) == 0) fl |= TextFormatFlags.Right;
fl |= TextFormatFlags.RightToLeft;
}
TextRenderer.DrawText(g, s, f, r, c, fl);
}
public static void Img(Graphics g, Image img, Point at)
{
if (img == null) return;
if (!Mirrored(g)) { g.DrawImage(img, at.X, at.Y, img.Width, img.Height); return; }
var state = g.Save();
g.ResetTransform();
g.DrawImage(img, mw - at.X - img.Width, at.Y, img.Width, img.Height);
g.Restore(state);
}
// After a layout pass has placed these children left-to-right, puts them in their mirrored place.
public static void Mirror(Control parent, params Control[] kids)
{
if (!Rtl) return;
int w = parent.ClientSize.Width;
foreach (var c in kids) c.Left = w - c.Left - c.Width;
}
public static int W(string s, Font f) { return TextRenderer.MeasureText(s, f, Size.Empty, TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix).Width; }
public static void Smooth(Graphics g) { g.SmoothingMode = SmoothingMode.AntiAlias; g.PixelOffsetMode = PixelOffsetMode.HighQuality; }
static Image logo; static readonly Dictionary<int, Bitmap> logos = new Dictionary<int, Bitmap>();
public static Bitmap Logo(int px)
{
Bitmap b;
if (logos.TryGetValue(px, out b)) return b;
try
{
if (logo == null) logo = Image.FromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream("nyxo.png"));
b = new Bitmap(px, px);
using (var g = Graphics.FromImage(b)) { g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.DrawImage(logo, 0, 0, px, px); }
}
catch { b = null; }
logos[px] = b;
return b;
}
[DllImport("dwmapi.dll")] static extern int DwmSetWindowAttribute(IntPtr h, int attr, ref int val, int size);
// Dark title bar (Windows 10 2004+ / 11) with the app background as caption color on Windows 11.
public static void DarkTitle(IntPtr h)
{
try
{
int on = 1;
if (DwmSetWindowAttribute(h, 20, ref on, 4) != 0) DwmSetWindowAttribute(h, 19, ref on, 4);
int c = Bg.R | (Bg.G << 8) | (Bg.B << 16);
DwmSetWindowAttribute(h, 35, ref c, 4);
}
catch { }
}
}
abstract class Painted : Control
{
protected Painted()
{
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.ResizeRedraw | ControlStyles.SupportsTransparentBackColor, true);
}
protected Color Under { get { return Parent != null ? Parent.BackColor : T.Bg; } }
protected override void OnEnabledChanged(EventArgs e) { base.OnEnabledChanged(e); Invalidate(); }
protected override void OnTextChanged(EventArgs e) { base.OnTextChanged(e); Invalidate(); UiaChanged(UiaApi.NameProp, AccessibleName ?? Text); }
// Screen readers: a native UI Automation provider per control, so every UIA client sees the real name, role and state
// (the MSAA AccessibleObjects below stay for MSAA clients).
UiaProvider uia;
protected virtual UiaProvider CreateUiaProvider() { return null; }
internal UiaProvider Uia { get { return uia; } }
protected override void WndProc(ref Message m)
{
if (m.Msg == UiaApi.WmGetObject && unchecked((int)(long)m.LParam) == UiaApi.RootObjectId)
{
if (uia == null || uia.Hwnd != Handle) uia = CreateUiaProvider();
if (uia != null) { m.Result = UiaApi.UiaReturnRawElementProvider(Handle, m.WParam, m.LParam, uia); return; }
}
else if (m.Msg == UiaApi.WmDestroy && uia != null)
{
try { UiaApi.UiaReturnRawElementProvider(Handle, IntPtr.Zero, IntPtr.Zero, null); } catch { }
uia = null;
}
base.WndProc(ref m);
}
protected void UiaChanged(int property, object value) { if (uia != null) UiaApi.PropertyChanged(uia, property, value); }
}
class Btn : Painted, IButtonControl
{
public enum Kind { Primary, Secondary, Ghost, Danger }
public Kind Style;
public string Glyph;
public bool IconOnly; // shows only the glyph (when space is short); the text stays the accessible name
bool hover, down;
public Btn(string text, Kind kind)
{
Text = text; Style = kind; Cursor = Cursors.Hand; TabStop = true;
Font = kind == Kind.Ghost ? T.Small : T.Bold;
}
public DialogResult DialogResult { get; set; }
public void NotifyDefault(bool value) { }
public void PerformClick() { if (Enabled && Visible) OnClick(EventArgs.Empty); }
protected override AccessibleObject CreateAccessibilityInstance() { return new ButtonAccess(this); }
class ButtonAccess : Control.ControlAccessibleObject
{
readonly Btn owner;
public ButtonAccess(Btn owner) : base(owner) { this.owner = owner; }
public override AccessibleRole Role { get { return AccessibleRole.PushButton; } }
public override string DefaultAction { get { return L.T("Press"); } }
public override void DoDefaultAction() { owner.PerformClick(); }
}
protected override UiaProvider CreateUiaProvider() { return new UiaProvider(this, UiaApi.Button, () => BeginInvoke((Action)PerformClick)); }
protected override void OnMouseEnter(EventArgs e) { hover = true; Invalidate(); base.OnMouseEnter(e); }
protected override void OnMouseLeave(EventArgs e) { hover = down = false; Invalidate(); base.OnMouseLeave(e); }
protected override void OnMouseDown(MouseEventArgs e) { down = true; Invalidate(); base.OnMouseDown(e); }
protected override void OnMouseUp(MouseEventArgs e) { down = false; Invalidate(); base.OnMouseUp(e); }
protected override void OnGotFocus(EventArgs e) { Invalidate(); base.OnGotFocus(e); }
protected override void OnLostFocus(EventArgs e) { Invalidate(); base.OnLostFocus(e); }
protected override void OnKeyUp(KeyEventArgs e) { if (e.KeyCode == Keys.Space) PerformClick(); base.OnKeyUp(e); }
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics; g.Clear(Under); T.Smooth(g); T.Begin(g, Width);
var r = new RectangleF(0.5f, 0.5f, Width - 1.5f, Height - 1.5f);
float rad = T.P(9);
Color fg = T.Text;
if (Style == Kind.Primary)
{
using (var b = T.Grad(r, !Enabled ? 0f : down ? -0.0f : hover ? 0.12f : 0f)) T.Fill(g, r, rad, b);
fg = T.OnAccent;
}
else if (Style == Kind.Danger) { T.Fill(g, r, rad, hover ? T.Mix(T.Danger, Color.White, 0.12f) : T.Danger); fg = Color.White; }
else if (Style == Kind.Secondary) { T.Fill(g, r, rad, hover ? T.Border : T.Surface); T.Stroke(g, r, rad, T.Border); }
else fg = hover ? T.Text : T.Accent;
if (Focused && ShowFocusCues && Style != Kind.Ghost) T.Stroke(g, new RectangleF(1.5f, 1.5f, Width - 3.5f, Height - 3.5f), rad - 1, T.Mix(T.Accent, Color.White, 0.4f));
if (!Enabled) fg = T.Mix(fg, Under, 0.45f);
var tr = ClientRectangle;
if (Glyph != null && IconOnly) T.Txt(g, Glyph, T.Glyph, tr, fg, T.C);
else if (Glyph != null)
{
int gw = T.P(22), tw = T.W(Text, Font), x0 = (Width - gw - tw) / 2;
T.Txt(g, Glyph, T.Glyph, new Rectangle(x0, 0, gw, Height), fg, T.L);
tr = new Rectangle(x0 + gw, 0, tw + T.P(4), Height);
T.Txt(g, Text, Font, tr, fg, T.L);
}
else T.Txt(g, Text, Font, tr, fg, Style == Kind.Ghost ? T.L : T.C);
if (!Enabled && Style == Kind.Primary) using (var b = new SolidBrush(Color.FromArgb(110, Under))) T.Fill(g, r, rad, b);
}
}
class Field : Painted
{
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern IntPtr SendMessage(IntPtr h, int msg, IntPtr w, string l);
public readonly TextBox Box;
string cue;
public Field(string cue, bool secret)
{
Cursor = Cursors.IBeam; this.cue = cue;
Box = new TextBox { BorderStyle = BorderStyle.None, BackColor = T.Elev2, ForeColor = T.Text, Font = T.Body, UseSystemPasswordChar = secret };
Box.HandleCreated += (s, e) => SendMessage(Box.Handle, 0x1501, (IntPtr)1, this.cue); // EM_SETCUEBANNER
Box.Enter += (s, e) => Invalidate();
Box.Leave += (s, e) => Invalidate();
if (cue != "") Box.AccessibleName = AccessibleName = cue;
TabStop = false; // the inner TextBox is the focusable control
Controls.Add(Box);
}
// Only a frame: hidden from the UIA control view so the text box sits directly in its group.
protected override UiaProvider CreateUiaProvider() { return new UiaProvider(this, UiaApi.Pane, null) { Frame = true }; }
public override string Text { get { return Box.Text; } set { Box.Text = value; } }
public void SetCue(string text)
{
cue = text; Box.AccessibleName = AccessibleName = text;
if (Box.IsHandleCreated) SendMessage(Box.Handle, 0x1501, (IntPtr)1, text);
}
protected override void OnClick(EventArgs e) { Box.Focus(); base.OnClick(e); }
protected override void OnLayout(LayoutEventArgs e)
{
base.OnLayout(e);
Box.SetBounds(T.P(13), (Height - Box.PreferredHeight) / 2, Width - T.P(26), Box.PreferredHeight);
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics; g.Clear(Under); T.Smooth(g);
var r = new RectangleF(0.5f, 0.5f, Width - 1.5f, Height - 1.5f);
T.Fill(g, r, T.P(9), T.Elev2);
T.Stroke(g, r, T.P(9), Box.Focused ? T.Accent : T.Border);
}
}
class Check : Painted
{
bool on, hover;
public event EventHandler Changed;
public Check(string text, bool value) { Text = text; on = value; Font = T.Small; Cursor = Cursors.Hand; TabStop = true; }
public bool Checked
{
get { return on; }
set
{
if (on == value) return;
on = value; Invalidate();
AccessibilityNotifyClients(AccessibleEvents.StateChange, -1);
UiaChanged(UiaApi.ToggleStateProp, on ? 1 : 0);
if (Changed != null) Changed(this, EventArgs.Empty);
}
}
protected override UiaProvider CreateUiaProvider() { return new CheckProvider(this); }
public int Wanted { get { return T.P(26) + T.W(Text, Font) + T.P(4); } }
protected override AccessibleObject CreateAccessibilityInstance() { return new CheckAccess(this); }
class CheckAccess : Control.ControlAccessibleObject
{
readonly Check owner;
public CheckAccess(Check owner) : base(owner) { this.owner = owner; }
public override AccessibleRole Role { get { return AccessibleRole.CheckButton; } }
public override AccessibleStates State { get { return base.State | (owner.Checked ? AccessibleStates.Checked : 0); } }
public override string DefaultAction { get { return L.T("Toggle"); } }
public override void DoDefaultAction() { if (owner.Enabled) owner.Checked = !owner.Checked; }
}
protected override void OnClick(EventArgs e) { Checked = !Checked; base.OnClick(e); }
protected override void OnKeyUp(KeyEventArgs e) { if (e.KeyCode == Keys.Space) Checked = !Checked; base.OnKeyUp(e); }
protected override void OnMouseEnter(EventArgs e) { hover = true; Invalidate(); base.OnMouseEnter(e); }
protected override void OnMouseLeave(EventArgs e) { hover = false; Invalidate(); base.OnMouseLeave(e); }
protected override void OnGotFocus(EventArgs e) { Invalidate(); base.OnGotFocus(e); }
protected override void OnLostFocus(EventArgs e) { Invalidate(); base.OnLostFocus(e); }
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics; g.Clear(Under); T.Smooth(g); T.Begin(g, Width);
int s = T.P(18);
var box = new RectangleF(0.5f, (Height - s) / 2 + 0.5f, s - 1, s - 1);
if (on)
{
using (var b = T.Grad(box, hover ? 0.12f : 0f)) T.Fill(g, box, T.P(5), b);
// The tick keeps its usual direction in right-to-left too (the transform would flip it).
Func<float, float> tx = f => T.Rtl ? box.X + s * (1 - f) : box.X + s * f;
using (var pen = new Pen(T.OnAccent, T.P(2)) { StartCap = LineCap.Round, EndCap = LineCap.Round, LineJoin = LineJoin.Round })
g.DrawLines(pen, new[] { new PointF(tx(0.25f), box.Y + s * 0.52f), new PointF(tx(0.43f), box.Y + s * 0.7f), new PointF(tx(0.76f), box.Y + s * 0.32f) });
}
else { T.Fill(g, box, T.P(5), T.Elev2); T.Stroke(g, box, T.P(5), hover || Focused ? T.Accent : T.Border); }
T.Txt(g, Text, Font, new Rectangle(s + T.P(8), 0, Width - s - T.P(8), Height), hover ? T.Text : T.Dim, T.L);
}
}
// Clickable rows (saved passcodes, recent folders) with hover, an optional remove (x) button and wheel scrolling.
class RowList : Painted
{
public class Item { public string Title, Sub, Glyph; public object Tag; }
public List<Item> Items = new List<Item>();
public int MaxRows = 3, Selected = -1;
public float RowH = 48, Gap = 6;
public bool PathSub;
public event Action<Item> Pick, Remove;
public event Action<Item, Point> Menu;
int hover = -1; internal int scroll, Gen; bool hoverX;
public RowList() { TabStop = true; }
public int Wanted { get { int n = Math.Min(Items.Count, MaxRows); return n == 0 ? 0 : n * T.P(RowH) + (n - 1) * T.P(Gap); } }
public void Set(List<Item> items)
{
Items = items; scroll = 0; hover = -1; Selected = -1; Gen++; Height = Wanted; Invalidate();
AccessibilityNotifyClients(AccessibleEvents.Reorder, -1);
if (Uia != null) UiaApi.ChildrenChanged(Uia);
}
public void SetMaxRows(int n) { MaxRows = Math.Max(1, n); scroll = Math.Max(0, Math.Min(scroll, Items.Count - MaxRows)); }
internal Rectangle Row(int i)
{
int sb = Items.Count > MaxRows ? T.P(10) : 0;
return new Rectangle(0, (i - scroll) * (T.P(RowH) + T.P(Gap)), Width - sb - 1, T.P(RowH));
}
// ---------- keyboard: Up/Down select, Enter/Space open, Delete removes, F2/menu key/Shift+F10 open the menu ----------
internal void SelectItem(int i)
{
if (Items.Count == 0) return;
Selected = Math.Max(0, Math.Min(i, Items.Count - 1));
if (Selected < scroll) scroll = Selected;
if (Selected >= scroll + MaxRows) scroll = Selected - MaxRows + 1;
Invalidate();
AccessibilityNotifyClients(AccessibleEvents.Selection, Selected);
UiaItemEvents();
}
// Tells UIA clients which row is selected and, while the list has focus, that the focus is on that row.
void UiaItemEvents()
{
var p = Uia as ListProvider;
if (p == null || Selected < 0 || Selected >= Items.Count) return;
var item = p.Item(Selected);
UiaApi.Raise(item, UiaApi.ElementSelectedEvent);
if (Focused) UiaApi.Raise(item, UiaApi.FocusChangedEvent);
}
internal void ActivateSelected() { if (Enabled && Selected >= 0 && Selected < Items.Count && Pick != null) Pick(Items[Selected]); }
protected override bool IsInputKey(Keys keyData)
{
Keys k = keyData & Keys.KeyCode;
return k == Keys.Up || k == Keys.Down || k == Keys.Enter || k == Keys.Space || k == Keys.Delete || k == Keys.Apps || k == Keys.F2 || base.IsInputKey(keyData);
}
protected override void OnGotFocus(EventArgs e)
{
if ((Selected < 0 || Selected >= Items.Count) && Items.Count > 0) SelectItem(0);
else UiaItemEvents();
Invalidate();
base.OnGotFocus(e);
}
protected override UiaProvider CreateUiaProvider() { return new ListProvider(this); }
protected override void OnLostFocus(EventArgs e) { Invalidate(); base.OnLostFocus(e); }
protected override void OnKeyDown(KeyEventArgs e)
{
if (!Enabled || Items.Count == 0) { base.OnKeyDown(e); return; }
int i = Selected < 0 || Selected >= Items.Count ? 0 : Selected;
if (e.KeyCode == Keys.Up) SelectItem(i - 1);
else if (e.KeyCode == Keys.Down) SelectItem(i + 1);
else if (e.KeyCode == Keys.Enter || e.KeyCode == Keys.Space) { SelectItem(i); ActivateSelected(); }
else if (e.KeyCode == Keys.Delete) { if (Remove != null) Remove(Items[i]); }
else if (e.KeyCode == Keys.Apps || e.KeyCode == Keys.F2 || (e.Shift && e.KeyCode == Keys.F10))
{
SelectItem(i);
if (Menu != null) { var row = Screen(Row(i)); Menu(Items[i], new Point(T.Rtl ? row.Right : row.Left, row.Bottom)); }
}
else { base.OnKeyDown(e); return; }
e.Handled = e.SuppressKeyPress = true;
}
// ---------- screen readers: the list and its rows ----------
protected override AccessibleObject CreateAccessibilityInstance() { return new ListAccess(this); }
class ListAccess : Control.ControlAccessibleObject
{
readonly RowList owner;
public ListAccess(RowList owner) : base(owner) { this.owner = owner; }
public override AccessibleRole Role { get { return AccessibleRole.List; } }
public override int GetChildCount() { return owner.Items.Count; }
public override AccessibleObject GetChild(int index) { return index < 0 || index >= owner.Items.Count ? null : new RowAccess(owner, index); }
}
class RowAccess : AccessibleObject
{
readonly RowList owner;
readonly int index;
public RowAccess(RowList owner, int index) { this.owner = owner; this.index = index; }
public override AccessibleObject Parent { get { return owner.AccessibilityObject; } }
public override string Name
{
get { return index >= owner.Items.Count ? "" : owner.Items[index].Title + (string.IsNullOrEmpty(owner.Items[index].Sub) ? "" : ". " + owner.Items[index].Sub); }
set { }
}
public override AccessibleRole Role { get { return AccessibleRole.ListItem; } }
public override Rectangle Bounds { get { return owner.RectangleToScreen(owner.Screen(owner.Row(index))); } }
public override AccessibleStates State
{
get
{
var state = AccessibleStates.Selectable | AccessibleStates.Focusable;
if (!owner.Enabled) state |= AccessibleStates.Unavailable;
if (index < owner.scroll || index >= owner.scroll + owner.MaxRows) state |= AccessibleStates.Offscreen;
if (index == owner.Selected)
{
state |= AccessibleStates.Selected;
if (owner.Focused) state |= AccessibleStates.Focused;
}
return state;
}
}
public override string DefaultAction { get { return L.T("Open"); } }
public override void Select(AccessibleSelection flags) { owner.SelectItem(index); owner.Focus(); }
public override void DoDefaultAction() { owner.SelectItem(index); owner.ActivateSelected(); }
}
Rectangle X(Rectangle r) { int s = T.P(28); return new Rectangle(r.Right - s - T.P(10), r.Y + (r.Height - s) / 2, s, s); }
// Rows are laid out left-to-right and painted mirrored in right-to-left, so mouse points are mirrored back.
Point Logical(Point p) { return T.Rtl ? new Point(Width - 1 - p.X, p.Y) : p; }
internal Rectangle Screen(Rectangle r) { return T.Rtl ? new Rectangle(Width - r.Right, r.Y, r.Width, r.Height) : r; }
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics; g.Clear(Under); T.Smooth(g); T.Begin(g, Width);
for (int i = scroll; i < Math.Min(Items.Count, scroll + MaxRows); i++)
{
var it = Items[i]; var r = Row(i); bool hov = i == hover && Enabled;
var rf = new RectangleF(r.X + 0.5f, r.Y + 0.5f, r.Width - 1, r.Height - 1);
T.Fill(g, rf, T.P(10), hov ? T.Surface : T.Elev2);
T.Stroke(g, rf, T.P(10), i == Selected ? T.Accent : hov ? T.Mix(T.Border, T.Text, 0.15f) : T.Border);
if (i == Selected && Focused && ShowFocusCues) T.Stroke(g, new RectangleF(rf.X + 1.5f, rf.Y + 1.5f, rf.Width - 3, rf.Height - 3), T.P(10) - 1, T.Mix(T.Accent, Color.White, 0.4f));
int x = r.X + T.P(14);
if (it.Glyph != null)
{
var gr = new Rectangle(x, r.Y + (r.Height - T.P(30)) / 2, T.P(30), T.P(30));
T.Fill(g, gr, T.P(8), T.Mix(T.Accent, T.Elev2, 0.82f));
T.Txt(g, it.Glyph, T.Glyph, gr, T.Accent, T.C);
x += T.P(42);
}
int right = r.Right - T.P(12) - (Remove != null ? T.P(32) : 0);
if (string.IsNullOrEmpty(it.Sub)) T.Txt(g, it.Title, T.Bold, new Rectangle(x, r.Y, right - x, r.Height), T.Text, T.L);
else
{
int top = r.Y + (r.Height - T.P(38)) / 2;
T.Txt(g, it.Title, T.Bold, new Rectangle(x, top, right - x, T.P(20)), T.Text, T.L);
T.Txt(g, it.Sub, T.Small, new Rectangle(x, top + T.P(20), right - x, T.P(18)), T.Faint, PathSub ? (T.L & ~TextFormatFlags.EndEllipsis) | TextFormatFlags.PathEllipsis : T.L);
}
if (hov && Remove != null)
{
var xr = X(r);
if (hoverX) T.Fill(g, xr, T.P(7), T.Mix(T.Danger, T.Surface, 0.8f));
T.Txt(g, "\uE711", T.GlyphSmall, xr, hoverX ? T.Danger : T.Dim, T.C);
}
}
if (Items.Count > MaxRows)
{
float track = Height, thumb = track * MaxRows / Items.Count, y = (track - thumb) * scroll / (Items.Count - MaxRows);
T.Fill(g, new RectangleF(Width - T.P(4), 0, T.P(4), track), T.P(2), T.Elev2);
T.Fill(g, new RectangleF(Width - T.P(4), y, T.P(4), thumb), T.P(2), T.Border);
}
if (!Enabled) using (var b = new SolidBrush(Color.FromArgb(120, Under))) g.FillRectangle(b, ClientRectangle);
}
protected override void OnMouseMove(MouseEventArgs e)
{
int h = -1; bool hx = false; var at = Logical(e.Location);
for (int i = scroll; i < Math.Min(Items.Count, scroll + MaxRows); i++)
if (Row(i).Contains(at)) { h = i; hx = Remove != null && X(Row(i)).Contains(at); }
if (h != hover || hx != hoverX) { hover = h; hoverX = hx; Invalidate(); }
Cursor = h >= 0 ? Cursors.Hand : Cursors.Default;
base.OnMouseMove(e);
}
protected override void OnMouseLeave(EventArgs e) { hover = -1; hoverX = false; Invalidate(); base.OnMouseLeave(e); }
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
if (!Enabled || hover < 0 || hover >= Items.Count) return;
var it = Items[hover];
if (e.Button == MouseButtons.Left) { if (hoverX) { if (Remove != null) Remove(it); } else if (Pick != null) Pick(it); }
else if (e.Button == MouseButtons.Right && Menu != null) Menu(it, e.Location);
}
protected override void OnMouseWheel(MouseEventArgs e)
{
base.OnMouseWheel(e);
if (Items.Count <= MaxRows) return;
scroll = Math.Max(0, Math.Min(Items.Count - MaxRows, scroll - Math.Sign(e.Delta)));
hover = -1; Invalidate();
}
}
class Card : Panel
{
public Card()
{
BackColor = T.Elev;
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true);
}
protected override void OnPaintBackground(PaintEventArgs e)
{
var g = e.Graphics; g.Clear(Parent != null ? Parent.BackColor : T.Bg); T.Smooth(g);
var r = new RectangleF(0.5f, 0.5f, Width - 1.5f, Height - 1.5f);
T.Fill(g, r, T.P(14), BackColor); T.Stroke(g, r, T.P(14), T.Border);
}
}
class DarkColors : ProfessionalColorTable
{
public override Color ToolStripDropDownBackground { get { return T.Elev2; } }
public override Color MenuBorder { get { return T.Border; } }
public override Color MenuItemBorder { get { return T.Border; } }
public override Color MenuItemSelected { get { return T.Surface; } }
public override Color MenuItemSelectedGradientBegin { get { return T.Surface; } }
public override Color MenuItemSelectedGradientEnd { get { return T.Surface; } }
public override Color ImageMarginGradientBegin { get { return T.Elev2; } }
public override Color ImageMarginGradientMiddle { get { return T.Elev2; } }
public override Color ImageMarginGradientEnd { get { return T.Elev2; } }
public override Color SeparatorDark { get { return T.Border; } }
public override Color SeparatorLight { get { return T.Elev2; } }
}
// A dark message / question / text-input dialog. Returns null when cancelled, else the typed text ("" without input).
class Dlg : Form
{
Field input;
public static string Ask(IWin32Window owner, string title, string message, string inputValue, string ok, string cancel, bool danger)
{
using (var d = new Dlg(owner, title, message, inputValue, ok, cancel, danger))
{
if (d.ShowDialog(owner) != DialogResult.OK) return null;
return d.input != null ? d.input.Text.Trim() : "";
}
}
Dlg(IWin32Window owner, string title, string message, string inputValue, string ok, string cancel, bool danger)
{
Text = title; AutoScaleMode = AutoScaleMode.None; FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = MinimizeBox = false; ShowInTaskbar = false; ShowIcon = false; BackColor = T.Bg; Font = T.Body;
StartPosition = FormStartPosition.CenterParent;
int W = T.P(430), pad = T.P(24);
const TextFormatFlags flags = TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix;
var size = TextRenderer.MeasureText(message, T.Body, new Size(W - 2 * pad, 0), flags);
// A long message (e.g. release notes) scrolls instead of making the dialog taller than ~80% of the screen.
Rectangle wa;
try { var oc = owner as Control; wa = oc != null && oc.IsHandleCreated ? Screen.FromControl(oc).WorkingArea : Screen.FromPoint(Cursor.Position).WorkingArea; }
catch { wa = Screen.PrimaryScreen.WorkingArea; }
int chrome = SystemInformation.CaptionHeight + 2 * SystemInformation.FixedFrameBorderSize.Height + T.P(8);
int fixedH = pad + T.P(16) + (inputValue != null ? T.P(40 + 18) : 0) + T.P(38) + pad;
int maxBody = Math.Max(T.P(80), wa.Height * 8 / 10 - chrome - fixedH);
int bodyH = size.Height + T.P(4);
if (T.Rtl) RightToLeft = RightToLeft.Yes; // RTL already right-aligns TopLeft text
Control body;
if (bodyH <= maxBody)
body = new Label { Text = message, ForeColor = T.Text, BackColor = T.Bg, Font = T.Body, AutoSize = false, UseMnemonic = false, Bounds = new Rectangle(pad, pad, W - 2 * pad, bodyH) };
else
{
int lw = W - 2 * pad - SystemInformation.VerticalScrollBarWidth - T.P(4);
int lh = TextRenderer.MeasureText(message, T.Body, new Size(lw, 0), flags).Height + T.P(4);
var lbl = new Label { Text = message, ForeColor = T.Text, BackColor = T.Bg, Font = T.Body, AutoSize = false, UseMnemonic = false, Bounds = new Rectangle(0, 0, lw, lh) };
var scroll = new Panel { AutoScroll = true, BackColor = T.Bg, Bounds = new Rectangle(pad, pad, W - 2 * pad, maxBody) };
scroll.Controls.Add(lbl);
body = scroll;
}
Controls.Add(body);
int y = body.Bottom + T.P(16);
if (inputValue != null)
{
input = new Field("", false) { Bounds = new Rectangle(pad, y, W - 2 * pad, T.P(40)) };
input.Box.MaxLength = 60;
input.Text = inputValue;
Controls.Add(input);
y += T.P(40) + T.P(18);
}
var okB = new Btn(ok, danger ? Btn.Kind.Danger : Btn.Kind.Primary);
int bw = Math.Max(T.P(96), T.W(ok, T.Bold) + T.P(36));
okB.Bounds = new Rectangle(W - pad - bw, y, bw, T.P(38));
okB.Click += (s, e) => { DialogResult = DialogResult.OK; Close(); };
AcceptButton = okB;
if (cancel != null)
{
var cb = new Btn(cancel, Btn.Kind.Secondary);
int cw = Math.Max(T.P(96), T.W(cancel, T.Bold) + T.P(36));
cb.Bounds = new Rectangle(okB.Left - T.P(10) - cw, y, cw, T.P(38));
cb.Click += (s, e) => { DialogResult = DialogResult.Cancel; Close(); };
CancelButton = cb;
Controls.Add(cb);
}
else CancelButton = okB;
Controls.Add(okB);
ClientSize = new Size(W, y + T.P(38) + pad);
foreach (Control c in Controls) if (c is Btn) T.Mirror(this, c);
if (owner0 == null) StartPosition = FormStartPosition.CenterScreen;
}
static Form owner0 { get { return Form.ActiveForm; } }
protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); T.DarkTitle(Handle); }
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
if (input != null) { input.Box.Focus(); input.Box.SelectAll(); }
}
}
// ---------- UI Automation: native providers for the custom-drawn controls ----------
// Each Painted control answers WM_GETOBJECT(UiaRootObjectId) with one of these; the window itself (bounds, focus,
// enabled) comes from the host HWND provider. Calls arrive on the UI thread; anything else is marshalled to it.
static class UiaApi
{
public const int WmGetObject = 0x3D, WmDestroy = 0x02, RootObjectId = -25, AppendRuntimeId = 3;
public const int Button = 50000, CheckBox = 50002, ListItem = 50007, List = 50008, Text = 50020, Pane = 50033;
public const int InvokePattern = 10000, SelectionPattern = 10001, SelectionItemPattern = 10010, TogglePattern = 10015;
public const int ControlTypeProp = 30003, NameProp = 30005, HasKeyboardFocusProp = 30008, IsKeyboardFocusableProp = 30009, IsEnabledProp = 30010,
AutomationIdProp = 30011, IsControlElementProp = 30016, IsContentElementProp = 30017, IsOffscreenProp = 30022, FrameworkIdProp = 30024,
IsSelectedProp = 30079, ToggleStateProp = 30086, PositionInSetProp = 30152, SizeOfSetProp = 30153;
public const int FocusChangedEvent = 20005, ElementSelectedEvent = 20012;
const int ElementNotAvailable = unchecked((int)0x80040201);
[DllImport("UIAutomationCore.dll")] public static extern IntPtr UiaReturnRawElementProvider(IntPtr hwnd, IntPtr wParam, IntPtr lParam, IRawElementProviderSimple el);
[DllImport("UIAutomationCore.dll")] public static extern int UiaHostProviderFromHwnd(IntPtr hwnd, out IRawElementProviderSimple provider);
[DllImport("UIAutomationCore.dll")] static extern int UiaRaiseAutomationPropertyChangedEvent(IRawElementProviderSimple el, int id, [MarshalAs(UnmanagedType.Struct)] object oldValue, [MarshalAs(UnmanagedType.Struct)] object newValue);
[DllImport("UIAutomationCore.dll")] static extern int UiaRaiseAutomationEvent(IRawElementProviderSimple el, int id);
[DllImport("UIAutomationCore.dll")] static extern int UiaRaiseStructureChangedEvent(IRawElementProviderSimple el, int type, int[] runtimeId, int runtimeIdLen);
[DllImport("UIAutomationCore.dll")] [return: MarshalAs(UnmanagedType.Bool)] static extern bool UiaClientsAreListening();
static bool Listening() { try { return UiaClientsAreListening(); } catch { return false; } }
public static void PropertyChanged(IRawElementProviderSimple el, int id, object value) { if (Listening()) try { UiaRaiseAutomationPropertyChangedEvent(el, id, null, value); } catch { } }
public static void Raise(IRawElementProviderSimple el, int id) { if (Listening()) try { UiaRaiseAutomationEvent(el, id); } catch { } }
public static void ChildrenChanged(IRawElementProviderSimple el) { if (Listening()) try { UiaRaiseStructureChangedEvent(el, 1, null, 0); } catch { } } // ChildrenInvalidated
public static T OnUi<T>(Control c, Func<T> f)
{
if (c.IsDisposed || !c.IsHandleCreated) throw new COMException("Element not available", ElementNotAvailable);
return c.InvokeRequired ? (T)c.Invoke(f) : f();
}
public static void OnUi(Control c, Action a) { OnUi<object>(c, () => { a(); return null; }); }
public static COMException Gone() { return new COMException("Element not available", ElementNotAvailable); }
}
[StructLayout(LayoutKind.Sequential)] public struct UiaRect { public double Left, Top, Width, Height; }
[ComImport, Guid("d6dd68d1-86fd-4332-8666-9abedea2d24c"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IRawElementProviderSimple
{
int ProviderOptions { get; }
[return: MarshalAs(UnmanagedType.IUnknown)] object GetPatternProvider(int patternId);
object GetPropertyValue(int propertyId);
IRawElementProviderSimple HostRawElementProvider { get; }
}
[ComImport, Guid("f7063da8-8359-439c-9297-bbc5299a7d87"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IRawElementProviderFragment
{
IRawElementProviderFragment Navigate(int direction);
[return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_I4)] int[] GetRuntimeId();
UiaRect BoundingRectangle { get; }
[return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_UNKNOWN)] IRawElementProviderSimple[] GetEmbeddedFragmentRoots();
void SetFocus();
IRawElementProviderFragmentRoot FragmentRoot { get; }
}
[ComImport, Guid("620ce2a5-ab8f-40a9-86cb-de3c75599b58"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IRawElementProviderFragmentRoot
{
IRawElementProviderFragment ElementProviderFromPoint(double x, double y);
IRawElementProviderFragment GetFocus();
}
[ComImport, Guid("54fcb24b-e18e-47a2-b4d3-eccbe77599a2"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IInvokeProvider { void Invoke(); }
[ComImport, Guid("56d00bd0-c4f4-433c-a836-1a52a57e0892"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IToggleProvider { void Toggle(); int ToggleState { get; } }
[ComImport, Guid("fb8b03af-3bdf-48d4-bd36-1a65793be168"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ISelectionProvider
{
[return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_UNKNOWN)] IRawElementProviderSimple[] GetSelection();
bool CanSelectMultiple { [return: MarshalAs(UnmanagedType.Bool)] get; }
bool IsSelectionRequired { [return: MarshalAs(UnmanagedType.Bool)] get; }
}
[ComImport, Guid("2acad808-b2d4-452d-a407-91ff1ad167b2"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ISelectionItemProvider
{
void Select(); void AddToSelection(); void RemoveFromSelection();
bool IsSelected { [return: MarshalAs(UnmanagedType.Bool)] get; }
IRawElementProviderSimple SelectionContainer { get; }
}
// A whole control: its name and role, plus Invoke when it has an action. Frame = a pure container, hidden from the control view.
[ComVisible(true)]
public class UiaProvider : IRawElementProviderSimple, IInvokeProvider
{
internal readonly Control Owner;
internal readonly IntPtr Hwnd;
readonly int type;
readonly Action invoke;
internal bool Frame;
internal UiaProvider(Control owner, int controlType, Action invoke) { Owner = owner; Hwnd = owner.Handle; type = controlType; this.invoke = invoke; }
public int ProviderOptions { get { return 2; } } // ServerSideProvider
public object GetPatternProvider(int id) { return id == UiaApi.InvokePattern && invoke != null ? this : Pattern(id); }
internal virtual object Pattern(int id) { return null; }
public object GetPropertyValue(int id) { return UiaApi.OnUi(Owner, () => Property(id)); }
internal virtual object Property(int id)
{
switch (id)
{
case UiaApi.ControlTypeProp: return type;
case UiaApi.NameProp: return Frame ? null : Owner.AccessibleName ?? Owner.Text;
case UiaApi.FrameworkIdProp: return "WinForm";
case UiaApi.IsKeyboardFocusableProp: return Owner.CanSelect && Owner.TabStop;
case UiaApi.IsControlElementProp: case UiaApi.IsContentElementProp: return !Frame;
}
return null;
}
public IRawElementProviderSimple HostRawElementProvider
{
get { IRawElementProviderSimple host; return UiaApi.UiaHostProviderFromHwnd(Hwnd, out host) == 0 ? host : null; }
}
public void Invoke()
{
UiaApi.OnUi(Owner, () => { if (!Owner.Enabled || !Owner.Visible) throw new InvalidOperationException(); invoke(); });
}
}
[ComVisible(true)]
public class CheckProvider : UiaProvider, IToggleProvider
{
readonly Check check;
internal CheckProvider(Check owner) : base(owner, UiaApi.CheckBox, null) { check = owner; }
internal override object Pattern(int id) { return id == UiaApi.TogglePattern ? this : null; }
public void Toggle() { UiaApi.OnUi(check, () => { if (!check.Enabled) throw new InvalidOperationException(); check.Checked = !check.Checked; }); }
public int ToggleState { get { return UiaApi.OnUi(check, () => check.Checked ? 1 : 0); } }
}
// The list (saved passcodes, recent folders) with each row as a selectable, invokable list item.
[ComVisible(true)]
public class ListProvider : UiaProvider, IRawElementProviderFragment, IRawElementProviderFragmentRoot, ISelectionProvider
{
internal readonly RowList List;
internal ListProvider(RowList owner) : base(owner, UiaApi.List, null) { List = owner; }
internal ListItemProvider Item(int i) { return new ListItemProvider(this, i, List.Gen); }
internal override object Pattern(int id) { return id == UiaApi.SelectionPattern ? this : null; }
internal override object Property(int id) { return id == UiaApi.IsKeyboardFocusableProp ? (object)(List.CanSelect && List.Items.Count > 0) : base.Property(id); }
public IRawElementProviderFragment Navigate(int direction)
{
return UiaApi.OnUi(List, () =>
{
int n = List.Items.Count;
if (n == 0) return null;
return direction == 3 ? Item(0) : direction == 4 ? Item(n - 1) : (IRawElementProviderFragment)null; // FirstChild, LastChild; the HWND tree does the rest
});
}
public int[] GetRuntimeId() { return null; }
public UiaRect BoundingRectangle { get { return new UiaRect(); } }
public IRawElementProviderSimple[] GetEmbeddedFragmentRoots() { return null; }
public void SetFocus() { UiaApi.OnUi(List, () => List.Focus()); }
public IRawElementProviderFragmentRoot FragmentRoot { get { return this; } }
public IRawElementProviderFragment ElementProviderFromPoint(double x, double y)
{
return UiaApi.OnUi(List, () =>
{
var p = List.PointToClient(new Point((int)x, (int)y));
for (int i = List.scroll; i < Math.Min(List.Items.Count, List.scroll + List.MaxRows); i++)
if (List.Screen(List.Row(i)).Contains(p)) return (IRawElementProviderFragment)Item(i);
return this;
});
}
public IRawElementProviderFragment GetFocus()
{
return UiaApi.OnUi(List, () => List.Focused && List.Selected >= 0 && List.Selected < List.Items.Count ? Item(List.Selected) : null);
}
public IRawElementProviderSimple[] GetSelection()
{
return UiaApi.OnUi(List, () => List.Selected >= 0 && List.Selected < List.Items.Count ? new IRawElementProviderSimple[] { Item(List.Selected) } : new IRawElementProviderSimple[0]);
}
public bool CanSelectMultiple { get { return false; } }
public bool IsSelectionRequired { get { return false; } }
}
[ComVisible(true)]
public class ListItemProvider : IRawElementProviderSimple, IRawElementProviderFragment, ISelectionItemProvider, IInvokeProvider
{
readonly ListProvider root;
readonly int index, gen;
internal ListItemProvider(ListProvider root, int index, int gen) { this.root = root; this.index = index; this.gen = gen; }
RowList List { get { return root.List; } }
T Live<T>(Func<T> f) { return UiaApi.OnUi(List, () => { if (gen != List.Gen || index >= List.Items.Count) throw UiaApi.Gone(); return f(); }); }
void Live(Action a) { Live<object>(() => { a(); return null; }); }
bool Visible { get { return index >= List.scroll && index < List.scroll + List.MaxRows; } }
public int ProviderOptions { get { return 2; } }
public object GetPatternProvider(int id) { return id == UiaApi.SelectionItemPattern || id == UiaApi.InvokePattern ? this : null; }
public object GetPropertyValue(int id)
{
return Live<object>(() =>
{
var it = List.Items[index];
switch (id)
{
case UiaApi.ControlTypeProp: return UiaApi.ListItem;
case UiaApi.NameProp: return it.Title + (string.IsNullOrEmpty(it.Sub) ? "" : ". " + it.Sub);
case UiaApi.FrameworkIdProp: return "WinForm";
case UiaApi.IsKeyboardFocusableProp: return List.Enabled;
case UiaApi.HasKeyboardFocusProp: return List.Focused && List.Selected == index;
case UiaApi.IsEnabledProp: return List.Enabled;
case UiaApi.IsOffscreenProp: return !List.Visible || !Visible;
case UiaApi.IsSelectedProp: return List.Selected == index;
case UiaApi.PositionInSetProp: return index + 1;
case UiaApi.SizeOfSetProp: return List.Items.Count;
case UiaApi.IsControlElementProp: case UiaApi.IsContentElementProp: return true;
}
return null;
});
}
public IRawElementProviderSimple HostRawElementProvider { get { return null; } }
public IRawElementProviderFragment Navigate(int direction)
{
return Live(() =>
{
if (direction == 0) return (IRawElementProviderFragment)root;
if (direction == 1 && index + 1 < List.Items.Count) return root.Item(index + 1);
if (direction == 2 && index > 0) return root.Item(index - 1);
return null;
});
}
public int[] GetRuntimeId() { return new[] { UiaApi.AppendRuntimeId, gen, index }; }
public UiaRect BoundingRectangle
{
get
{
return Live(() =>
{
if (!Visible || !List.Visible) return new UiaRect();
var r = List.RectangleToScreen(List.Screen(List.Row(index)));
return new UiaRect { Left = r.Left, Top = r.Top, Width = r.Width, Height = r.Height };
});
}
}
public IRawElementProviderSimple[] GetEmbeddedFragmentRoots() { return null; }
public void SetFocus() { Live(() => { List.Focus(); List.SelectItem(index); }); }
public IRawElementProviderFragmentRoot FragmentRoot { get { return root; } }
public void Select() { Live(() => List.SelectItem(index)); }
public void AddToSelection() { Select(); }
public void RemoveFromSelection() { Live(() => { if (List.Selected == index) { List.Selected = -1; List.Invalidate(); } }); }
public bool IsSelected { get { return Live(() => List.Selected == index); } }
public IRawElementProviderSimple SelectionContainer { get { return root; } }
public void Invoke()
{
Live(() =>
{
if (!List.Enabled) throw new InvalidOperationException();
List.SelectItem(index);
List.BeginInvoke((Action)List.ActivateSelected); // may open a dialog: never block the screen reader's call
});
}
}
build.ps1يبني Nyxo Code.exe باستخدام مترجم C# المرفق مع Windows.42 سطر
# Builds "Nyxo Code.exe" (a Windows app) on the Desktop, or at -Out, with the C# compiler that ships with Windows (.NET Framework 4.8).
param([string]$Out)
$ErrorActionPreference = 'Stop'
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$ico = Join-Path $here 'nyxo.ico'
$png = Join-Path $here 'nyxo.png'
# Icon: PNG frames at 256/48/32/16 in one .ico (Windows Vista+ reads PNG frames).
if (-not (Test-Path $ico) -and (Test-Path $png)) {
Add-Type -AssemblyName System.Drawing
$src = [Drawing.Image]::FromFile((Resolve-Path $png))
$frames = foreach ($s in 256, 48, 32, 16) {
$bmp = New-Object Drawing.Bitmap $s, $s
$g = [Drawing.Graphics]::FromImage($bmp)
$g.InterpolationMode = 'HighQualityBicubic'; $g.SmoothingMode = 'HighQuality'; $g.PixelOffsetMode = 'HighQuality'
$g.DrawImage($src, 0, 0, $s, $s); $g.Dispose()
$ms = New-Object IO.MemoryStream; $bmp.Save($ms, [Drawing.Imaging.ImageFormat]::Png); $bmp.Dispose()
, @($s, $ms.ToArray())
}
$src.Dispose()
$icoData = New-Object IO.MemoryStream; $w = New-Object IO.BinaryWriter $icoData
$w.Write([uint16]0); $w.Write([uint16]1); $w.Write([uint16]$frames.Count)
$offset = 6 + 16 * $frames.Count
foreach ($f in $frames) {
$s = $f[0]; $b = $f[1]
$w.Write([byte]($s % 256)); $w.Write([byte]($s % 256)); $w.Write([byte]0); $w.Write([byte]0)
$w.Write([uint16]1); $w.Write([uint16]32); $w.Write([uint32]$b.Length); $w.Write([uint32]$offset)
$offset += $b.Length
}
foreach ($f in $frames) { $w.Write([byte[]]$f[1]) }
[IO.File]::WriteAllBytes($ico, $icoData.ToArray())
}
$csc = Join-Path $env:WINDIR 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'
$fw = Join-Path $env:WINDIR 'Microsoft.NET\Framework64\v4.0.30319'
$exe = if ($Out) { $Out } else { Join-Path $here '..\Nyxo Code.exe' }
$icoArg = if (Test-Path $ico) { "/win32icon:$ico" } else { $null }
$sources = 'Core.cs', 'L.cs', 'Ui.cs', 'Pages.cs', 'History.cs', 'Buy.cs', 'Update.cs', 'App.cs' | ForEach-Object { Join-Path $here $_ }
& $csc /nologo /target:winexe /platform:anycpu /optimize+ /debug- "/out:$exe" $icoArg "/resource:$((Resolve-Path $png).Path),nyxo.png" `
"/r:$fw\System.Security.dll" "/r:$fw\System.Web.Extensions.dll" "/r:$fw\System.Windows.Forms.dll" "/r:$fw\System.Drawing.dll" $sources
if ($LASTEXITCODE) { exit $LASTEXITCODE }
Get-Item $exe | Select-Object FullName, Length
ملفا الأيقونة (nyxo.ico و nyxo.png) وملف README مضمّنة في ملف .zip.