From 0a41296ce8c2ef4757fac4955678c97b7571c3c3 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Fri, 24 Apr 2026 18:34:32 -0500 Subject: [PATCH] docs: rewrite README and trim docs/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite README to lead with what SharpFM does (extract FileMaker objects as editable XML and round-trip them back) and why it's useful (version control, cross-RDP/Citrix, real editors, inspection). Drop the step-by-step menu/plugin walkthroughs in favor of a tighter How It Works section. Remove obsolete docs: - docs/plans/ — POCO migration is complete. - docs/step-definitions.md — the POCO migration tracker; all Priority 1 steps have typed implementations and the Priority 3 long tail is explicitly on-demand. - docs/plugins/ — described four plugin types (Panel, Event, Persistence, Transform) but only Panel and Transform exist; replaced with a minimal docs/plugins.md pointer to the contracts and sample project. Update docs/advanced-filemaker-scripting-syntax.md to note that Show Custom Dialog's Inputs: block is already implemented, and drop the single-entry change log. --- README.md | 108 +-- docs/advanced-filemaker-scripting-syntax.md | 11 +- docs/plans/script-step-poco-sweep.md | 217 ----- docs/plugins.md | 7 + docs/plugins/event-plugins.md | 72 -- docs/plugins/host-api.md | 69 -- docs/plugins/overview.md | 51 -- docs/plugins/panel-plugins.md | 73 -- docs/plugins/persistence-plugins.md | 85 -- docs/plugins/transform-plugins.md | 63 -- docs/step-definitions.md | 921 -------------------- 11 files changed, 43 insertions(+), 1634 deletions(-) delete mode 100644 docs/plans/script-step-poco-sweep.md create mode 100644 docs/plugins.md delete mode 100644 docs/plugins/event-plugins.md delete mode 100644 docs/plugins/host-api.md delete mode 100644 docs/plugins/overview.md delete mode 100644 docs/plugins/panel-plugins.md delete mode 100644 docs/plugins/persistence-plugins.md delete mode 100644 docs/plugins/transform-plugins.md delete mode 100644 docs/step-definitions.md diff --git a/README.md b/README.md index b63c70d..dfa4054 100644 --- a/README.md +++ b/README.md @@ -1,107 +1,65 @@ # SharpFM -SharpFM is a cross-platform (Mac, Windows, Linux) FileMaker Pro Developer Utility for migrating FileMaker code between files, sharing code with other developers, or inspecting what's happening under the hood. Copy scripts, layouts, tables, and fields from FileMaker Pro, edit them in structured editors, and paste them back -- across machine barriers like Remote Desktop, Citrix, or anything that supports plain text. +**Get FileMaker objects out of FileMaker, edit or share them, and paste them back.** -## Getting Started - -Head over to [Releases](https://github.com/fuzzzerd/SharpFM/releases) and grab the latest version for your platform (Windows, Mac, Linux). - -SharpFM and FileMaker must be running on the same computer. To share clips across machines, use the XML files directly. - -### Importing Clips from FileMaker - -1. Open SharpFM and FileMaker side by side. -2. In FileMaker, copy something to the clipboard (scripts, tables, layouts, etc). -3. In SharpFM, use **Edit > Paste from FileMaker** (`Ctrl+V`). -4. The clip appears in the left panel with the appropriate editor on the right. - -### Exporting Clips to FileMaker +SharpFM is a cross-platform (Windows, macOS, Linux) developer utility for FileMaker Pro. It captures the proprietary clipboard format FileMaker uses for scripts, tables, fields, and layouts, exposes it as plain XML plus structured editors, and lets you paste the result back into FileMaker when you're done. -1. Select a clip in the left panel. -2. Use **Edit > Copy to FileMaker** (`Ctrl+Shift+C`). -3. Switch to FileMaker and open the appropriate destination (Database Manager, Script Workspace, Layout mode, etc). -4. Paste as you normally would. +If you've ever wanted to diff two scripts, commit a table definition to git, or move code across a Remote Desktop / Citrix boundary that strips custom clipboard formats — that's what SharpFM is for. -### Creating New Clips +## Why SharpFM -- **File > New Script** (`Ctrl+N`) -- creates an empty script clip with the FmScript editor. -- **File > New Table** (`Ctrl+Shift+N`) -- creates an empty table clip with the DataGrid editor. +FileMaker's clipboard is a closed format. Scripts, tables, and layouts never really leave FileMaker — the only way out is a copy/paste into another FileMaker session on the same machine. SharpFM sits in the middle of that copy/paste: it decodes the clipboard into plain XML, gives you real editors to work with it, and re-encodes it on the way back. Whatever you do in between — edit, save, diff, share — is up to you. -### Editing Scripts +That middle step unlocks a few things: -Select a script clip to open the plain-text script editor with FmScript syntax highlighting, autocomplete, bracket matching, and validation diagnostics. Edit the script text directly -- changes sync to the underlying XML automatically. +- **Version control FileMaker code.** Save clips as XML files, drop them in a git repo, review diffs like any other source. +- **Share snippets with other developers.** XML files travel through any text-based channel — chat, gists, shared folders. +- **Cross Remote Desktop / Citrix boundaries.** Those environments often strip custom clipboard data. Plain text survives. +- **Edit in a real editor.** Scripts get syntax highlighting, autocomplete, and validation. Tables get a spreadsheet-style grid with a calculation editor. +- **Inspect what FileMaker is actually doing.** See the raw XML behind any clip and learn the underlying structure. -### Editing Tables +## How It Works -Select a table clip to open the DataGrid editor with columns for Field Name, Type, Kind, Required, Unique, and Comment. Use **+ Add Field** to add fields, **Remove** or `Delete` to remove them. Change a field's Kind to Calculated or Summary, then click **Edit Calculation...** for the calculation editor. +1. **Copy from FileMaker.** Select a script, table, field set, or layout in FileMaker and copy it. +2. **Paste into SharpFM** (`Ctrl+V` / `Edit > Paste from FileMaker`). The clip appears in the tree on the left; the matching editor opens on the right. +3. **Edit or save.** Modify the script text, tweak field definitions, or save the clip as an XML file to share or commit. +4. **Copy back to FileMaker** (`Ctrl+Shift+C` / `Edit > Copy to FileMaker`), then paste into the Script Workspace, Database Manager, or Layout mode. -### Viewing Raw XML +SharpFM and FileMaker need to run on the same machine for clipboard hand-off. To move clips between machines, share the XML files directly. -Use the **XML Viewer** plugin (`Ctrl+Shift+X`) to open a live XML panel alongside any structured editor. Edits in either direction sync automatically -- change the script and the XML updates, edit the XML and the script rebuilds. +## Getting Started -### Saving and Loading Clips +Grab the latest build for your platform from [Releases](https://github.com/fuzzzerd/SharpFM/releases). -SharpFM persists clips as XML files in a local folder. +## Features -- **File > Save All** (`Ctrl+S`) -- saves all clips to the current folder. -- **File > Open Folder...** -- load clips from a different folder. -- Clip files are plain XML and can be shared via git, email, or any text-based tool. +- **Round-trip clipboard support** for FileMaker scripts, tables, fields, and layouts. +- **FmScript editor** with syntax highlighting, autocomplete, bracket matching, and inline validation diagnostics. +- **Table/field grid editor** with inline editing, type and kind selection, and a dedicated calculation editor for calculated and summary fields. +- **Live XML view** (`Ctrl+Shift+X`) — structured edits and raw XML stay in sync, either direction. +- **New clips from scratch** — start an empty script (`Ctrl+N`) or table (`Ctrl+Shift+N`) without needing to copy from FileMaker first. +- **Tree browser with VS Code-style tabs** for working across multiple clips at once. +- **Plain-XML storage** — clips are files on disk, shareable by any text-based channel. ## Keyboard Shortcuts | Shortcut | Action | |----------|--------| -| `Ctrl+N` | New Script | -| `Ctrl+Shift+N` | New Table | | `Ctrl+V` | Paste from FileMaker | | `Ctrl+Shift+C` | Copy to FileMaker | +| `Ctrl+N` | New Script | +| `Ctrl+Shift+N` | New Table | | `Ctrl+S` | Save All | | `Ctrl+Shift+X` | Toggle XML Viewer | -## Menu Reference - -| Menu | Items | -|------|-------| -| **File** | New Script, New Table, Open Folder..., Save All, Exit | -| **Edit** | Paste from FileMaker, Copy to FileMaker, Copy as C# Class | -| **Plugins** | Loaded plugins (toggle panels), Manage Plugins... | - -## Features - -- Copy FileMaker Scripts, Tables, Fields, and Layouts to their XML representation and back. -- Persist clips as XML files shareable via git, email, or other text-based tools. -- Plain-text script editor with FmScript syntax highlighting, autocomplete, and validation. -- DataGrid table/field editor with inline editing, calculation editor, and type/kind selection. -- Live bidirectional XML viewer -- edit XML or structured data, both stay in sync. -- Extensible plugin architecture for adding custom panels and tools. - -## Plugins - -SharpFM supports plugins via the `SharpFM.Plugin` contract library. Plugins are loaded from the `plugins/` directory at startup and can be managed from **Plugins > Manage Plugins...**. - -### Bundled Plugins - -- **XML Viewer** -- Live XML panel with syntax highlighting and bidirectional sync (`Ctrl+Shift+X`). -- **Clip Inspector** -- Displays clip metadata (name, type, element count, size). - -### Writing a Plugin - -1. Create a .NET 10 class library referencing `SharpFM.Plugin`. -2. Implement `IPanelPlugin` -- provide an `Id`, `DisplayName`, `CreatePanel()` returning an Avalonia `Control`. -3. Use `IPluginHost` in `Initialize()` to observe clip selection changes and content updates. -4. Optionally register keyboard shortcuts via `KeyBindings` and custom menu actions via `MenuActions`. -5. Build the DLL and drop it in the `plugins/` directory. - -See `src/SharpFM.Plugin.Sample/` for a complete working example. - ## Troubleshooting -Logs are stored in `${specialfolder:folder=CommonApplicationData}\SharpFM` and are automatically rotated after thirty days. +Logs are stored in `${specialfolder:folder=CommonApplicationData}\SharpFM` and rotate after thirty days. -## Similar Mac OS / Apple Based Developer Utilities +## Similar Tools -- Apple Script utility: -- FileMaker based Generator: +- [FmClipTools](https://github.com/DanShockley/FmClipTools) — AppleScript-based clipboard utilities (macOS only). +- [Generator](https://github.com/proofgeist/generator) — FileMaker-based code generator. ## App Icon diff --git a/docs/advanced-filemaker-scripting-syntax.md b/docs/advanced-filemaker-scripting-syntax.md index d18905f..17686fc 100644 --- a/docs/advanced-filemaker-scripting-syntax.md +++ b/docs/advanced-filemaker-scripting-syntax.md @@ -68,7 +68,9 @@ Bulk or structured state that doesn't reduce to a single flag: - `; Buttons: ["OK" commit; "Cancel" nocommit; "" nocommit]` — used by Show Custom Dialog for its button configuration. -- Future bulk state (Input Field specs, etc.) takes this form. +- `; Inputs: [Table::Field "Label" password; ...]` — used by Show + Custom Dialog for its input field specs. +- Additional bulk state surfaced in the future takes this same shape. ## Parsing precedence @@ -177,10 +179,3 @@ per-step extension. Covered here for completeness. - `PerformScriptStep.FromDisplayParams`, `GoToLayoutStep.FromDisplayParams` — Form 1 regex parsers for named refs with `(#id)` suffixes. - -## Change log - -- **2026-04** — Extracted from `docs/step-definitions.md:44-69` into - its own document as part of the POCO big-bang migration. Rationale - section ("what to drop vs. surface") added with `Restore` on `If` as - the canonical drop example. diff --git a/docs/plans/script-step-poco-sweep.md b/docs/plans/script-step-poco-sweep.md deleted file mode 100644 index 2c46745..0000000 --- a/docs/plans/script-step-poco-sweep.md +++ /dev/null @@ -1,217 +0,0 @@ -# Script Step POCO Migration — Sweep-Phase Plan - -Status: completed on `fuzzz/all-pocos`. All 205 script steps are typed -`IStepFactory` POCOs discovered via `StepRegistry`. This doc is kept as -a record of the sweep's approach; new step POCOs (e.g. after a FileMaker -release that adds a step) should follow the same per-step TDD workflow -below. - -## Context - -The [pilot](../advanced-filemaker-scripting-syntax.md) established the -typed POCO pattern with three representative steps: `BeepStep`, -`SetErrorCaptureStep`, `IfStep`. The sweep covers the remaining 189 -FileMaker script steps (206 total − 14 pre-existing POCOs − 3 pilot). -When the sweep finishes, all 206 steps are typed POCOs, -`StepCatalogLoader` and the catalog-driven helpers are deleted, and -the `step-catalog-en.json` file is no longer embedded. - -The sweep is not a single PR. Each wave lands independently, leaves the -tree green, and can be reviewed in isolation. - -## Tier-based ordering - -Run the catalog through the tier filter from the earlier analysis (see -message history in the pilot branch's `agent/catalogs` walkthrough): - -| Tier | Shape | Count | Rationale | -|---|---|---|---| -| A | Zero-param | 43 | Trivially mechanical; establish the cadence. | -| B | Only boolean/enum params | 29 | Locks in `ParamMetadata.ValidValues` patterns. | -| C | Text-bearing | 7 | Introduces text-escaping and display bracket grammar. | -| D | Calc / field / named-ref / complex | ~110 | The variety bucket. Contains real work — advanced-syntax decisions land here. | - -Migrate in A → B → C → D order. Within a tier, batch by FileMaker -category (`control`, `windows`, `fields`, etc.) to keep related steps' -PRs discoverable. - -## Per-step TDD workflow - -For each step, in this order: - -1. **Copy canonical XML** from - `C:\source\personal\agentic-fm\agent\snippet_examples\steps\\.xml` - (outside this repo — read-only reference, never committed) into an - inline `const string` fixture in the new test class. -2. **Write the round-trip test first.** Parse the fixture → run through - the POCO's `Metadata.FromXml!(el).ToXml()` → assert - `XNode.DeepEquals` against the source. Fails because the POCO - doesn't exist yet. -3. **Write the POCO** — implement `IStepFactory`, define `Metadata`, - implement `ToXml` / `ToDisplayLine` / `FromXml` / - `FromDisplayParams`. Pattern from `BeepStep.cs` (zero-param), - `SetErrorCaptureStep.cs` (single-boolean), or `IfStep` (block-pair, - calc) — pick the closest match. -4. **Add the display tests.** `ToDisplayLine()` returns expected - string; `FromDisplay(Metadata.FromDisplay!(...))` survives a - round-trip. -5. **Add the disabled-step test.** `enable="False"` → `step.Enabled` - is false → `ToXml()` emits `enable="False"`. -6. **Complete the zero-loss audit** in the POCO's XML doc comment. - Every XML element/attribute either rendered natively by FM Pro, - covered by an advanced-syntax extension, or intentionally dropped - with rationale (see - [`advanced-filemaker-scripting-syntax.md`](../advanced-filemaker-scripting-syntax.md)). -7. **For Tier D steps that need advanced syntax**, land the syntax - extension behind its own commit before the POCO that uses it, so - reviewers can validate the grammar decision in isolation. - -## Rollout cadence - -- **One PR per batch of ~10 POCOs within a tier.** Smaller is too - noisy; larger is unreviewable. -- **Tier A fits in 4-5 PRs.** Tier B in 3. Tier C in 1. Tier D is - where PR planning gets real — expect 12-15 PRs depending on - per-step complexity. -- **Each PR's diff is only steps + their tests.** No consumer rewires, - no deletions (with the specific exceptions listed below). -- **Commit per POCO within a PR** so each step is individually - revertable if the pattern needs adjusting. - -## Consumer migration schedule - -Consumers of the legacy `StepCatalogLoader` / `StepCatalogGenerated` -flip to `StepRegistry` as their dependencies complete: - -| Consumer | Flips when | What changes | -|---|---|---| -| `FmScriptCompletionProvider` | Already migrated in pilot | — | -| `ScriptValidator` | All block-pair partners (If/End If, Loop/End Loop, Open/Commit Transaction, etc.) are POCOs | `definition.BlockPair` reads become `StepRegistry.ByName[name].BlockPair` | -| `ScriptTextParser.FromDisplayLine` | All catalog-name resolutions have POCO equivalents | Drops the `StepCatalogLoader.ByName.TryGetValue` fallback | -| `FmScript.ApplyUpdate` | Typed setters exist on every POCO | MCP apply-op dispatches to POCO-specific `With*` methods instead of generic `CatalogXmlBuilder.UpdateParam` | - -Until its flip point, each consumer keeps reading the legacy surface — -the pilot's `StepRegistry` bridge populates `StepXmlFactory` / -`StepDisplayFactory` so this works transparently. - -## Deletion schedule - -In order: - -1. **Per-wave**: as a tier completes, delete any catalog-helper code - paths that are now exclusively serving that tier's step shapes. - (Usually none — the helpers are shape-agnostic.) -2. **After Tier D completes** — when all 206 steps are POCOs and all - consumers above have flipped: - - Delete `src/SharpFM.Model/Scripting/StepCatalogLoader.cs`. - - Delete `src/SharpFM.Model/Scripting/IStepCatalog.cs`. - - Delete `src/SharpFM.Model/Scripting/Serialization/StepXmlFactory.cs`. - - Delete `src/SharpFM.Model/Scripting/Serialization/StepDisplayFactory.cs`. - - Remove the `StepRegistry → legacy factory` bridge from - `StepRegistry.Scan()` — now dead code. - - Delete `src/SharpFM.Model/Scripting/Steps/RawStep.cs`. - - Delete `src/SharpFM.Model/Scripting/Steps/RawStepAllowList.cs` - and its tests. - - Delete `src/SharpFM.Model/Scripting/Serialization/CatalogXmlBuilder.cs`. - - Delete `src/SharpFM.Model/Scripting/Serialization/CatalogParamExtractor.cs`. - - Delete `src/SharpFM.Model/Scripting/Serialization/CatalogDisplayRenderer.cs`. - - Delete `src/SharpFM.Model/Scripting/Serialization/CatalogValidator.cs`. - - Remove the `` - line from `src/SharpFM.Model/SharpFM.Model.csproj`. - - Delete `src/SharpFM.Model/Scripting/Catalog/step-catalog-en.json`. - - Introduce `UnknownStep` in place of `RawStep` for forward-compat - against future FileMaker step additions (wraps raw XElement; - same semantics as the retired `RawStep` when `Definition == null`). -3. **After ship verification** — `StepDefinition` / `StepParam` / - `StepBlockPair` / `BlockPairRole` / `StepEnumValue` no longer have - any catalog-side producers. They may be kept (used by - `StepMetadata.BlockPair`) or retired depending on whether the new - metadata types fully absorb them. Expect a small cleanup commit - here. - -## Coverage verification - -One test asserts every step catalogued by upstream is present in our -registry: - -```csharp -[Fact] -public void StepRegistry_CoversFullCatalog() -{ - var expected = File.ReadAllText("path/to/canonical/step-list.json"); - var names = JsonSerializer.Deserialize(expected); - foreach (var name in names) - Assert.True(StepRegistry.ByName.ContainsKey(name), - $"Missing POCO for step '{name}'"); -} -``` - -The canonical list is a snapshot of agentic-fm step names taken at -sweep kickoff (checked in as a one-off fixture — not the same as the -JSON catalog, just names + ids). As steps are added to upstream later, -the fixture is updated deliberately, preventing silent drift. - -## Regression containment - -The pilot's `FmScriptCompletionProvider` regression (only 3 steps -suggested in completions) resolves as the sweep progresses. Gate each -sweep PR on **monotonically increasing completion coverage**: - -- `CompletionProviderTests` has an `EmptyLine_SuggestsPocoStepNames` - test that enumerates all expected pilot step names. -- Each sweep PR adds the new step names to that test's expected list. -- A PR that removes a name from the list is a review blocker unless - justified (e.g. a step was consolidated). - -After Tier A merges, the list is 43 + 14 + 3 = 60 steps. After Tier B, -89. After Tier C, 96. After Tier D, 206. Upstream adds thereafter append. - -## Risks and gotchas (filled in during pilot execution) - -> This section gets populated with concrete discoveries from pilot -> implementation. Left as a placeholder until pilot lands. - -Observed so far: - -- **`StepRegistry` lazy init doesn't run before pre-existing consumers - touch `StepXmlFactory`.** Mitigation: single `[ModuleInitializer]` - on `StepRegistry` itself. Acceptable — one initializer on the - registry, not per-POCO. -- **Legacy `FmScript.ToDisplayLines` reads `step.Definition.BlockPair` - for indent decisions.** POCOs passing `Definition = null` break - indentation on block-pair steps. Mitigation for pilot: project a - minimal `StepDefinition` with just `BlockPair` populated into - `base(...)`. Cleanup: when `FmScript.ToDisplayLines` migrates to - `StepRegistry`, drop the synthesized `Definition`. -- **Tests that used `Beep` as a "catalog-but-not-POCO" canary break - when Beep migrates.** Mitigation: swap to `Halt Script` (or - whatever zero-param step is still on the catalog-only path). Expect - similar swaps as each subsequent zero-param step migrates. - -## Open questions deferred from pilot - -- **Plugin-scenario multi-assembly registration.** Today - `StepRegistry.Scan()` reflects only the model assembly. If a plugin - ships its own POCOs (future scenario), should the registry scan - loaded plugin assemblies too? Probable answer: yes, with an - `RegisterAssembly(Assembly)` public method plugins call on load. - Address when the first plugin author asks for it. -- **`Metadata.Notes` wiring into UI.** The pilot populates `StepNotes` - on `SetErrorCaptureStep` and `IfStep`. No UI reads it yet. Tooltip - and hover integration is a post-sweep concern — sequencing depends - on Monaco / Avalonia tooltip API decisions outside this migration's - scope. -- **`Metadata.MonacoSnippet` or synthesis.** Pilot dropped the - catalog's `MonacoSnippet` field entirely; completion falls back to - the step name for self-closing steps and provides nothing for - multi-param ones. Options: (a) let each POCO author an optional - snippet string, (b) synthesize at runtime from `ParamMetadata`, (c) - defer snippets until someone complains. Recommend (c) — the three - pilot POCOs work fine without; revisit after Tier B lands. -- **When to retire `StepDefinition`/`StepParam` records.** The new - metadata types (`StepMetadata` / `ParamMetadata`) are intended to - replace them, but `StepMetadata.BlockPair` currently reuses - `StepBlockPair` and `IfStep` still synthesizes a `StepDefinition` - for legacy `FmScript.ToDisplayLines`. Clean-up sequence TBD — - probably a small follow-up commit after the legacy factories are - deleted. diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 0000000..5fc3980 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,7 @@ +# Plugins + +SharpFM has a plugin system, currently in development. + +The plugin contracts live in [`src/SharpFM.Plugin/`](../src/SharpFM.Plugin/) and [`src/SharpFM.Plugin.UI/`](../src/SharpFM.Plugin.UI/). [`src/SharpFM.Plugin.Sample/`](../src/SharpFM.Plugin.Sample/) is a working reference plugin. + +Plugins are loaded from a `plugins/` directory next to the SharpFM executable, or installed through **Plugins > Manage Plugins...** in the app. diff --git a/docs/plugins/event-plugins.md b/docs/plugins/event-plugins.md deleted file mode 100644 index 08e5398..0000000 --- a/docs/plugins/event-plugins.md +++ /dev/null @@ -1,72 +0,0 @@ -# Event Plugins - -Event plugins are headless — they react to host events with no UI panel. - -## Interface - -```csharp -public interface IEventPlugin : IPlugin { } -``` - -`IEventPlugin` is a marker interface. All behavior comes from subscribing to `IPluginHost` events in `Initialize()` and unsubscribing in `Dispose()`. - -## When to Use - -- Auto-formatters that reformat XML on clip selection -- Linters that validate clip structure and post diagnostics -- Sync agents that mirror clips to an external system -- Analytics or telemetry - -## Available Events - -| Event | When | -|-------|------| -| `SelectedClipChanged` | User selects a different clip | -| `ClipContentChanged` | Clip XML changes (user edit or plugin push) | -| `ClipCollectionChanged` | Clips added, removed, or reloaded | - -## Host Capabilities - -- `AllClips` — read the full clip collection for bulk operations -- `ShowStatus(message)` — display feedback in the status bar -- `UpdateSelectedClipXml(xml, pluginId)` — push XML changes back to the editor - -## Example: Auto-Formatter - -```csharp -public class AutoFormatPlugin : IEventPlugin -{ - public string Id => "auto-format"; - public string DisplayName => "Auto Formatter"; - public string Version => "1.0.0"; - public IReadOnlyList KeyBindings => []; - public IReadOnlyList MenuActions => []; - - private IPluginHost? _host; - - public void Initialize(IPluginHost host) - { - _host = host; - _host.SelectedClipChanged += OnClipChanged; - } - - private void OnClipChanged(object? sender, ClipInfo? clip) - { - if (clip is null) return; - var formatted = FormatXml(clip.Xml); - if (formatted != clip.Xml) - { - _host!.UpdateSelectedClipXml(formatted, Id); - _host.ShowStatus("Clip auto-formatted"); - } - } - - private static string FormatXml(string xml) => xml; // your formatting logic - - public void Dispose() - { - if (_host is not null) - _host.SelectedClipChanged -= OnClipChanged; - } -} -``` diff --git a/docs/plugins/host-api.md b/docs/plugins/host-api.md deleted file mode 100644 index 8cd33f8..0000000 --- a/docs/plugins/host-api.md +++ /dev/null @@ -1,69 +0,0 @@ -# IPluginHost API Reference - -The `IPluginHost` interface is provided to plugins during `Initialize()`. It exposes the host application's state and services. - -## Properties - -### `ClipInfo? SelectedClip` - -The currently selected clip, or `null` if nothing is selected. Returns a snapshot — reading it multiple times may return different instances if the clip changed. - -### `IReadOnlyList AllClips` - -All clips currently loaded in the application. Useful for plugins that operate across the full clip set (linters, search indexers, sync agents). - -## Events - -### `SelectedClipChanged` - -```csharp -event EventHandler SelectedClipChanged; -``` - -Raised when the user selects a different clip in the list. The argument is the new clip, or `null` if deselected. - -### `ClipContentChanged` - -```csharp -event EventHandler ClipContentChanged; -``` - -Raised when clip content changes. Check `args.Origin` to determine the source: -- `"editor"` — user edited in the structured editor (debounced) -- Plugin ID — a plugin pushed XML changes (immediate) - -```csharp -public record ClipContentChangedArgs(ClipInfo Clip, string Origin, bool IsPartial); -``` - -`IsPartial` is `true` when the XML was produced from an incomplete parse (e.g., user is mid-edit in the script editor). - -### `ClipCollectionChanged` - -```csharp -event EventHandler? ClipCollectionChanged; -``` - -Raised when clips are added, removed, or the collection is reloaded. - -## Methods - -### `UpdateSelectedClipXml(string xml, string originPluginId)` - -Replace the XML content of the currently selected clip. The host syncs the new XML back to the structured editor automatically. Pass your plugin's `Id` as `originPluginId` for origin tagging — you'll receive your own change back via `ClipContentChanged` but can skip it by checking `args.Origin == Id`. - -### `RefreshSelectedClip() -> ClipInfo?` - -Flush the editor's in-progress state to XML and return a fresh snapshot. Use this before reading `SelectedClip` if you need XML that reflects any uncommitted edits in the structured editors. - -### `ShowStatus(string message)` - -Display a message in the status bar. Use this for user-visible feedback. The message auto-clears after a few seconds. - -## ClipInfo - -```csharp -public record ClipInfo(string Name, string ClipType, string Xml); -``` - -A read-only snapshot of clip metadata and content. `ClipType` is the FileMaker clipboard format (e.g., `"Mac-XMSS"` for script steps, `"Mac-XMTB"` for tables). diff --git a/docs/plugins/overview.md b/docs/plugins/overview.md deleted file mode 100644 index f95f6e9..0000000 --- a/docs/plugins/overview.md +++ /dev/null @@ -1,51 +0,0 @@ -# SharpFM Plugin System - -SharpFM supports four types of plugins, all sharing a common base interface (`IPlugin`) for metadata and lifecycle. - -## Plugin Types - -| Type | Interface | Purpose | -|------|-----------|---------| -| **Panel** | `IPanelPlugin` | Sidebar UI panels that display clip data | -| **Event** | `IEventPlugin` | Headless handlers that react to host events | -| **Persistence** | `IPersistencePlugin` | Alternative storage backends (cloud, database) | -| **Transform** | `IClipTransformPlugin` | Modify clip XML during import/export | - -## Getting Started - -### 1. Create a Class Library - -Create a new .NET 10 class library and reference `SharpFM.Plugin`: - -```xml - - - net10.0 - - - - - -``` - -### 2. Implement a Plugin Interface - -Choose the interface that matches your use case and implement it. Every plugin must provide: - -- `Id` — unique identifier (e.g., `"my-plugin"`) -- `DisplayName` — shown in the Plugins menu -- `Version` — shown in the Plugin Manager -- `Initialize(IPluginHost host)` — called once at startup -- `Dispose()` — cleanup when unloaded - -### 3. Build and Install - -Build your plugin as a DLL and install it via the Plugin Manager ("Install from File...") or copy it to the `plugins/` directory next to the SharpFM executable. - -## Discovery - -SharpFM scans the `plugins/` directory at startup for `.dll` files. Each assembly is loaded in its own `AssemblyLoadContext` and reflected for types implementing `IPlugin` subtypes. Each class is categorized into exactly one plugin type based on the interface it implements. - -## Licensing - -SharpFM and its plugin interfaces are licensed under the GNU General Public License v3. diff --git a/docs/plugins/panel-plugins.md b/docs/plugins/panel-plugins.md deleted file mode 100644 index 98acfb9..0000000 --- a/docs/plugins/panel-plugins.md +++ /dev/null @@ -1,73 +0,0 @@ -# Panel Plugins - -Panel plugins provide a sidebar UI panel in the SharpFM main window. - -## Interface - -```csharp -public interface IPanelPlugin : IPlugin -{ - Control CreatePanel(); -} -``` - -`CreatePanel()` is called once when the user activates the plugin. Only one panel plugin can be active at a time. Toggling the same plugin closes it; toggling a different one switches the panel content. - -## Lifecycle - -1. `Initialize(IPluginHost host)` — subscribe to host events here -2. `CreatePanel()` — create your Avalonia `Control` (called when plugin is toggled on) -3. `Dispose()` — unsubscribe from events and clean up - -## Subscribing to Events - -```csharp -public void Initialize(IPluginHost host) -{ - _host = host; - _host.SelectedClipChanged += OnClipChanged; - _host.ClipContentChanged += OnContentChanged; -} -``` - -## Bidirectional Sync - -If your plugin edits clip XML, use origin tagging to avoid feedback loops: - -```csharp -// Push changes to the host -_host.UpdateSelectedClipXml(newXml, Id); - -// Skip your own updates -private void OnContentChanged(object? sender, ClipContentChangedArgs args) -{ - if (args.Origin == Id) return; // I caused this change - _viewModel.LoadClip(args.Clip); -} -``` - -## Example: Minimal Read-Only Panel - -```csharp -public class MyPlugin : IPanelPlugin -{ - public string Id => "my-plugin"; - public string DisplayName => "My Plugin"; - public string Version => "1.0.0"; - public IReadOnlyList KeyBindings => []; - public IReadOnlyList MenuActions => []; - - private IPluginHost? _host; - - public void Initialize(IPluginHost host) => _host = host; - - public Control CreatePanel() - { - var text = new TextBlock { Text = _host?.SelectedClip?.Name ?? "No clip" }; - _host!.SelectedClipChanged += (_, clip) => text.Text = clip?.Name ?? "No clip"; - return text; - } - - public void Dispose() { } -} -``` diff --git a/docs/plugins/persistence-plugins.md b/docs/plugins/persistence-plugins.md deleted file mode 100644 index 4a81bd0..0000000 --- a/docs/plugins/persistence-plugins.md +++ /dev/null @@ -1,85 +0,0 @@ -# Persistence Plugins - -Persistence plugins provide alternative storage backends for clips. - -## Interfaces - -### IPersistencePlugin - -```csharp -public interface IPersistencePlugin : IPlugin -{ - IClipRepository CreateRepository(); -} -``` - -The plugin handles discovery and lifecycle. `CreateRepository()` is called when the user selects this storage provider. - -### IClipRepository - -```csharp -public interface IClipRepository -{ - string ProviderName { get; } - string CurrentLocation { get; } - bool SupportsLocationPicker { get; } - Task> LoadClipsAsync(); - Task SaveClipsAsync(IReadOnlyList clips); - Task PickLocationAsync(); -} -``` - -The repository handles all data operations. Methods are async to support remote backends. - -### ClipData - -```csharp -public record ClipData(string Name, string ClipType, string Xml); -``` - -The persistence DTO. Separate from `ClipInfo` (which is used for plugin notifications) so the two can evolve independently. - -## Built-In vs Plugin Storage - -The built-in file system storage (`ClipRepository`) implements `IClipRepository` directly — it is **not** a plugin. Plugin-provided backends come through `IPersistencePlugin`. - -At startup, the host builds a list of available repositories: -1. The built-in file system repository -2. One from each loaded `IPersistencePlugin` via `CreateRepository()` - -## Example: Cloud API Storage Plugin - -```csharp -public class CloudStoragePlugin : IPersistencePlugin -{ - public string Id => "cloud-storage"; - public string DisplayName => "Cloud Storage"; - public string Version => "1.0.0"; - public IReadOnlyList KeyBindings => []; - public IReadOnlyList MenuActions => []; - - public void Initialize(IPluginHost host) { } - public IClipRepository CreateRepository() => new CloudRepository(); - public void Dispose() { } -} - -public class CloudRepository : IClipRepository -{ - public string ProviderName => "Cloud API"; - public string CurrentLocation => "https://api.example.com/clips"; - public bool SupportsLocationPicker => false; - - public async Task> LoadClipsAsync() - { - // Fetch clips from your API - return []; - } - - public async Task SaveClipsAsync(IReadOnlyList clips) - { - // Push clips to your API - } - - public Task PickLocationAsync() => Task.FromResult(null); -} -``` diff --git a/docs/plugins/transform-plugins.md b/docs/plugins/transform-plugins.md deleted file mode 100644 index 2b87ad4..0000000 --- a/docs/plugins/transform-plugins.md +++ /dev/null @@ -1,63 +0,0 @@ -# Transform Plugins - -Transform plugins modify clip XML during import (paste/load) and export (copy to clipboard) operations. - -## Interface - -```csharp -public interface IClipTransformPlugin : IPlugin -{ - Task OnImportAsync(string clipType, string xml); - Task OnExportAsync(string clipType, string xml); - bool IsEnabled { get; set; } -} -``` - -## When to Use - -- Strip internal FileMaker IDs when pasting clips between files -- Inject boilerplate fields into table definitions -- Rename table/field references during refactoring -- Sanitize clips before sharing with other developers -- Convert between FileMaker versions - -## Behavior - -- **Import**: Runs when clips are pasted from FileMaker or loaded from storage -- **Export**: Runs when clips are copied to the FileMaker clipboard -- **Ordering**: Transforms run in plugin load order (DLL discovery order) -- **Passthrough**: Return the input `xml` unchanged to skip transformation -- **IsEnabled**: Users can toggle transforms on/off without uninstalling - -## Example: Strip Internal IDs - -```csharp -public class StripIdsPlugin : IClipTransformPlugin -{ - public string Id => "strip-ids"; - public string DisplayName => "Strip Internal IDs"; - public string Version => "1.0.0"; - public bool IsEnabled { get; set; } = true; - public IReadOnlyList KeyBindings => []; - public IReadOnlyList MenuActions => []; - - public void Initialize(IPluginHost host) { } - - public Task OnImportAsync(string clipType, string xml) - { - if (!IsEnabled) return Task.FromResult(xml); - - // Strip id="..." attributes from elements - var cleaned = Regex.Replace(xml, @"\s+id=""\d+""", ""); - return Task.FromResult(cleaned); - } - - public Task OnExportAsync(string clipType, string xml) - { - // No transformation on export - return Task.FromResult(xml); - } - - public void Dispose() { } -} -``` diff --git a/docs/step-definitions.md b/docs/step-definitions.md deleted file mode 100644 index e4532f6..0000000 --- a/docs/step-definitions.md +++ /dev/null @@ -1,921 +0,0 @@ -# FileMaker Script Step — Typed POCO Migration Tracker - -This document is the working checklist for migrating FileMaker script steps from the generic `RawStep` fallback to typed POCOs under `src/SharpFM.Model/Scripting/Steps/`. It exists to drive TDD: each step block holds a placeholder for verbatim FileMaker Pro clipboard XML, a list of the expected display-text form, and a checklist for the small refactor needed to land the typed POCO. - -**Rule of thumb:** we only add a typed POCO when a step has behavior the generic catalog-driven pipeline can't express (named references with ids, discriminated unions, bespoke display formats, nested calculations, custom parsing). Simple steps stay on `RawStep` indefinitely — that's fine, `RawStep` is fully lossless for the source XML. - ---- - -## How to use this document - -For each step you want to migrate: - -1. **Copy verbatim XML from FileMaker Pro.** Create the step in the script workspace, right-click the step in the script and choose Copy, then paste directly into the sample block here. Do this once per meaningful permutation (e.g. each `LayoutDestination` for Go to Layout, each `RowPageLocation` for Go to Record). -2. **Write failing tests first.** Create `tests/SharpFM.Tests/Scripting/Steps/{StepName}StepTests.cs` with the verbatim fixture as a constant and one test per behavior assertion: display rendering, round-trip XML preservation, and display-text → XML parsing. -3. **Run the tests and watch them fail.** They'll fail against the current `RawStep` path with whatever generic output the catalog produces. -4. **Add the typed POCO.** Create `src/SharpFM.Model/Scripting/Steps/{StepName}Step.cs` following the `GoToLayoutStep` pattern. Register with `StepXmlFactory` and `StepDisplayFactory` via a `[ModuleInitializer]`. -5. **Run the tests and watch them pass.** -6. **Check the regression list.** Any previously-failing test assertions for this step (see the tests under `tests/SharpFM.Tests/Scripting/` that failed after the `StepParamValue` deletion) should come back green. -7. **Check the box** on this document. - -## Reference: value types already defined - -Reuse these wherever they fit — don't create new ones unless the step genuinely needs a new shape. - -| Type | Location | Purpose | -|---|---|---| -| `NamedRef(int Id, string Name)` | `Values/NamedRef.cs` | id+name pairs for Script, Layout (named), TableOccurrence refs | -| `FieldRef(Table?, Id, Name, VariableName?)` | `Values/FieldRef.cs` | field or variable, with `ToDisplayString()` → `Table::Field` / `$var` | -| `Calculation(string Text)` | `Values/Calculation.cs` | CDATA-wrapped calc expression | -| `Animation(string WireValue)` | `Values/Animation.cs` | raw wire-string for FM animation names (unknown values pass through) | -| `LayoutTarget` (sealed record hierarchy) | `Values/LayoutTarget.cs` | Original / Named / ByNameCalc / ByNumberCalc | - -## Reference: architecture pattern - -See `src/SharpFM.Model/Scripting/Steps/GoToLayoutStep.cs` as the canonical example. Every typed POCO has: - -- Typed properties (no `XElement`, no string bags) -- `public static new ScriptStep FromXml(XElement step)` — parse from source XML -- `public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams)` — parse from display-text tokens -- `public override XElement ToXml()` — pure function of the typed state -- `public override string ToDisplayLine()` — pure function of the typed state -- A `[ModuleInitializer]` method that calls `StepXmlFactory.Register` and `StepDisplayFactory.Register` - -## Reference: display extension style guide - -See [`advanced-filemaker-scripting-syntax.md`](advanced-filemaker-scripting-syntax.md) for the full spec — the three extension forms (`(#id)` suffix, inline word tokens, trailing `; Kind: [...]` blocks), parsing precedence, the zero-loss audit every POCO must complete, and the judgment call on when to drop hidden state rather than surface it. - ---- - -## Priority 1 — handler-backed steps (failing regressions) - -These 13 steps used to have bespoke display rendering or display-text parsing via the retired `Handlers/` tree. Their tests are currently failing because the generic catalog path can't reproduce their special formatting. Each one needs a typed POCO. - -P1 regression status: - -| Previously failing test | Status | Fixed by | -|---|---|---| -| `ScriptStepTests.SetField_FromXml_ToDisplayLine` | ✓ | `SetFieldStep` | -| `ScriptStepTests.SetVariable_FromXml_ToDisplayLine` | ✓ | `SetVariableStep` | -| `ScriptStepTests.SetVariable_WithRepetition_ToDisplayLine` | ✓ | `SetVariableStep` | -| `ScriptStepTests.PerformScript_FromXml_ToDisplayLine` | ✓ | `PerformScriptStep` | -| `FmScriptModelTests.FromDisplayText_ToXml_SetVariable` | ✓ | `SetVariableStep` | -| `FmScriptModelTests.FromXml_ToDisplayText_IfEndIf_Indented` | ✓ | `ControlFlowSteps` | -| `FmScriptModelTests.RoundTrip_RealisticScript` | ✓ | All above + Loop / End Loop typed POCOs | - ---- - -### Set Field *(#76)* - -- [x] **Typed POCO landed** — `SetFieldStep.cs`, tests in `SetFieldStepTests.cs` - -**Why it's special:** FM Pro's display format is `Set Field [ Target ; Calculation ]` — the Field param appears *before* the Calculation param, even though the catalog order is [Calculation, Field]. The old `SetFieldHandler.ToDisplayLine` reordered these explicitly. - -**Fields to carry:** `FieldRef Target`, `Calculation Expression`. Both required (though tolerant of missing for partial input). - -**Verbatim XML samples** *(captured from FM Pro via Raw Clipboard Viewer)*: - -```xml - - - - - -``` - -```xml - - - - - -``` - -```xml - - - - - -``` - -**Expected display forms:** - -- `Set Field [ ScriptDefinitionHelper::ModifiedBy ; "just-a-string" ]` -- `Set Field [ ScriptDefinitionHelper::ModifiedBy ; $variable + " string" ]` -- `Set Field [ ScriptDefinitionHelper::ModifiedBy ; ScriptDefinitionHelper::PrimaryKey & " " & ScriptDefinitionHelper::CreatedBy ]` (multi-line calc preserves internal newlines in CDATA but renders on one line for the display form) -- `Set Field [ MyField ]` (no calc — degenerate case, still worth testing) - -**Notes on the captured XML:** - -- The `Field` element carries `table`, `id`, and `name` attributes — all three must round-trip (id is the lossless anchor, matching the `(#id)` convention used by `GoToLayoutStep`). -- `Calculation` content is wrapped in CDATA and may contain literal newlines (sample 3). The typed POCO's `Calculation.Text` should preserve those newlines byte-for-byte. -- Param order in the source XML is `[Calculation, Field]` but FM Pro's display is `[ Field ; Calculation ]` — this reorder is exactly what `SetFieldHandler.ToDisplayLine` used to do. - ---- - -### Set Variable *(#141)* - -- [x] **Typed POCO landed** — `SetVariableStep.cs`, tests in `SetVariableStepTests.cs` - -**Why it's special:** The canonical display is `Set Variable [ $name[rep] ; Value: calc ]` — the `[rep]` suffix is shown only when repetition is set and not equal to 1. The catalog params are `Value` (namedCalc), `Repetition` (namedCalc), `Name` (text), which the old `SetVariableHandler` reordered and rewrapped into the display form above. Parsing back is also bespoke (`ParseVarRepetition` splits `$name[rep]`). - -**Fields to carry:** `string Name`, `Calculation Value`, `Calculation Repetition` (default `"1"`). - -**Verbatim XML samples** *(captured from FM Pro via Raw Clipboard Viewer)*: - -```xml - - - - - - - - - $count - -``` - -```xml - - - - - - - - - $arr - -``` - -```xml - - - - - - - - - $arr - -``` - -**Expected display forms:** - -- `Set Variable [ $count ; Value: 0 ]` -- `Set Variable [ $arr[3] ; Value: "third" ]` -- `Set Variable [ $arr[$anotherVariable] ; Value: $count + 5 ]` -- `Set Variable [ $x ]` (no value — degenerate) - -**Notes on the captured XML:** - -- XML param order is `[Value, Repetition, Name]`; display reorders to `[ Name[rep] ; Value: calc ]`. -- `Repetition` is a full `Calculation`, not an integer — it can be a literal (`1`, `3`) or any calc expression (`$anotherVariable`, `$count + 5`). The typed POCO should carry `Calculation Repetition`, not `int`. -- `Repetition` is *always present* in the source XML, even when it equals `1`. The display-line writer must suppress `[rep]` when `Repetition.Text == "1"` (literal-one check, not arithmetic), and the XML writer must always emit a `` element (round-trip requirement). -- `Name` carries the `$` prefix as part of the text content — do not strip it on parse; treat the whole string as the variable name. Parsing `$arr[3]` or `$arr[$anotherVariable]` back from display text means splitting on the outermost `[` / `]` to separate name from repetition calc (watch for nested brackets in the repetition expression). -- Both `Value` and `Repetition` wrap their `Calculation` in a named wrapper element (``) — this is the `namedCalc` catalog shape. - ---- - -### Perform Script *(#1)* - -- [x] **Typed POCO landed** — `PerformScriptStep.cs`, tests in `PerformScriptStepTests.cs` - -**Why it's special:** Two discriminated modes (like Go to Layout's `LayoutTarget`): - -1. **Static reference** — a named ` - -``` - -```xml - - - - - - - -``` - -```xml - - - - -``` - -```xml - - - - - - -``` - -**Expected display forms** *(FM Pro's native wording confirmed; SharpFM appends `(#id)` on static refs per the style guide)*: - -- `Perform Script [ Specified: From list ; "Dummy-Script-For-Reference" (#4) ; Parameter: $$SomeGlobalVariable ]` -- `Perform Script [ Specified: By name ; $$globalVar & " literal-string" ; Parameter: $$SomeGlobalVariable ]` *(no id — by-name form has no `