Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1bc1317
feat: add plain-text FileMaker script editor with bidirectional XML c…
fuzzzerd Mar 30, 2026
b4c2428
feat: add typed FmScript object model for script editing
fuzzzerd Mar 30, 2026
dc5d99e
refactor: simplify ScriptValidator to delegate to FmScript model
fuzzzerd Mar 30, 2026
d8d1745
refactor: wire UI to FmScript model, Script tab is primary editor
fuzzzerd Mar 30, 2026
7010330
refactor: remove old string-based converter and renderer infrastructure
fuzzzerd Mar 30, 2026
7c737b4
fix: validation squiggles for unknown steps and invalid param values
fuzzzerd Mar 30, 2026
3e72a98
refactor: extract BracketMatcher utility, fix dead code, add error lo…
fuzzzerd Mar 30, 2026
f9bba2e
refactor: extract step handlers using Strategy pattern
fuzzzerd Mar 30, 2026
afcea49
refactor: consolidate block pair validation in ScriptValidator
fuzzzerd Mar 30, 2026
2035b69
refactor: extract ScriptEditorController from MainWindow
fuzzzerd Mar 30, 2026
d013a20
refactor: add BlockPairRole enum, IStepCatalog interface, rename RawX…
fuzzzerd Mar 30, 2026
658cc82
refactor: move project to src/SharpFM, organize scripting into SharpF…
fuzzzerd Mar 30, 2026
a61d9e0
feat: add ClipboardService, status bar feedback, and model sync on sa…
fuzzzerd Mar 30, 2026
02031fa
refactor: inject FolderService, extract shared theme constants, renam…
fuzzzerd Mar 30, 2026
b064e20
perf: lazy-load XML editor, remove tab switching, open XML in separat…
fuzzzerd Mar 30, 2026
562139b
fix: make clip search case-insensitive
fuzzzerd Mar 30, 2026
03fa68d
fix: prevent crash on empty XML with FirstOrDefault
fuzzzerd Mar 30, 2026
4b90cb6
fix: log and skip malformed clip files instead of crashing
fuzzzerd Mar 30, 2026
91fdb8d
perf: cache RawData, invalidate on XmlData change
fuzzzerd Mar 30, 2026
b46a3ea
chore: remove unused Script property from ClipViewModel
fuzzzerd Mar 30, 2026
32bf28c
ux: error status messages stay visible longer (8s vs 3s)
fuzzzerd Mar 30, 2026
6be1fbb
ux: clear status message when selecting a different clip
fuzzzerd Mar 30, 2026
108968e
ux: add keyboard shortcuts (Ctrl+S save, Ctrl+N new, Ctrl+Shift+C cop…
fuzzzerd Mar 30, 2026
f0b2a65
ux: make XML window editable, sync changes back to model on close
fuzzzerd Mar 30, 2026
b87e8d9
test: add ClipViewModel sync and binding tests
fuzzzerd Mar 30, 2026
e35c423
perf: use StringBuilder for multi-line statement merging
fuzzzerd Mar 30, 2026
171c905
ux: preserve clip selection when search filter narrows results
fuzzzerd Mar 30, 2026
b016b59
fix: text editor fills available width instead of sizing to content
fuzzzerd Mar 30, 2026
8551101
fix: add window-level KeyBindings for Ctrl+S, Ctrl+N, Ctrl+Shift+C
fuzzzerd Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ jobs:
run: dotnet test --no-build

- name: Publish
run: dotnet publish SharpFM.csproj --runtime "${{ matrix.target }}" -c Debug
run: dotnet publish src/SharpFM/SharpFM.csproj --runtime "${{ matrix.target }}" -c Debug
2 changes: 1 addition & 1 deletion .github/workflows/release-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
release_name="SharpFM-$tag-${{ matrix.target }}"

# Build everything
dotnet publish SharpFM.csproj --runtime "${{ matrix.target }}" -c Release -o "$release_name"
dotnet publish src/SharpFM/SharpFM.csproj --runtime "${{ matrix.target }}" -c Release -o "$release_name"

# Pack files
if [ "${{ matrix.target }}" == "win-x64" ]; then
Expand Down
6 changes: 3 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
"preLaunchTask": "build",
"windows":
{
"program": "${workspaceFolder}/bin/Debug/net8.0/win-x64/SharpFM.dll",
"program": "${workspaceFolder}/src/SharpFM/bin/Debug/net8.0/win-x64/SharpFM.dll",
},
"linux":
"linux":
{
"program": "${workspaceFolder}/bin/Debug/net8.0/linux-x64/SharpFM.dll",
"program": "${workspaceFolder}/src/SharpFM/bin/Debug/net8.0/linux-x64/SharpFM.dll",
},
"args": [],
"cwd": "${workspaceFolder}",
Expand Down
36 changes: 0 additions & 36 deletions MainWindow.axaml.cs

This file was deleted.

13 changes: 12 additions & 1 deletion SharpFM.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpFM", "SharpFM.csproj", "{5245F468-DAD7-478C-8E5F-518A03664F71}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpFM", "src\SharpFM\SharpFM.csproj", "{5245F468-DAD7-478C-8E5F-518A03664F71}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E2FF2BB3-AF37-44BA-BD84-999B352D814E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpFM.Tests", "tests\SharpFM.Tests\SharpFM.Tests.csproj", "{5B228160-ECB9-4DFC-91D7-413AE9900617}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -22,5 +26,12 @@ Global
{9E4B6169-0E69-430A-BF6C-184A10C71F9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E4B6169-0E69-430A-BF6C-184A10C71F9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E4B6169-0E69-430A-BF6C-184A10C71F9B}.Release|Any CPU.Build.0 = Release|Any CPU
{5B228160-ECB9-4DFC-91D7-413AE9900617}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5B228160-ECB9-4DFC-91D7-413AE9900617}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B228160-ECB9-4DFC-91D7-413AE9900617}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5B228160-ECB9-4DFC-91D7-413AE9900617}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5B228160-ECB9-4DFC-91D7-413AE9900617} = {E2FF2BB3-AF37-44BA-BD84-999B352D814E}
EndGlobalSection
EndGlobal
24 changes: 24 additions & 0 deletions THIRD_PARTY_NOTICES
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
SharpFM uses third-party components under the following licenses:

================================================================================
agentic-fm
https://github.com/petrowsky/agentic-fm
================================================================================

Copyright 2026 Matt Petrowsky

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Files derived from agentic-fm:
- Core/ScriptConverter/step-catalog-en.json (step catalog data)
- Core/ScriptConverter/ (converter logic ported from TypeScript to C#)
71 changes: 0 additions & 71 deletions ViewModels/ClipViewModel.cs

This file was deleted.

File renamed without changes.
15 changes: 8 additions & 7 deletions App.axaml.cs → src/SharpFM/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@ public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(logger)
};
desktop.MainWindow = new MainWindow();

var services = new ServiceCollection();

services.AddSingleton(x => new FolderService(desktop.MainWindow));

services.AddSingleton<IFolderService>(x => new FolderService(desktop.MainWindow));
services.AddSingleton<IClipboardService>(x => new ClipboardService(desktop.MainWindow));
Services = services.BuildServiceProvider();

desktop.MainWindow.DataContext = new MainWindowViewModel(
logger,
Services.GetRequiredService<IClipboardService>(),
Services.GetRequiredService<IFolderService>());
}

base.OnFrameworkInitializationCompleted();
Expand Down
File renamed without changes.
28 changes: 21 additions & 7 deletions Core/FileMakerClip.cs → src/SharpFM/Core/FileMakerClip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public FileMakerClip(string name, string format, byte[] data)

// try to show better "name" if possible
var xdoc = XDocument.Load(new StringReader(XmlData));
var containerName = xdoc.Element("fmxmlsnippet")?.Descendants().First()?.Attribute("name")?.Value ?? "new-clip";
var containerName = xdoc.Element("fmxmlsnippet")?.Descendants().FirstOrDefault()?.Attribute("name")?.Value ?? "new-clip";

// set the name from the xml data if possible and fall back to constructor parameter
Name = containerName ?? name;
Expand All @@ -82,23 +82,37 @@ public FileMakerClip(string name, string format, byte[] data)

/// <summary>
/// Raw data that can be put back onto the Clipboard in FileMaker structure.
/// Cached — invalidated when XmlData changes.
/// </summary>
public byte[] RawData
{
get
{
// recalculate the length of the original text and make sure that is the first four bytes in the stream
byte[] byteList = Encoding.UTF8.GetBytes(XmlData);
int bl = byteList.Length;
byte[] intBytes = BitConverter.GetBytes(bl);
return intBytes.Concat(byteList).ToArray();
if (_cachedRawData == null)
{
byte[] byteList = Encoding.UTF8.GetBytes(XmlData);
byte[] intBytes = BitConverter.GetBytes(byteList.Length);
_cachedRawData = intBytes.Concat(byteList).ToArray();
}
return _cachedRawData;
}
}

private string _xmlData = string.Empty;
private byte[]? _cachedRawData;

/// <summary>
/// The actual clip. Users work with the Xml version here, and then pull the RawData property when ready to write back to FileMaker.
/// </summary>
public string XmlData { get; set; }
public string XmlData
{
get => _xmlData;
set
{
_xmlData = value;
_cachedRawData = null; // invalidate cache
}
}

/// <summary>
/// The fields exposed through this FileMaker Clip (if its a table or a layout).
Expand Down
File renamed without changes.
84 changes: 48 additions & 36 deletions MainWindow.axaml → src/SharpFM/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,50 @@
d:DesignWidth="700"
x:DataType="vm:MainWindowViewModel"
mc:Ignorable="d">

<Window.KeyBindings>
<KeyBinding Gesture="Ctrl+S" Command="{Binding SaveClipsStorage}" />
<KeyBinding Gesture="Ctrl+N" Command="{Binding NewEmptyItem}" />
<KeyBinding Gesture="Ctrl+Shift+C" Command="{Binding CopySelectedToClip}" />
</Window.KeyBindings>

<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_New">
<MenuItem Command="{Binding NewEmptyItem}" Header="Blank Clip" />
<MenuItem Command="{Binding PasteFileMakerClipData}" Header="From Clipboard (copied from FileMaker)" />
<MenuItem Command="{Binding NewEmptyItem}" Header="Blank Clip" InputGesture="Ctrl+N" />
<MenuItem Command="{Binding PasteFileMakerClipData}" Header="From Clipboard (copied from FileMaker)" InputGesture="Ctrl+V" />
</MenuItem>
<Separator />
<MenuItem Command="{Binding OpenFolderPicker}" Header="Open Folder" />
<Separator />
<MenuItem Header="Save">
<MenuItem Command="{Binding SaveClipsStorage}" Header="Save All To Folder" />
<MenuItem Command="{Binding CopySelectedToClip}" Header="Selected clip to Clipboard (to paste into FileMaker)" />
<MenuItem Command="{Binding SaveClipsStorage}" Header="Save All To Folder" InputGesture="Ctrl+S" />
<MenuItem Command="{Binding CopySelectedToClip}" Header="Selected clip to Clipboard (to paste into FileMaker)" InputGesture="Ctrl+Shift+C" />
</MenuItem>
<Separator />
<MenuItem Command="{Binding ExitApplication}" Header="_Exit" />
</MenuItem>
<MenuItem Header="Transform">
<MenuItem Command="{Binding CopyAsClass}" Header="Copy as C# Class" />
</MenuItem>
<MenuItem Header="_View">
<MenuItem x:Name="viewXmlMenuItem" Header="Show XML" InputGesture="Ctrl+Shift+X" />
</MenuItem>
<MenuItem Header="{Binding Version}" />
</Menu>

<!-- Status bar (docked before main content so Grid fills remaining space) -->
<Border
DockPanel.Dock="Bottom"
Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
Padding="16,6"
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock
Classes="Fluent2Caption"
Text="{Binding StatusMessage}" />
</Border>

<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
Expand Down Expand Up @@ -147,43 +169,33 @@
Width="16" />

<!-- Right panel: Code editor with Fluent 2 surface treatment -->
<Border
<Border
Grid.Column="2"
Classes="Fluent2SurfaceElevated">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<!-- Editor header -->
<Border
Grid.Row="0"
Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
CornerRadius="8,8,0,0"
Padding="16,12">
<TextBlock
Classes="Fluent2Subtitle"
Text="Clip Content" />
</Border>

<!-- AvaloniaEdit component with Document binding -->
<Border
Grid.Row="1"
CornerRadius="0,0,8,8"
ClipToBounds="True">
<AvaloniaEdit:TextEditor
x:Name="avaloniaEditor"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
ShowLineNumbers="True"
SyntaxHighlighting="Xml"
WordWrap="False"
Document="{Binding SelectedClip.XmlDocument}" />
</Border>
</Grid>
<DockPanel>
<!-- Script editor for script clips -->
<AvaloniaEdit:TextEditor
x:Name="scriptEditor"
DockPanel.Dock="Top"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
ShowLineNumbers="True"
WordWrap="False"
IsVisible="{Binding SelectedClip.IsScriptClip}"
Document="{Binding SelectedClip.ScriptDocument}" />
<!-- Fallback XML editor for non-script clips -->
<AvaloniaEdit:TextEditor
x:Name="fallbackXmlEditor"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
ShowLineNumbers="True"
SyntaxHighlighting="Xml"
WordWrap="False"
IsVisible="{Binding SelectedClip.IsScriptClip, Converter={x:Static BoolConverters.Not}}"
Document="{Binding SelectedClip.XmlDocument}" />
</DockPanel>
</Border>
</Grid>
</Grid>

</DockPanel>

</Window>
Loading
Loading