diff --git a/OP/.gitignore b/OP/.gitignore new file mode 100644 index 0000000..6303abf --- /dev/null +++ b/OP/.gitignore @@ -0,0 +1,270 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ +publish_release/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +**/.DS_Store + +**/.vs/ +.DS_Store + + +publish_release/ \ No newline at end of file diff --git a/OP/Build/Build.csproj b/OP/Build/Build.csproj new file mode 100644 index 0000000..6c1dc92 --- /dev/null +++ b/OP/Build/Build.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/OP/Build/Program.cs b/OP/Build/Program.cs new file mode 100644 index 0000000..1e8db2d --- /dev/null +++ b/OP/Build/Program.cs @@ -0,0 +1,304 @@ +using System.Diagnostics; +using System.Text.Json; +using System.Text.RegularExpressions; + +const string DocsFolderName = "op.docs"; +const string PluginSourceFolder = "OP_plugin"; +const string PluginManifestFileName = "manifest.json"; + +try +{ + var buildProjectDir = GetBuildProjectDirectory(); + var workspaceRoot = Path.GetFullPath(Path.Combine(buildProjectDir, "..")); + var docsRoot = Path.GetFullPath(Path.Combine(workspaceRoot, "..", DocsFolderName)); + var pluginSourceRoot = Path.GetFullPath(Path.Combine(workspaceRoot, "..", PluginSourceFolder)); + + Console.WriteLine($"Workspace root: {workspaceRoot}"); + Console.WriteLine($"Docs root: {docsRoot}"); + Console.WriteLine($"Plugin source: {pluginSourceRoot}"); + + EnsureDirectoryExists(docsRoot, "Docs folder not found. Make sure op.docs exists in the workspace root."); + EnsureDirectoryExists(pluginSourceRoot, "Plugin source folder not found. Make sure OP_plugin exists in the workspace root."); + + var manifestPath = Path.Combine(pluginSourceRoot, PluginManifestFileName); + if (!File.Exists(manifestPath)) + throw new InvalidOperationException($"Plugin manifest not found at {manifestPath}"); + + BuildPlugin(pluginSourceRoot); + + var pluginId = ReadPluginId(manifestPath); + Console.WriteLine($"Detected plugin id: {pluginId}"); + + var userRoot = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var docsTargets = ScanDocTargets(docsRoot); + + if (!docsTargets.Any()) + { + Console.WriteLine("No docs files with 'category: docs' were found."); + return; + } + + var uniqueTargets = docsTargets.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + + Console.WriteLine($"Found {uniqueTargets.Count} unique docs target(s):"); + foreach (var target in uniqueTargets) + { + Console.WriteLine($" - {target}"); + } + + foreach (var targetPath in uniqueTargets) + { + var targetRoot = BuildAbsolutePathFromUserRoot(userRoot, targetPath); + var pluginsRoot = Path.Combine(targetRoot, ".obsidian", "plugins"); + var pluginDestination = Path.Combine(pluginsRoot, pluginId); + + Console.WriteLine(); + Console.WriteLine($"Installing plugin to: {pluginDestination}"); + + if (Directory.Exists(pluginDestination)) + { + Console.WriteLine("Existing plugin folder found. Deleting previous version..."); + Directory.Delete(pluginDestination, recursive: true); + } + + Directory.CreateDirectory(pluginsRoot); + CopyDirectory(pluginSourceRoot, pluginDestination, GetCopyExcludes()); + Console.WriteLine("Plugin copied successfully."); + } +} +catch (Exception ex) +{ + Console.Error.WriteLine("ERROR: " + ex.Message); + Environment.Exit(1); +} + +static string GetBuildProjectDirectory() +{ + var baseDir = AppContext.BaseDirectory; + // Current baseDir is Build/bin/Debug/net10.0/ + // We want to go up to the project root (where Build/ and OP.sln are) + return Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..")); +} + +static void EnsureDirectoryExists(string path, string message) +{ + if (!Directory.Exists(path)) + throw new DirectoryNotFoundException(message); +} + +static IReadOnlyCollection ScanDocTargets(string docsRoot) +{ + var docsTargetPaths = new List(); + var markdownFiles = Directory.EnumerateFiles(docsRoot, "*.md", SearchOption.AllDirectories); + + foreach (var filePath in markdownFiles) + { + var content = File.ReadAllText(filePath); + var frontMatter = ExtractFrontMatter(content); + if (frontMatter is null) + continue; + + if (!frontMatter.TryGetValue("category", out var categoryValue) || + !string.Equals(categoryValue.Trim(), "docs", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (!frontMatter.TryGetValue("path", out var pathValue) || string.IsNullOrWhiteSpace(pathValue)) + { + Console.WriteLine($"WARNING: File '{filePath}' has category: docs but missing path."); + continue; + } + + docsTargetPaths.Add(pathValue.Trim()); + } + + return docsTargetPaths; +} + +static Dictionary? ExtractFrontMatter(string content) +{ + var match = Regex.Match(content, @"\A---\r?\n(.*?)\r?\n---", RegexOptions.Singleline); + if (!match.Success) + return null; + + var frontMatter = new Dictionary(StringComparer.OrdinalIgnoreCase); + var body = match.Groups[1].Value; + + foreach (var rawLine in body.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) + { + var line = rawLine.Trim(); + if (line.Length == 0 || line.StartsWith("#")) + continue; + + var separatorIndex = line.IndexOf(':'); + if (separatorIndex < 0) + continue; + + var key = line.Substring(0, separatorIndex).Trim(); + var value = line.Substring(separatorIndex + 1).Trim().Trim('"').Trim(); + frontMatter[key] = value; + } + + return frontMatter; +} + +static string BuildAbsolutePathFromUserRoot(string userRoot, string relativePath) +{ + if (Path.IsPathRooted(relativePath)) + { + return Path.GetFullPath(relativePath); + } + + var segments = relativePath + .Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries) + .Select(segment => segment.Trim()) + .Where(segment => segment.Length > 0) + .ToArray(); + + if (segments.Length == 0) + throw new InvalidOperationException($"Invalid path value: '{relativePath}'"); + + return Path.GetFullPath(Path.Combine(new[] { userRoot }.Concat(segments).ToArray())); +} + +static string ReadPluginId(string manifestPath) +{ + using var stream = File.OpenRead(manifestPath); + using var doc = JsonDocument.Parse(stream); + if (!doc.RootElement.TryGetProperty("id", out var idElement)) + throw new InvalidOperationException("Plugin manifest does not contain an 'id' property."); + + var pluginId = idElement.GetString(); + if (string.IsNullOrWhiteSpace(pluginId)) + throw new InvalidOperationException("Plugin id is empty."); + + return pluginId.Trim(); +} + +static void BuildPlugin(string pluginSourceRoot) +{ + Console.WriteLine("Building plugin in OP_plugin..."); + var npmExecutable = FindNpmExecutable(); + if (npmExecutable is null) + { + throw new InvalidOperationException("Could not find npm on PATH. Install Node.js/npm or add npm to your PATH."); + } + + var processStartInfo = new ProcessStartInfo(npmExecutable) + { + WorkingDirectory = pluginSourceRoot, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + processStartInfo.FileName = "cmd"; + processStartInfo.ArgumentList.Add("/c"); + processStartInfo.ArgumentList.Add(npmExecutable); + processStartInfo.ArgumentList.Add("run"); + processStartInfo.ArgumentList.Add("build"); + + using var process = Process.Start(processStartInfo) ?? throw new InvalidOperationException($"Failed to start 'cmd /c {npmExecutable}'."); + var stdout = process.StandardOutput.ReadToEnd(); + var stderr = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + Console.WriteLine(stdout); + if (!string.IsNullOrWhiteSpace(stderr)) + Console.Error.WriteLine(stderr); + + if (process.ExitCode != 0) + throw new InvalidOperationException($"Plugin build failed with exit code {process.ExitCode}."); + + Console.WriteLine("Plugin build completed successfully."); +} + +static string? FindNpmExecutable() +{ + var candidates = new[] { "npm.cmd", "npm.exe", "npm" }; + foreach (var candidate in candidates) + { + try + { + var processStartInfo = new ProcessStartInfo("cmd", $"/c {candidate} --version") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + using var process = Process.Start(processStartInfo); + if (process is null) + continue; + + bool exited = process.WaitForExit(5000); + if (!exited) + { + process.Kill(); + continue; + } + + if (process.ExitCode == 0) + return candidate; + } + catch + { + continue; + } + } + + return null; +} + +static HashSet GetCopyExcludes() +{ + return new(StringComparer.OrdinalIgnoreCase) + { + "node_modules", + "src", + "obj", + "bin", + ".git", + ".idea", + ".vscode", + ".editorconfig", + ".npmrc", + "package.json", + "package-lock.json", + "tsconfig.json", + "esbuild.config.mjs", + "eslint.config.mts", + "version-bump.mjs", + "versions.json", + "data.json", + "README.md", + "AGENTS.md", + "LICENSE" + }; +} + +static void CopyDirectory(string sourceDir, string destinationDir, HashSet excludes) +{ + Directory.CreateDirectory(destinationDir); + + foreach (var filePath in Directory.GetFiles(sourceDir)) + { + var fileName = Path.GetFileName(filePath); + if (excludes.Contains(fileName)) + continue; + + var destinationFile = Path.Combine(destinationDir, fileName); + File.Copy(filePath, destinationFile, overwrite: true); + } + + foreach (var directoryPath in Directory.GetDirectories(sourceDir)) + { + var dirName = Path.GetFileName(directoryPath); + if (excludes.Contains(dirName)) + continue; + + CopyDirectory(directoryPath, Path.Combine(destinationDir, dirName), excludes); + } +} diff --git a/OP/OP.sln b/OP/OP.sln new file mode 100644 index 0000000..497efb2 --- /dev/null +++ b/OP/OP.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "Build\Build.csproj", "{57A2E434-08CD-47BF-9F3D-E1BF74C34EE7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {57A2E434-08CD-47BF-9F3D-E1BF74C34EE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57A2E434-08CD-47BF-9F3D-E1BF74C34EE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57A2E434-08CD-47BF-9F3D-E1BF74C34EE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57A2E434-08CD-47BF-9F3D-E1BF74C34EE7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/OP/.editorconfig b/OP_plugin/.editorconfig similarity index 100% rename from OP/.editorconfig rename to OP_plugin/.editorconfig diff --git a/OP/.npmrc b/OP_plugin/.npmrc similarity index 100% rename from OP/.npmrc rename to OP_plugin/.npmrc diff --git a/OP/AGENTS.md b/OP_plugin/AGENTS.md similarity index 100% rename from OP/AGENTS.md rename to OP_plugin/AGENTS.md diff --git a/OP/LICENSE b/OP_plugin/LICENSE similarity index 100% rename from OP/LICENSE rename to OP_plugin/LICENSE diff --git a/OP/README.md b/OP_plugin/README.md similarity index 100% rename from OP/README.md rename to OP_plugin/README.md diff --git a/OP/esbuild.config.mjs b/OP_plugin/esbuild.config.mjs similarity index 100% rename from OP/esbuild.config.mjs rename to OP_plugin/esbuild.config.mjs diff --git a/OP/eslint.config.mts b/OP_plugin/eslint.config.mts similarity index 100% rename from OP/eslint.config.mts rename to OP_plugin/eslint.config.mts diff --git a/OP/manifest.json b/OP_plugin/manifest.json similarity index 100% rename from OP/manifest.json rename to OP_plugin/manifest.json diff --git a/OP/package-lock.json b/OP_plugin/package-lock.json similarity index 100% rename from OP/package-lock.json rename to OP_plugin/package-lock.json diff --git a/OP/package.json b/OP_plugin/package.json similarity index 100% rename from OP/package.json rename to OP_plugin/package.json diff --git a/OP/src/main.ts b/OP_plugin/src/main.ts similarity index 100% rename from OP/src/main.ts rename to OP_plugin/src/main.ts diff --git a/OP/src/settings.ts b/OP_plugin/src/settings.ts similarity index 100% rename from OP/src/settings.ts rename to OP_plugin/src/settings.ts diff --git a/OP/styles.css b/OP_plugin/styles.css similarity index 100% rename from OP/styles.css rename to OP_plugin/styles.css diff --git a/OP/tsconfig.json b/OP_plugin/tsconfig.json similarity index 100% rename from OP/tsconfig.json rename to OP_plugin/tsconfig.json diff --git a/OP/version-bump.mjs b/OP_plugin/version-bump.mjs similarity index 100% rename from OP/version-bump.mjs rename to OP_plugin/version-bump.mjs diff --git a/OP/versions.json b/OP_plugin/versions.json similarity index 100% rename from OP/versions.json rename to OP_plugin/versions.json diff --git a/op.docs/.obsidian/app.json b/op.docs/.obsidian/app.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/op.docs/.obsidian/app.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/op.docs/.obsidian/appearance.json b/op.docs/.obsidian/appearance.json new file mode 100644 index 0000000..4be7969 --- /dev/null +++ b/op.docs/.obsidian/appearance.json @@ -0,0 +1,3 @@ +{ + "theme": "obsidian" +} \ No newline at end of file diff --git a/op.docs/.obsidian/community-plugins.json b/op.docs/.obsidian/community-plugins.json new file mode 100644 index 0000000..a4b4a08 --- /dev/null +++ b/op.docs/.obsidian/community-plugins.json @@ -0,0 +1,3 @@ +[ + "frontmatter-folder-organizer" +] \ No newline at end of file diff --git a/op.docs/.obsidian/core-plugins.json b/op.docs/.obsidian/core-plugins.json new file mode 100644 index 0000000..639b90d --- /dev/null +++ b/op.docs/.obsidian/core-plugins.json @@ -0,0 +1,33 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "canvas": true, + "outgoing-link": true, + "tag-pane": true, + "footnotes": false, + "properties": true, + "page-preview": true, + "daily-notes": true, + "templates": true, + "note-composer": true, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "bookmarks": true, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": true, + "word-count": true, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": true, + "publish": false, + "sync": true, + "bases": true, + "webviewer": false +} \ No newline at end of file diff --git a/op.docs/.obsidian/plugins/frontmatter-folder-organizer/manifest.json b/op.docs/.obsidian/plugins/frontmatter-folder-organizer/manifest.json new file mode 100644 index 0000000..a6f3782 --- /dev/null +++ b/op.docs/.obsidian/plugins/frontmatter-folder-organizer/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "frontmatter-folder-organizer", + "name": "Frontmatter Folder Organizer", + "version": "1.0.0", + "minAppVersion": "1.0.0", + "description": "Organize markdown files into folders based on frontmatter hierarchy.", + "author": "Obsidian", + "authorUrl": "https://obsidian.md", + "isDesktopOnly": false +} diff --git a/op.docs/.obsidian/plugins/frontmatter-folder-organizer/styles.css b/op.docs/.obsidian/plugins/frontmatter-folder-organizer/styles.css new file mode 100644 index 0000000..71cc60f --- /dev/null +++ b/op.docs/.obsidian/plugins/frontmatter-folder-organizer/styles.css @@ -0,0 +1,8 @@ +/* + +This CSS file will be included with your plugin, and +available in the app when your plugin is enabled. + +If your plugin does not need CSS, delete this file. + +*/ diff --git a/op.docs/.obsidian/types.json b/op.docs/.obsidian/types.json new file mode 100644 index 0000000..be6e75f --- /dev/null +++ b/op.docs/.obsidian/types.json @@ -0,0 +1,8 @@ +{ + "types": { + "aliases": "aliases", + "cssclasses": "multitext", + "tags": "tags", + "shouldInstallPlugins": "checkbox" + } +} \ No newline at end of file diff --git a/op.docs/.obsidian/workspace.json b/op.docs/.obsidian/workspace.json new file mode 100644 index 0000000..8d4e22e --- /dev/null +++ b/op.docs/.obsidian/workspace.json @@ -0,0 +1,194 @@ +{ + "main": { + "id": "eeb36d4e48f0ca25", + "type": "split", + "children": [ + { + "id": "4fe61a13d63c2732", + "type": "tabs", + "children": [ + { + "id": "362018eb264ab766", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "docs/op.docs.md", + "mode": "source", + "source": false + }, + "icon": "lucide-file", + "title": "op.docs" + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "b9bf87c6cad06498", + "type": "split", + "children": [ + { + "id": "6685cdba50e4a0a7", + "type": "tabs", + "children": [ + { + "id": "6ef95b00a28c418d", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical", + "autoReveal": false + }, + "icon": "lucide-folder-closed", + "title": "Files" + } + }, + { + "id": "c5043b22e38e990c", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + }, + "icon": "lucide-search", + "title": "Search" + } + }, + { + "id": "b159963fcca61ebc", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": {}, + "icon": "lucide-bookmark", + "title": "Bookmarks" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300 + }, + "right": { + "id": "c5e47ea76d041492", + "type": "split", + "children": [ + { + "id": "897865c856161773", + "type": "tabs", + "children": [ + { + "id": "379d0b312b9d30c9", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "file": "doc.zs.md", + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-coming-in", + "title": "Backlinks for doc.zs" + } + }, + { + "id": "a8f559c72a82e929", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "file": "doc.zs.md", + "linksCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-going-out", + "title": "Outgoing links from doc.zs" + } + }, + { + "id": "e1552c44ca209983", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-tags", + "title": "Tags" + } + }, + { + "id": "2d2defa7638fb5a3", + "type": "leaf", + "state": { + "type": "all-properties", + "state": { + "sortOrder": "frequency", + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-archive", + "title": "All properties" + } + }, + { + "id": "82b06aefc5b6df13", + "type": "leaf", + "state": { + "type": "outline", + "state": { + "file": "doc.zs.md", + "followCursor": false, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-list", + "title": "Outline of doc.zs" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300, + "collapsed": true + }, + "left-ribbon": { + "hiddenItems": { + "switcher:Open quick switcher": false, + "graph:Open graph view": false, + "canvas:Create new canvas": false, + "daily-notes:Open today's daily note": false, + "templates:Insert template": false, + "command-palette:Open command palette": false, + "bases:Create new base": false + } + }, + "active": "362018eb264ab766", + "lastOpenFiles": [ + "docs/doc.zs.md", + "docs/op.docs.md", + "docs", + "os.docs.md", + "PLUGIN_SETTINGS_UI_GUIDE.md" + ] +} \ No newline at end of file diff --git a/op.docs/PLUGIN_SETTINGS_UI_GUIDE.md b/op.docs/PLUGIN_SETTINGS_UI_GUIDE.md new file mode 100644 index 0000000..9cbb050 --- /dev/null +++ b/op.docs/PLUGIN_SETTINGS_UI_GUIDE.md @@ -0,0 +1,604 @@ +# Plugin Settings UI Guide + +This guide explains how to structure and format plugin settings UI in Obsidian, with examples for building multiple plugins with distinct features. + +## Table of Contents + +1. [Core Concepts](#core-concepts) +2. [Setting Types](#setting-types) +3. [UI Organization Patterns](#ui-organization-patterns) +4. [Multi-Plugin Architecture](#multi-plugin-architecture) +5. [Plugin Examples](#plugin-examples) + +## Core Concepts + +### Settings Interface & Settings Tab + +Every plugin has: + +1. **Settings Interface** - TypeScript interface defining your data structure +2. **Settings Tab Class** - Extends `PluginSettingTab` and defines the UI + +```typescript +// Define the data structure +export interface MyPluginSettings { + settingName: string; + settingValue: boolean; +} + +// Default values +export const DEFAULT_SETTINGS: MyPluginSettings = { + settingName: 'default', + settingValue: false, +}; + +// UI definition +export class MySettingTab extends PluginSettingTab { + plugin: MyPlugin; + + constructor(app: App, plugin: MyPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); // Clear previous UI + + // Add UI elements here + } +} +``` + +### Container Structure + +The `containerEl` is where you build your UI. Always start with `containerEl.empty()` to clear old content when the settings tab reopens. + +```typescript +display(): void { + const { containerEl } = this; + containerEl.empty(); + + // Add heading + containerEl.createEl('h2', { text: 'Plugin Name' }); + + // Add settings + new Setting(containerEl) + .setName('Setting Name') + .setDesc('Description of what this does') + .addText((text) => { /* ... */ }); +} +``` + +## Setting Types + +### Text Input + +Single-line text input: + +```typescript +new Setting(containerEl) + .setName('Plugin Name') + .setDesc('The name of your plugin') + .addText((text) => + text + .setPlaceholder('Enter name') + .setValue(this.plugin.settings.pluginName) + .onChange(async (value) => { + this.plugin.settings.pluginName = value; + await this.plugin.saveSettings(); + }) + ); +``` + +### Text Area + +Multi-line text input: + +```typescript +new Setting(containerEl) + .setName('Configuration') + .setDesc('Multi-line configuration') + .addTextArea((text) => + text + .setPlaceholder('Enter configuration') + .setValue(this.plugin.settings.config) + .onChange(async (value) => { + this.plugin.settings.config = value; + await this.plugin.saveSettings(); + }) + ); +``` + +### Toggle + +Boolean switch: + +```typescript +new Setting(containerEl) + .setName('Enable Feature') + .setDesc('Turn this feature on or off') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enabled) + .onChange(async (value) => { + this.plugin.settings.enabled = value; + await this.plugin.saveSettings(); + }) + ); +``` + +### Dropdown + +Select from predefined options: + +```typescript +new Setting(containerEl) + .setName('Theme') + .setDesc('Choose a color theme') + .addDropdown((dropdown) => + dropdown + .addOption('light', 'Light') + .addOption('dark', 'Dark') + .addOption('auto', 'Auto') + .setValue(this.plugin.settings.theme) + .onChange(async (value) => { + this.plugin.settings.theme = value; + await this.plugin.saveSettings(); + }) + ); +``` + +### Slider + +Numeric input with range: + +```typescript +new Setting(containerEl) + .setName('Search Depth') + .setDesc('How deep to search in folders (1-10)') + .addSlider((slider) => + slider + .setMin(1) + .setMax(10) + .setStep(1) + .setValue(this.plugin.settings.searchDepth) + .onChange(async (value) => { + this.plugin.settings.searchDepth = value; + await this.plugin.saveSettings(); + }) + ); +``` + +### Button + +Trigger an action: + +```typescript +new Setting(containerEl) + .setName('Process Files') + .setDesc('Start processing all files') + .addButton((button) => + button + .setButtonText('Process Now') + .onClick(async () => { + await this.plugin.processAllFiles(); + new Notice('Processing complete!'); + }) + ); +``` + +### Color Picker + +Select a color: + +```typescript +new Setting(containerEl) + .setName('Tag Color') + .setDesc('Color for tags') + .addColorPicker((color) => + color + .setValue(this.plugin.settings.tagColor) + .onChange(async (value) => { + this.plugin.settings.tagColor = value; + await this.plugin.saveSettings(); + }) + ); +``` + +## UI Organization Patterns + +### Section Headers + +Organize settings into logical sections: + +```typescript +display(): void { + const { containerEl } = this; + containerEl.empty(); + + // Main heading + containerEl.createEl('h2', { text: 'Plugin Settings' }); + + // Section 1 + containerEl.createEl('h3', { text: 'Basic Configuration' }); + new Setting(containerEl).setName('Setting 1')... + new Setting(containerEl).setName('Setting 2')... + + // Section 2 + containerEl.createEl('h3', { text: 'Advanced Options' }); + new Setting(containerEl).setName('Setting 3')... + new Setting(containerEl).setName('Setting 4')... +} +``` + +### Conditional Display + +Show/hide settings based on other settings: + +```typescript +display(): void { + const { containerEl } = this; + containerEl.empty(); + + new Setting(containerEl) + .setName('Enable Advanced') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.advancedMode) + .onChange(async (value) => { + this.plugin.settings.advancedMode = value; + await this.plugin.saveSettings(); + this.display(); // Redraw to show/hide advanced settings + }) + ); + + if (this.plugin.settings.advancedMode) { + new Setting(containerEl) + .setName('Advanced Option') + .setDesc('This only shows when advanced mode is on') + .addText((text) => { /* ... */ }); + } +} +``` + +### Grouped Controls + +Multiple controls in one setting: + +```typescript +new Setting(containerEl) + .setName('Color') + .setDesc('Choose color and opacity') + .addColorPicker((color) => + color + .setValue(this.plugin.settings.color) + .onChange(async (value) => { + this.plugin.settings.color = value; + await this.plugin.saveSettings(); + }) + ) + .addSlider((slider) => + slider + .setMin(0) + .setMax(100) + .setValue(this.plugin.settings.opacity) + .onChange(async (value) => { + this.plugin.settings.opacity = value; + await this.plugin.saveSettings(); + }) + ); +``` + +## Multi-Plugin Architecture + +When building multiple plugins within one codebase, consider this structure: + +### Folder Structure + +``` +src/ + main.ts (Plugin entry point) + settings.ts (Shared settings) + plugins/ + folder/ + folderPlugin.ts + folderSettings.ts + markdown/ + markdownPlugin.ts + markdownSettings.ts + kanban/ + kanbanPlugin.ts + kanbanSettings.ts +``` + +### Unified Settings Interface + +```typescript +// settings.ts +export interface PluginConfig { + // Folder Plugin settings + folderPlugin: { + enabled: boolean; + organizationHierarchy: string; + }; + + // Markdown Plugin settings + markdownPlugin: { + enabled: boolean; + autoCreatePath: string; + template: string; + }; + + // Kanban Plugin settings + kanbanPlugin: { + enabled: boolean; + boardTemplate: string; + defaultLayout: string; + }; +} + +export const DEFAULT_SETTINGS: PluginConfig = { + folderPlugin: { + enabled: true, + organizationHierarchy: 'Category, Status', + }, + markdownPlugin: { + enabled: true, + autoCreatePath: 'Notes', + template: 'default', + }, + kanbanPlugin: { + enabled: false, + boardTemplate: 'default', + defaultLayout: 'columns', + }, +}; +``` + +### Unified Settings Tab + +```typescript +// settings.ts +export class UnifiedSettingTab extends PluginSettingTab { + plugin: MultiPluginBase; + + display(): void { + const { containerEl } = this; + containerEl.empty(); + + containerEl.createEl('h2', { text: 'Plugin Suite Settings' }); + + // Folder Plugin Section + this.displayFolderPluginSettings(containerEl); + + // Markdown Plugin Section + this.displayMarkdownPluginSettings(containerEl); + + // Kanban Plugin Section + this.displayKanbanPluginSettings(containerEl); + } + + displayFolderPluginSettings(containerEl: HTMLElement): void { + containerEl.createEl('h3', { text: 'Folder Organization Plugin' }); + + new Setting(containerEl) + .setName('Enable Folder Plugin') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.folderPlugin.enabled) + .onChange(async (value) => { + this.plugin.settings.folderPlugin.enabled = value; + await this.plugin.saveSettings(); + }) + ); + + if (this.plugin.settings.folderPlugin.enabled) { + new Setting(containerEl) + .setName('Organization Hierarchy') + .setDesc('Comma-separated frontmatter fields') + .addTextArea((text) => + text + .setValue(this.plugin.settings.folderPlugin.organizationHierarchy) + .onChange(async (value) => { + this.plugin.settings.folderPlugin.organizationHierarchy = value; + await this.plugin.saveSettings(); + }) + ); + } + } + + displayMarkdownPluginSettings(containerEl: HTMLElement): void { + containerEl.createEl('h3', { text: 'Markdown Auto-Create Plugin' }); + + new Setting(containerEl) + .setName('Enable Markdown Plugin') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.markdownPlugin.enabled) + .onChange(async (value) => { + this.plugin.settings.markdownPlugin.enabled = value; + await this.plugin.saveSettings(); + }) + ); + + if (this.plugin.settings.markdownPlugin.enabled) { + new Setting(containerEl) + .setName('Auto-Create Path') + .setDesc('Default folder for auto-created files') + .addText((text) => + text + .setPlaceholder('Notes') + .setValue(this.plugin.settings.markdownPlugin.autoCreatePath) + .onChange(async (value) => { + this.plugin.settings.markdownPlugin.autoCreatePath = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Template') + .setDesc('Template to use for new files') + .addDropdown((dropdown) => + dropdown + .addOption('default', 'Default') + .addOption('minimal', 'Minimal') + .addOption('detailed', 'Detailed') + .setValue(this.plugin.settings.markdownPlugin.template) + .onChange(async (value) => { + this.plugin.settings.markdownPlugin.template = value; + await this.plugin.saveSettings(); + }) + ); + } + } + + displayKanbanPluginSettings(containerEl: HTMLElement): void { + containerEl.createEl('h3', { text: 'Kanban Board Plugin' }); + + new Setting(containerEl) + .setName('Enable Kanban Plugin') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.kanbanPlugin.enabled) + .onChange(async (value) => { + this.plugin.settings.kanbanPlugin.enabled = value; + await this.plugin.saveSettings(); + }) + ); + + if (this.plugin.settings.kanbanPlugin.enabled) { + new Setting(containerEl) + .setName('Board Template') + .setDesc('Default board template') + .addDropdown((dropdown) => + dropdown + .addOption('default', 'Default') + .addOption('agile', 'Agile') + .addOption('gtd', 'Getting Things Done') + .setValue(this.plugin.settings.kanbanPlugin.boardTemplate) + .onChange(async (value) => { + this.plugin.settings.kanbanPlugin.boardTemplate = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Default Layout') + .setDesc('How boards are displayed by default') + .addDropdown((dropdown) => + dropdown + .addOption('columns', 'Columns') + .addOption('rows', 'Rows') + .addOption('grid', 'Grid') + .setValue(this.plugin.settings.kanbanPlugin.defaultLayout) + .onChange(async (value) => { + this.plugin.settings.kanbanPlugin.defaultLayout = value; + await this.plugin.saveSettings(); + }) + ); + } + } +} +``` + +## Plugin Examples + +### Example 1: Folder Organization Plugin + +**Settings Interface:** +```typescript +export interface FolderOrganizerSettings { + enabled: boolean; + organizationHierarchy: string; + createMissingFolders: boolean; + overwriteExisting: boolean; + excludeFolders: string[]; +} +``` + +**Key UI Elements:** +- Toggle to enable/disable +- Text area for hierarchy configuration +- Toggle for auto-create folders +- Toggle for overwrite confirmation +- Multi-select or list for excluded folders +- Action button to organize files + +### Example 2: Markdown Auto-Create Plugin + +**Settings Interface:** +```typescript +export interface MarkdownAutoCreateSettings { + enabled: boolean; + defaultPath: string; + template: 'default' | 'minimal' | 'detailed'; + autoOpenNewFile: boolean; + defaultFrontmatter: string; + hotkey: string; +} +``` + +**Key UI Elements:** +- Toggle to enable/disable +- Text input for default path +- Dropdown for template selection +- Toggle for auto-open +- Text area for frontmatter template +- Display current hotkey (or hotkey picker if available) + +### Example 3: Kanban Board Plugin + +**Settings Interface:** +```typescript +export interface KanbanBoardSettings { + enabled: boolean; + boardTemplate: 'default' | 'agile' | 'gtd'; + defaultLayout: 'columns' | 'rows' | 'grid'; + cardWidth: number; + showDates: boolean; + showTags: boolean; + defaultColumns: string[]; + darkMode: boolean; +} +``` + +**Key UI Elements:** +- Toggle to enable/disable +- Dropdown for board templates +- Dropdown for default layout +- Slider for card width +- Toggles for feature flags +- Text area for default columns (comma-separated) +- Color picker for theme customization + +## Best Practices + +1. **Always save settings** - Call `await this.plugin.saveSettings()` after any change +2. **Use descriptive names** - Users should understand what each setting does +3. **Group related settings** - Use section headers (h3) to organize +4. **Provide defaults** - Always have sensible defaults in `DEFAULT_SETTINGS` +5. **Validate input** - Check user input before saving (trim, validate paths, etc.) +6. **Use conditionals wisely** - Show/hide advanced options based on basic settings +7. **Provide feedback** - Use `new Notice()` to confirm actions +8. **Redraw on enable/disable** - Call `this.display()` to update UI when features toggle +9. **Document complex settings** - Use `.setDesc()` liberally +10. **Test multi-step workflows** - Ensure settings changes work with all plugins enabled + +## Testing Your Settings UI + +```typescript +// In your test file +const plugin = new MyPlugin(app, manifest); +plugin.loadSettings(); +plugin.registerSettingTab(new UnifiedSettingTab(app, plugin)); + +// Settings should load without errors +// Check that all toggles/inputs work +// Verify settings persist after reload +// Test conditional displays +``` + +--- + +This guide provides a foundation for building clean, organized, and user-friendly plugin settings. Adapt these patterns to your specific needs! diff --git a/op.docs/docs/doc.zs.md b/op.docs/docs/doc.zs.md new file mode 100644 index 0000000..649b603 --- /dev/null +++ b/op.docs/docs/doc.zs.md @@ -0,0 +1,5 @@ +--- +category: docs +path: ZeroSpace\zs.docs +shouldInstallPlugins: true +--- diff --git a/op.docs/docs/op.docs.md b/op.docs/docs/op.docs.md new file mode 100644 index 0000000..2a41495 --- /dev/null +++ b/op.docs/docs/op.docs.md @@ -0,0 +1,4 @@ +--- +category: docs +path: Obsidian-Plugins\op.docs +--- diff --git a/os.tasks/.obsidian/app.json b/os.tasks/.obsidian/app.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/os.tasks/.obsidian/app.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/os.tasks/.obsidian/appearance.json b/os.tasks/.obsidian/appearance.json new file mode 100644 index 0000000..4be7969 --- /dev/null +++ b/os.tasks/.obsidian/appearance.json @@ -0,0 +1,3 @@ +{ + "theme": "obsidian" +} \ No newline at end of file diff --git a/os.tasks/.obsidian/core-plugins.json b/os.tasks/.obsidian/core-plugins.json new file mode 100644 index 0000000..639b90d --- /dev/null +++ b/os.tasks/.obsidian/core-plugins.json @@ -0,0 +1,33 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "canvas": true, + "outgoing-link": true, + "tag-pane": true, + "footnotes": false, + "properties": true, + "page-preview": true, + "daily-notes": true, + "templates": true, + "note-composer": true, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "bookmarks": true, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": true, + "word-count": true, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": true, + "publish": false, + "sync": true, + "bases": true, + "webviewer": false +} \ No newline at end of file diff --git a/os.tasks/.obsidian/workspace.json b/os.tasks/.obsidian/workspace.json new file mode 100644 index 0000000..58509fe --- /dev/null +++ b/os.tasks/.obsidian/workspace.json @@ -0,0 +1,190 @@ +{ + "main": { + "id": "5aefe3eaa01298aa", + "type": "split", + "children": [ + { + "id": "8e01d97cdc20d278", + "type": "tabs", + "children": [ + { + "id": "46c2d403c5136d23", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "Generate Build Script Project.md", + "mode": "source", + "source": false + }, + "icon": "lucide-file", + "title": "Generate Build Script Project" + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "97c9b935bff66d1d", + "type": "split", + "children": [ + { + "id": "1ecf357cfe02325f", + "type": "tabs", + "children": [ + { + "id": "19ac1b441316dcfc", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical", + "autoReveal": false + }, + "icon": "lucide-folder-closed", + "title": "Files" + } + }, + { + "id": "99feed5ad5a90deb", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + }, + "icon": "lucide-search", + "title": "Search" + } + }, + { + "id": "de452041497e4cd8", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": {}, + "icon": "lucide-bookmark", + "title": "Bookmarks" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300 + }, + "right": { + "id": "a984cdfdcdf28365", + "type": "split", + "children": [ + { + "id": "a63d615d35bc9daf", + "type": "tabs", + "children": [ + { + "id": "4b86dc95dc16032b", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "file": "Generate Build Script Project.md", + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-coming-in", + "title": "Backlinks for Generate Build Script Project" + } + }, + { + "id": "4c3dbdb8f44e7955", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "file": "Generate Build Script Project.md", + "linksCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-going-out", + "title": "Outgoing links from Generate Build Script Project" + } + }, + { + "id": "da94bd2266074d5a", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-tags", + "title": "Tags" + } + }, + { + "id": "46b74855b4b336d7", + "type": "leaf", + "state": { + "type": "all-properties", + "state": { + "sortOrder": "frequency", + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-archive", + "title": "All properties" + } + }, + { + "id": "ae88324a14179265", + "type": "leaf", + "state": { + "type": "outline", + "state": { + "file": "Generate Build Script Project.md", + "followCursor": false, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-list", + "title": "Outline of Generate Build Script Project" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300, + "collapsed": true + }, + "left-ribbon": { + "hiddenItems": { + "switcher:Open quick switcher": false, + "graph:Open graph view": false, + "canvas:Create new canvas": false, + "daily-notes:Open today's daily note": false, + "templates:Insert template": false, + "command-palette:Open command palette": false, + "bases:Create new base": false + } + }, + "active": "46c2d403c5136d23", + "lastOpenFiles": [ + "Generate Build Script Project.md" + ] +} \ No newline at end of file diff --git a/os.tasks/Generate Build Script Project.md b/os.tasks/Generate Build Script Project.md new file mode 100644 index 0000000..f1d5f40 --- /dev/null +++ b/os.tasks/Generate Build Script Project.md @@ -0,0 +1,17 @@ +--- +category: AI Task +--- + + +In OP/Build project, create a program that scans the markdown files in op.docs. + +It will grab files with the frontmatter of catergory: docs. + +For this doc files, get the frontmatter path. This will tell you the location of the docs relative to the current user root. Such as user/Obsidian-Plugins/op.docs. + +You will want to build the Obsidian plugin located in OP_plugin. + +Once it's built, you copy the contents of OP_plugin and move it to the relative plugin folders, such as ZeroSpace\zs.docs\.obsidian\plugins\ + +Delete the previous plugin obviously, if the location is already occupied. +