using System.Diagnostics; using System.Security.Cryptography; using System.Text; using NozCli.Repl; using Spectre.Console; namespace NozCli.Commands; public class EditCommand : ICommand { public string Name => "edit"; public string Description => "Open a note in nano (or -c for VS Code)"; public string Usage => "edit [-c] "; public async Task ExecuteAsync(string[] args, ReplContext ctx, CancellationToken ct = default) { var useCode = args.Contains("-c") || args.Contains("--code"); var slug = args.FirstOrDefault(a => !a.StartsWith('-')); if (slug is null) { AnsiConsole.MarkupLine("[red]Usage: edit [-c] [/]"); return; } slug = ctx.ResolveSlug(slug); NozCli.Core.Models.NoteContent note = default!; await AnsiConsole.Status().StartAsync("Fetching…", async _ => note = await ctx.Api.GetNoteAsync(slug, ct)); var tmp = Path.Combine(Path.GetTempPath(), $"noz-{slug.Replace('/', '-')}.md"); await File.WriteAllTextAsync(tmp, note.Markdown); string edited; try { var (editor, editorArgs) = useCode ? ResolveCode(tmp, ctx.Config.CodeEditor) : ResolveTerminal(tmp, ctx.Config.Editor); AnsiConsole.MarkupLine($"[dim]Opening {editor} …[/]"); var proc = Process.Start(new ProcessStartInfo { FileName = editor, Arguments = editorArgs, UseShellExecute = false, }); proc?.WaitForExit(); edited = await File.ReadAllTextAsync(tmp); } finally { if (File.Exists(tmp)) File.Delete(tmp); } var newHash = Sha256(edited); if (newHash == note.Hash) { AnsiConsole.MarkupLine("[dim]No changes.[/]"); return; } AnsiConsole.MarkupLine("[green]Changes detected.[/]"); var push = AnsiConsole.Confirm("Push changes to server?"); if (!push) { AnsiConsole.MarkupLine("[dim]Discarded.[/]"); return; } await AnsiConsole.Status().StartAsync("Pushing…", async _ => { await ctx.Api.PutNoteAsync(slug, edited, ct); ctx.NoteCache = await ctx.Api.GetNotesAsync(ct); }); AnsiConsole.MarkupLine($"[green]✓[/] {slug} updated"); } // nano (or configured terminal editor) — quick edits internal static (string, string) ResolveTerminal(string filePath, string? configured) { var editor = configured ?? Environment.GetEnvironmentVariable("EDITOR") ?? (IsOnPath("nano") ? "nano" : IsOnPath("vi") ? "vi" : "nano"); return (editor, $"\"{filePath}\""); } // -c editor: configured, or auto-detect code/zed/etc. // GUI editors that need a --wait flag are detected by name. internal static (string, string) ResolveCode(string filePath, string? configured) { var editor = configured ?? (IsOnPath("code") ? "code" : IsOnPath("code-insiders")? "code-insiders": IsOnPath("zed") ? "zed" : IsOnPath("subl") ? "subl" : "code"); // These editors need --wait to block until the file is closed var needsWait = editor is "code" or "code-insiders" or "subl"; var fileArg = $"\"{filePath}\""; return (editor, needsWait ? $"--wait {fileArg}" : fileArg); } internal static bool IsOnPath(string name) { try { using var p = Process.Start(new ProcessStartInfo { FileName = OperatingSystem.IsWindows() ? "where" : "which", Arguments = name, RedirectStandardOutput = true, UseShellExecute = false, }); p?.WaitForExit(); return p?.ExitCode == 0; } catch { return false; } } private static string Sha256(string content) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(content))).ToLower(); }