Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/build-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ concurrency:

jobs:
native:
uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.1.0
uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.2.0
permissions:
contents: write
pull-requests: write
Expand Down
17 changes: 14 additions & 3 deletions claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,13 @@ the comment there about not caching "nothing staged" asks for.
structure, which is what makes the text snapshots meaningful and keeps three renderers honest.
- `ViewerProgram.Run(args, OpenWindow)` owns the loop for all heads. A head is a `Main` that
chooses a renderer; nothing else about the app is per platform.
- `SessionHost`, `ScreenCache`, `IViewerWindow`'s base `IHeadWindow`, `OpenWindow`,
`ClipboardPump` (driven by `ClipboardCopy`), `WindowPlacement` and `NativeResolver` are
PlatformHeads.Core's, the kernel BuildMonitor shares, in the `PlatformHeads` namespace;
`Directory.Build.props` names the generic ones for the viewer's state and screen, and
`DiffEngineViewer.Benchmarks.csproj` again, since it imports nothing above it. A fix to any of
them goes there. `SessionHost.Mutate` throws when a mutation mutates again, which silently undid
the outer one.
- The loop presents sixty times a second and builds a screen only when the state is another one.
`ScreenCache` keeps the last `SessionState` and its `Screen`: a state is immutable and only ever
replaced, so the same reference is the same screen, and building one a frame was the whole
Expand Down Expand Up @@ -502,7 +509,10 @@ the comment there about not caching "nothing staged" asks for.
which the three pixel heads draw and `AsciiRenderer` cannot - a character grid has no way to
invert part of a line without changing its width - so what the model universally states about a
selection goes in the **status line**, where every renderer draws it, and the highlight is the
enrichment on top. Copying is `IViewerWindow.SetClipboard` rather than a `ViewerActions` member,
enrichment on top. A copy waits in `SessionState.Clipboard` and the loop offers it to the
window through `ClipboardPump` for a few frames, since a Windows clipboard another process holds
refuses a set outright, and handed over once from inside the mutation it was lost while the
status said it had been copied. Copying is `IViewerWindow.SetClipboard` rather than a `ViewerActions` member,
because a clipboard belongs to a toolkit the way a window does, and it is answered before the
owner link: the text is already in this process.
- Columns are cells of `CellGrid`, decided in the model rather than by any head's fonts: a wide
Expand Down Expand Up @@ -605,9 +615,10 @@ the comment there about not caching "nothing staged" asks for.
**Native shim (`native/`), used by the Mac and Linux heads only:**
- The build and packaging of the heads is PlatformHeads' (`D:\Code\PlatformHeads`,
github.com/SimonCropp/PlatformHeads), which BuildMonitor shares: it was worked out here and
copied there, and fixes made here did not reach the copy. Three pins name one tag of it and move
copied there, and fixes made here did not reach the copy. Four pins name one tag of it and move
together: the `uses:` of `.github/workflows/build-native.yml`, the `GIT_TAG` in
`native/CMakeLists.txt`, and `PlatformHeads` in `Directory.Packages.props`. The workflow is its
`native/CMakeLists.txt`, and `PlatformHeads` and `PlatformHeads.Core` in
`Directory.Packages.props`. The workflow is its
reusable `build-native`. Its CMake module pins raylib and Dear ImGui, carries the raylib 6.0
option workarounds and links the library, and `native/CMakeLists.txt` names only the picture
formats, the source file and the two libraries the viewer links beyond raylib. Its package's
Expand Down
2 changes: 1 addition & 1 deletion native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ include(FetchContent)
FetchContent_Declare(
platformheads
GIT_REPOSITORY https://github.com/SimonCropp/PlatformHeads.git
GIT_TAG 0.1.0
GIT_TAG 0.2.0
GIT_SHALLOW ON)
FetchContent_MakeAvailable(platformheads)
include(${platformheads_SOURCE_DIR}/cmake/PlatformHeads.cmake)
Expand Down
2 changes: 1 addition & 1 deletion src/DiffEngineTray.Tests/TrayViewerSyncTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
using CommandKind = viewer::CommandKind;
using OwnerLink = viewer::OwnerLink;
using QueueProjection = viewer::QueueProjection;
using SessionHost = viewer::SessionHost;
using SessionHost = PlatformHeads.SessionHost<viewer::SessionState>;
using SessionMessageHandler = viewer::MessageHandler;
using SessionState = viewer::SessionState;
using ViewerActions = viewer::ViewerActions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
them ambiguous.
-->
<Using Include="DiffEngine" />
<!-- The kernel's types for the viewer's state and screen, as ../Directory.Build.props names
them for every other viewer project. That file is not imported here: see the one beside
this. -->
<Using Include="PlatformHeads" />
<Using Include="PlatformHeads.SessionHost&lt;SessionState&gt;" Alias="SessionHost" />
<Using Include="PlatformHeads.ScreenCache&lt;SessionState, Screen&gt;" Alias="ScreenCache" />
<Using Include="PlatformHeads.OpenWindow&lt;IViewerWindow&gt;" Alias="OpenWindow" />
<PackageReference Include="BenchmarkDotNet" />
<ProjectReference Include="..\DiffEngineViewer\DiffEngineViewer.csproj" />
</ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/DiffEngineViewer.Benchmarks/FrameBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void Setup()
state = built;
}

readonly ScreenCache screens = new();
readonly ScreenCache screens = new(ScreenBuilder.Build);

// One screen from one state: what every frame paid, and what a change to the state that leaves
// the queue the list it was still does. A scroll and a drag are that, every frame they last.
Expand All @@ -52,5 +52,5 @@ public object BuildScreenOfAChangedQueue() =>
// whenever the state is the one the last frame had.
[Benchmark]
public object FrameWithNothingChanged() =>
screens.For(state);
screens.Get(state);
}
5 changes: 2 additions & 3 deletions src/DiffEngineViewer.Benchmarks/GroupAcceptBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,8 @@ public void SetHidden(bool hidden)
{
}

public void SetClipboard(string text)
{
}
public bool SetClipboard(string text) =>
true;

public void Focus()
{
Expand Down
2 changes: 1 addition & 1 deletion src/DiffEngineViewer.Benchmarks/NativeHead.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ static bool TryFindShim([NotNullWhen(true)] out string? path)
return true;
}

if (NativeResolver.TryFind(out path))
if (NativeResolver.TryFind(Deview.Library, out path))
{
shimPath = path;
return true;
Expand Down
4 changes: 2 additions & 2 deletions src/DiffEngineViewer.Benchmarks/SelectionFrameBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void Setup()
}
}

readonly ScreenCache screens = new();
readonly ScreenCache screens = new(ScreenBuilder.Build);

// What a frame of a drag still pays, since each one moves the selection's end.
[Benchmark(Baseline = true)]
Expand All @@ -40,7 +40,7 @@ public object BuildScreen() =>
// And what every frame after ctrl+a pays, for as long as the selection stands.
[Benchmark]
public object FrameWithNothingChanged() =>
screens.For(selected);
screens.Get(selected);

static string Text(int lines, int changeEvery)
{
Expand Down
2 changes: 1 addition & 1 deletion src/DiffEngineViewer.Linux/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ static int Main(string[] args)
{
// Registered before anything can trigger a load. Only the heads that P/Invoke need this,
// so it lives with the renderer choice rather than in ViewerProgram.
NativeResolver.Register();
Deview.Resolve();
return ViewerProgram.Run(args, NativeViewerWindow.Open);
}
}
2 changes: 1 addition & 1 deletion src/DiffEngineViewer.Mac/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ static int Main(string[] args)
{
// Registered before anything can trigger a load. Only the heads that P/Invoke need this,
// so it lives with the renderer choice rather than in ViewerProgram.
NativeResolver.Register();
Deview.Resolve();
return ViewerProgram.Run(args, NativeViewerWindow.Open);
}
}
5 changes: 2 additions & 3 deletions src/DiffEngineViewer.Tests/AcceptAllProgressTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,8 @@ public void Focus()
{
}

public void SetClipboard(string text)
{
}
public bool SetClipboard(string text) =>
true;

public bool Capture(Screen screen, int width, int height, string pngPath) =>
false;
Expand Down
5 changes: 2 additions & 3 deletions src/DiffEngineViewer.Tests/AttachedViewerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -697,9 +697,8 @@ public void Focus()
{
}

public void SetClipboard(string text)
{
}
public bool SetClipboard(string text) =>
true;

public bool Capture(Screen screen, int width, int height, string pngPath) =>
false;
Expand Down
136 changes: 136 additions & 0 deletions src/DiffEngineViewer.Tests/ClipboardCopyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/// <summary>
/// A copy waits in the state until the window takes it, and the status says so when it never does.
/// The window was handed the text inside the copy's mutation, and a clipboard another process held
/// lost it while the status said it had been copied.
/// </summary>
public class ClipboardCopyTests
{
[Test]
public async Task ACopyTheWindowTakesIsLetGo()
{
var window = new Desktop();
var host = Copied(out var copy);

ClipboardCopy.Offer(new(), host, window, copy);

await Assert.That(window.Taken).IsEquivalentTo(["the text"]);
await Assert.That(host.State.Clipboard).IsNull();
await Assert.That(host.State.Message).IsEqualTo("Copied 1 line from the selection.");
}

[Test]
public async Task ABusyClipboardKeepsTheCopyForTheNextFrame()
{
var window = new Desktop { Busy = true };
var host = Copied(out var copy);

ClipboardCopy.Offer(new(), host, window, copy);

await Assert.That(host.State.Clipboard).IsSameReferenceAs(copy);
await Assert.That(host.State.Message).IsEqualTo("Copied 1 line from the selection.");
}

[Test]
public async Task AClipboardThatStaysBusyIsGivenUpOnAndSaidSo()
{
var window = new Desktop { Busy = true };
var host = Copied(out var copy);
var pump = new ClipboardPump();
for (var attempt = 0; attempt < ClipboardPump.Attempts; attempt++)
{
ClipboardCopy.Offer(pump, host, window, copy);
}

await Assert.That(host.State.Clipboard).IsNull();
await Assert.That(host.State.Message).IsEqualTo("Could not copy the selection: another program is holding the clipboard.");
}

/// <summary>
/// What came of one copy is not what came of a copy made since, which is still to be offered.
/// </summary>
[Test]
public async Task AnOutcomeLeavesALaterCopyAlone()
{
var window = new Desktop();
var host = Copied(out var first);
var second = host.Mutate(_ => _ with { Clipboard = new("other text", "the selection") }).Clipboard!;

ClipboardCopy.Offer(new(), host, window, first);

await Assert.That(host.State.Clipboard).IsSameReferenceAs(second);
}

/// <summary>
/// The loop offers what is waiting before it presents, so a copy reaches the window on the
/// frame after the key that made it.
/// </summary>
[Test]
public async Task TheLoopHandsTheCopyToTheWindow()
{
var window = new Desktop { Frames = 2 };
var host = Copied(out _);

ViewerProgram.Loop(host, window, null, null, null, new(), null, new(), new(ScreenBuilder.Build));

await Assert.That(window.Taken).IsEquivalentTo(["the text"]);
await Assert.That(host.State.Clipboard).IsNull();
}

static SessionHost Copied(out PendingCopy copy)
{
var host = new SessionHost(Fixtures.File());
copy = host.Mutate(_ => _ with
{
Clipboard = new("the text", "the selection"),
Message = "Copied 1 line from the selection."
}).Clipboard!;
return host;
}

/// <summary>
/// A window whose clipboard can be held by someone else, and which closes after a number of
/// frames.
/// </summary>
sealed class Desktop : IViewerWindow
{
int presented;

public bool Busy { get; init; }

public int Frames { get; init; } = 1;

public List<string> Taken { get; } = [];

public bool Present(Screen screen) =>
++presented < Frames;

public ViewerInput Poll() =>
new(CommandKind.None, -1, -1, 0, false, Fixtures.Columns, Fixtures.Rows);

public void SetHidden(bool hidden)
{
}

public void Focus()
{
}

public bool SetClipboard(string text)
{
if (Busy)
{
return false;
}

Taken.Add(text);
return true;
}

public bool Capture(Screen screen, int width, int height, string pngPath) =>
false;

public void Dispose()
{
}
}
}
5 changes: 2 additions & 3 deletions src/DiffEngineViewer.Tests/DerivedAcceptTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -460,9 +460,8 @@ public void Focus()
{
}

public void SetClipboard(string text)
{
}
public bool SetClipboard(string text) =>
true;

public bool Capture(Screen screen, int width, int height, string pngPath) =>
false;
Expand Down
5 changes: 2 additions & 3 deletions src/DiffEngineViewer.Tests/DiscardBatchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,8 @@ public void Focus()
{
}

public void SetClipboard(string text)
{
}
public bool SetClipboard(string text) =>
true;

public bool Capture(Screen screen, int width, int height, string pngPath) =>
false;
Expand Down
5 changes: 2 additions & 3 deletions src/DiffEngineViewer.Tests/DrawingViewTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,8 @@ public void Focus()
{
}

public void SetClipboard(string text)
{
}
public bool SetClipboard(string text) =>
true;

public bool Capture(Screen screen, int width, int height, string pngPath) =>
false;
Expand Down
5 changes: 2 additions & 3 deletions src/DiffEngineViewer.Tests/EntryMenuTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,8 @@ public void Focus()
{
}

public void SetClipboard(string text)
{
}
public bool SetClipboard(string text) =>
true;

public bool Capture(Screen screen, int width, int height, string pngPath) =>
false;
Expand Down
5 changes: 2 additions & 3 deletions src/DiffEngineViewer.Tests/GroupAcceptBatchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,8 @@ public void Focus()
{
}

public void SetClipboard(string text)
{
}
public bool SetClipboard(string text) =>
true;

public bool Capture(Screen screen, int width, int height, string pngPath) =>
false;
Expand Down
5 changes: 2 additions & 3 deletions src/DiffEngineViewer.Tests/MapProjectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -416,9 +416,8 @@ public void Focus()
{
}

public void SetClipboard(string text)
{
}
public bool SetClipboard(string text) =>
true;

public bool Capture(Screen screen, int width, int height, string pngPath) =>
false;
Expand Down
5 changes: 2 additions & 3 deletions src/DiffEngineViewer.Tests/MinimalViewTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,8 @@ public void Focus()
{
}

public void SetClipboard(string text)
{
}
public bool SetClipboard(string text) =>
true;

public bool Capture(Screen screen, int width, int height, string pngPath) =>
false;
Expand Down
Loading
Loading