Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tool: Add Watcher feature to Manifest and SSG #47

Merged
merged 10 commits into from
Jul 6, 2024
163 changes: 116 additions & 47 deletions Nodsoft.MoltenObsidian.Tool/Commands/Manifest/GenerateManifestCommand.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Nodsoft.MoltenObsidian.Vaults.FileSystem;
using Nodsoft.MoltenObsidian.Manifest;
using Nodsoft.MoltenObsidian.Vault;
using Spectre.Console;
using Spectre.Console.Cli;

Expand Down Expand Up @@ -36,7 +38,11 @@ public sealed class GenerateManifestSettings : CommandSettings

[CommandOption("--debug", IsHidden = true)]
public bool DebugMode { get; set; }

[CommandOption("--watch"), Description("Watches the vault for changes and regenerates the manifest.")]
public bool Watch { get; set; }


public override ValidationResult Validate()
{
if (VaultPathStr is "" || (VaultPath = new(VaultPathStr)) is { Exists: false })
Expand All @@ -49,18 +55,16 @@ public override ValidationResult Validate()
return ValidationResult.Error($"The output path '{OutputPath}' does not exist.");
}

if (!Force)
if (!Force && !VaultPath.GetDirectories().Any(static d => d.Name == ".obsidian"))
{
if (!VaultPath.GetDirectories().Any(static d => d.Name == ".obsidian"))
{
return ValidationResult.Error($"The vault path '{VaultPath}' does not appear to be a valid Obsidian vault.");
}
return ValidationResult.Error($"The vault path '{VaultPath}' does not appear to be a valid Obsidian vault.");
}

return ValidationResult.Success();
}
}


/// <summary>
/// Provides a command that generates a manifest for a Molten Obsidian vault.
/// </summary>
Expand All @@ -72,15 +76,15 @@ public override async Task<int> ExecuteAsync(CommandContext context, GenerateMan
if (settings.DebugMode)
{
// Inform the user that we're in debug mode.
AnsiConsole.MarkupLine(/*lang=md*/"[bold blue]Debug mode is enabled.[/]");
AnsiConsole.MarkupLine( /*lang=md*/"[bold blue]Debug mode is enabled.[/]");
}

// This is where the magic happens.
// We'll be using the Spectre.Console library to provide a nice CLI experience.
// Statuses at each step, and a nice summary at the end.

FileSystemVault vault = null!;

// First, load the Obsidian vault. This will validate the vault and load all the files.
AnsiConsole.Console.Status().Start("Loading vault...", _ =>
{
Expand All @@ -90,8 +94,6 @@ public override async Task<int> ExecuteAsync(CommandContext context, GenerateMan
AnsiConsole.Console.MarkupLine(/*lang=md*/$"[grey]Ignoring folders:[/] {string.Join("[grey], [/]", settings.IgnoredFolders ?? ["*None*"])}");
AnsiConsole.Console.MarkupLine(/*lang=md*/$"[grey]Ignoring files:[/] {string.Join("[grey], [/]", settings.IgnoreFiles ?? ["*None*"])}");
}



settings.IgnoredFolders ??= FileSystemVault.DefaultIgnoredFolders.ToArray();
settings.IgnoreFiles ??= FileSystemVault.DefaultIgnoredFiles.ToArray();
Expand All @@ -101,62 +103,129 @@ public override async Task<int> ExecuteAsync(CommandContext context, GenerateMan
});

AnsiConsole.MarkupLine(/*lang=md*/$"Loaded vault with [green]{vault.Files.Count}[/] files.");

// Next, generate the manifest.
RemoteVaultManifest manifest = null!;
await AnsiConsole.Console.Status().StartAsync("Generating manifest...", async _ =>
{
// Generate the manifest.
manifest = await VaultManifestGenerator.GenerateManifestAsync(vault);
});

AnsiConsole.MarkupLine(/*lang=md*/$"Generated manifest with [green]{manifest.Files.Count}[/] files.");

// Finally, write the manifest to disk, at the specified location (or the vault root if not specified).
FileInfo manifestFile = new(Path.Combine((settings.OutputPath ?? settings.VaultPath).FullName, RemoteVaultManifest.ManifestFileName));

// Check if the file already exists.
if (manifestFile.Exists)
await GenerateManifestAsync(vault, settings.VaultPath, settings.OutputPath, settings.DebugMode, file =>
{
if (settings.Force)
if (settings.Force || settings.Watch)
{
// Warn the user that the file will be overwritten.
AnsiConsole.MarkupLine(/*lang=md*/"[yellow]A manifest file already exists at the specified location, but [green]--force[/] was specified. Overwriting.[/]");
string forceFlagStr = settings switch
{
{ Force: true } => "--force",
{ Watch: true } => "--watch",
_ => throw new UnreachableException()
};

AnsiConsole.MarkupLine(/*lang=md*/$"[yellow]A manifest file already exists at the specified location, but [green]{forceFlagStr}[/] was specified. Overwriting.[/]");
}
else
{
// If it does, ask the user if they want to overwrite it.
bool overwrite = AnsiConsole.Prompt(new ConfirmationPrompt(/*lang=md*/"[yellow]The manifest file already exists. Overwrite?[/]"));

if (!overwrite)
{
// If they don't, abort.
AnsiConsole.MarkupLine("[red]Aborted.[/]");
return 1;
AnsiConsole.MarkupLine(/*lang=md*/"[red]Aborted.[/]");
return false;
}

// If they do, delete the file.
manifestFile.Delete();
file.Delete();
}
}

await AnsiConsole.Console.Status().StartAsync("Writing manifest...", async _ =>
return true;
});

if (settings.Watch)
{
// Write the new manifest to disk.
await using FileStream stream = manifestFile.Open(FileMode.OpenOrCreate, FileAccess.Write);
stream.SetLength(0);


#pragma warning disable CA1869
await JsonSerializer.SerializeAsync(stream, manifest, new JsonSerializerOptions
await AnsiConsole.Console.Status().StartAsync("Watching vault for changes...", async ctx =>
{
WriteIndented = settings.DebugMode, // If debug mode is enabled, write the manifest with indentation.
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Use camelCase for property names.
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // Don't write null values.
// Print a status message.
ctx.Spinner(Spinner.Known.Dots);
ctx.SpinnerStyle(Style.Parse("purple bold"));

vault.VaultUpdate += async (_, args) =>
{
try
{
if (args.Entity.Path is RemoteVaultManifest.ManifestFileName)
{
AnsiConsole.MarkupLine(/*lang=md*/"[grey]Manifest update detected. Ignoring...[/]");
return;
}

// Print a status message.
AnsiConsole.MarkupLine(/*lang=md*/$"[grey]Vault update detected (Entity name: [/]{args.Entity.Path}[grey], Change type: [/]{args.Type})");
AnsiConsole.MarkupLine(/*lang=md*/"[blue]Regenerating manifest...[/]");

// Regenerate the manifest.
await GenerateManifestAsync(vault, settings.VaultPath, settings.OutputPath, settings.DebugMode, _ => true);
}
catch (Exception e)
{
// Print an error message.
AnsiConsole.MarkupLine(/*lang=md*/"[red]An error occurred while regenerating the manifest:[/]");
AnsiConsole.WriteException(e, ExceptionFormats.ShortenEverything);
}
};

// Watch the vault for changes.
await Task.Delay(-1);
});
});
}

AnsiConsole.MarkupLine(/*lang=md*/$"Wrote manifest to [green link]{manifestFile.FullName}[/].");
return 0;
}
}

internal static async Task<RemoteVaultManifest> GenerateManifestAsync(
IVault vault,
DirectoryInfo vaultPath,
DirectoryInfo? outputPath,
bool debugMode,
Func<FileInfo, bool> promptOverwrite
) {
// Assert the vault as FileSystem-Based
if (vault is not FileSystemVault)
{
throw new InvalidOperationException("The vault must be a FileSystemVault to generate a manifest.");
}

// Next, generate the manifest.
AnsiConsole.Console.MarkupLine( /*lang=md*/"Generating manifest...");
RemoteVaultManifest manifest = await VaultManifestGenerator.GenerateManifestAsync(vault);

AnsiConsole.MarkupLine(/*lang=md*/$"Generated manifest with [green]{manifest.Files.Count}[/] files.");

// Write the manifest to disk, at the specified location (or the vault root if not specified).
FileInfo manifestFile = new(Path.Combine((outputPath ?? vaultPath).FullName, RemoteVaultManifest.ManifestFileName));

if (manifestFile.Exists && !promptOverwrite(manifestFile))
{
return manifest;
}

// Write the manifest to disk.
await WriteManifestFileAsync(manifestFile, manifest, debugMode);
return manifest;
}

private static async Task WriteManifestFileAsync(FileInfo file, RemoteVaultManifest manifest, bool debugMode)
{
AnsiConsole.MarkupLine(/*lang=md*/$"Writing manifest...");

// Write the new manifest to disk.
await using FileStream fs = file.Open(FileMode.OpenOrCreate, FileAccess.Write);
fs.SetLength(0);

#pragma warning disable CA1869
await JsonSerializer.SerializeAsync(fs, manifest, new JsonSerializerOptions
{
WriteIndented = debugMode, // If debug mode is enabled, write the manifest with indentation.
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Use camelCase for property names.
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // Don't write null values.
});

AnsiConsole.MarkupLine(/*lang=md*/$"Wrote manifest to [green link]{file.FullName}[/].");
}
}
Loading
Loading