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
7 changes: 6 additions & 1 deletion claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,12 @@ the comment there about not caching "nothing staged" asks for.
Its one option is "Run at startup", a checked item over
PlatformHeads' `XdgAutostart` (`TrayActions.RunAtLogin`), and the settings file is not read: the
hot keys are Windows', and target on left and the launch limit are user environment variables,
which .NET sets only on Windows. No purge, which needs a folder picker. The debug view is
which .NET sets only on Windows. Purge asks its folder and its go ahead through a program the
desktop has (`DesktopDialog`: zenity, or kdialog on KDE, and AppleScript's on the macOS head),
as `DesktopNotification` pops a balloon, and is not on the menu where there is none. Each
question waits on the person, so the purge runs on a thread of its own (`DesktopPurge`), and a
file it cannot delete is counted where the Windows head asks about it. What it finds and
deletes is the core's `VerifiedFiles`, which the Windows head's `FilePurger` uses too. The debug view is
written beside the logs and opened (`DebugReportFile`). The icon is the Windows head's `.ico`,
whose one picture is a 48 pixel, 32 bit bitmap and needs no decoder (`IconPixels`); the core
embeds the Windows head's pictures for both heads off Windows (`TrayPictures`). With no
Expand Down
6 changes: 4 additions & 2 deletions docs/mdsource/tray.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ Discard will clear all currently tracked items. It is the same discard as the pe

### Purge verified files

Windows only. Prompts for a directory, and then recursively deletes all `*.verified.*` in that directory.
Prompts for a directory, says how many `*.verified.*` and `*.received.*` files are under it, and once that is confirmed deletes them all.

On Windows a file that cannot be deleted is asked about, with a choice to retry it, skip it or stop. On macOS and Linux the questions are put by the desktop's own programs: AppleScript's dialogs on macOS, and on Linux zenity's, or kdialog's on KDE. Either one is used where only one is installed, and with neither the item is not on the menu. A file that cannot be deleted there is skipped, and a notification says how many were deleted and what stopped the rest.


### Debug view
Expand Down Expand Up @@ -188,6 +190,7 @@ The tray does the same there: it tracks pending moves and deletes, holds the inl
* Run at startup
* [Update](#update)
* [Debug view](#debug-view)
* [Purge verified files](#purge-verified-files), on Linux where zenity or kdialog is installed
* Open logs
* Raise issue

Expand All @@ -201,7 +204,6 @@ What is not there:

* The Options window. "Open on left" and "Max instances to launch" are environment variables, which the window sets for the user on Windows. Elsewhere set [`DiffEngine_TargetOnLeft`](/docs/diff-tool.md#leftright-diff-behavior) or [`DiffEngine_MaxInstances`](/docs/diff-tool.md#using-an-environment-variable) in the environment test runs start in.
* The HotKeys, which are registered with Windows.
* [Purge verified files](#purge-verified-files).
* The [locked files](#locked-files) prompt, which has nothing to resolve.


Expand Down
6 changes: 4 additions & 2 deletions docs/tray.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ Discard will clear all currently tracked items. It is the same discard as the pe

### Purge verified files

Windows only. Prompts for a directory, and then recursively deletes all `*.verified.*` in that directory.
Prompts for a directory, says how many `*.verified.*` and `*.received.*` files are under it, and once that is confirmed deletes them all.

On Windows a file that cannot be deleted is asked about, with a choice to retry it, skip it or stop. On macOS and Linux the questions are put by the desktop's own programs: AppleScript's dialogs on macOS, and on Linux zenity's, or kdialog's on KDE. Either one is used where only one is installed, and with neither the item is not on the menu. A file that cannot be deleted there is skipped, and a notification says how many were deleted and what stopped the rest.


### Debug view
Expand Down Expand Up @@ -195,6 +197,7 @@ The tray does the same there: it tracks pending moves and deletes, holds the inl
* Run at startup
* [Update](#update)
* [Debug view](#debug-view)
* [Purge verified files](#purge-verified-files), on Linux where zenity or kdialog is installed
* Open logs
* Raise issue

Expand All @@ -208,7 +211,6 @@ What is not there:

* The Options window. "Open on left" and "Max instances to launch" are environment variables, which the window sets for the user on Windows. Elsewhere set [`DiffEngine_TargetOnLeft`](/docs/diff-tool.md#leftright-diff-behavior) or [`DiffEngine_MaxInstances`](/docs/diff-tool.md#using-an-environment-variable) in the environment test runs start in.
* The HotKeys, which are registered with Windows.
* [Purge verified files](#purge-verified-files).
* The [locked files](#locked-files) prompt, which has nothing to resolve.


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
osascript
folder: [-e] [on run argv] [-e] [return POSIX path of (choose folder with prompt (item 1 of argv))] [-e] [end run] [The folder to delete every *.verified.* and *.received.* file under]
confirm: [-e] [on run argv] [-e] [display dialog (item 1 of argv) with title (item 2 of argv) buttons {"Cancel", "Delete"} default button "Delete" cancel button "Cancel" with icon caution] [-e] [end run] [Files found: 2, in /home/a & <b>.\nDelete files?] [Purge verified files]

zenity
folder: [--file-selection] [--directory] [--title=The folder to delete every *.verified.* and *.received.* file under]
confirm: [--question] [--no-markup] [--title=Purge verified files] [--text=Files found: 2, in /home/a & <b>.\nDelete files?] [--ok-label=Delete] [--cancel-label=Cancel]

kdialog
folder: [--title] [The folder to delete every *.verified.* and *.received.* file under] [--getexistingdirectory] [{Home}]
confirm: [--title] [Purge verified files] [--yes-label] [Delete] [--no-label] [Cancel] [--yesno] [Files found: 2, in /home/a & <b>.\nDelete files?]
55 changes: 55 additions & 0 deletions src/DiffEngineTray.Core.Tests/DesktopDialogTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
public class DesktopDialogTests
{
/// <summary>
/// What each program is handed, which is all there is to see of them without a desktop.
/// </summary>
[Test]
public async Task Arguments() =>
await Verify(
string.Join(
"\n\n",
new[] {DesktopDialog.AppleScript, DesktopDialog.Zenity, DesktopDialog.Kdialog}
.Select(_ => $"""
{_.Program}
folder: {Quoted(_.FolderArguments)}
confirm: {Quoted(_.ConfirmArguments("Files found: 2, in /home/a & <b>.\nDelete files?"))}
""")));

/// <summary>
/// kdialog opens in the home folder, which is whoever runs the tests'.
/// </summary>
static string Quoted(IEnumerable<string> arguments) =>
string.Join(
" ",
arguments.Select(_ => _ == Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
? "[{Home}]"
: $"[{_.Replace("\n", "\\n")}]"));

[Test]
[Arguments("KDE", "kdialog")]
[Arguments("ubuntu:GNOME", "zenity")]
[Arguments("kde:plasma", "kdialog")]
[Arguments(null, "zenity")]
public async Task EachDesktopsOwnFirst(string? desktop, string expected) =>
await Assert.That(DesktopDialog.ForLinux(desktop, _ => true)!.Program).IsEqualTo(expected);

[Test]
public async Task EitherStandsInForTheOther()
{
await Assert.That(DesktopDialog.ForLinux("GNOME", _ => _ == "kdialog")!.Program).IsEqualTo("kdialog");
await Assert.That(DesktopDialog.ForLinux("KDE", _ => _ == "zenity")!.Program).IsEqualTo("zenity");
}

[Test]
public async Task NoneWhereNeitherIsInstalled() =>
await Assert.That(DesktopDialog.ForLinux("GNOME", _ => false)).IsNull();

[Test]
[Arguments("/home/a/project\n", "/home/a/project")]
[Arguments("/Users/a/project/\n", "/Users/a/project")]
[Arguments("/\n", "/")]
[Arguments("", null)]
[Arguments("\n", null)]
public async Task TheFolderAsPrinted(string output, string? expected) =>
await Assert.That(DesktopDialog.FolderFrom(output)).IsEqualTo(expected);
}
123 changes: 123 additions & 0 deletions src/DiffEngineTray.Core.Tests/DesktopPurgeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
public class DesktopPurgeTests :
IDisposable
{
[Test]
public async Task DeletesWhatItFindsOnceConfirmed()
{
var kept = Write("Sample.cs");
Write("Sample.Test.verified.txt");
Write(Path.Combine("nested", "Sample.Test.received.png"));
string? asked = null;

DesktopPurge.Run(() => directory, _ =>
{
asked = _;
return true;
}, told.Add);

await Assert.That(Remaining()).IsEquivalentTo([kept]);
await Assert.That(asked!).StartsWith($"Files found: 2, in {directory}.");
await Assert.That(told).IsEquivalentTo([$"Deleted 2 files from {directory}."]);
}

[Test]
public async Task DeletesNothingWhenNotConfirmed()
{
var verified = Write("Sample.Test.verified.txt");

DesktopPurge.Run(() => directory, _ => false, told.Add);

await Assert.That(Remaining()).IsEquivalentTo([verified]);
await Assert.That(told).IsEmpty();
}

[Test]
public async Task AsksNothingMoreWhenNoFolderIsChosen()
{
Write("Sample.Test.verified.txt");
var asked = false;

DesktopPurge.Run(() => null, _ => asked = true, told.Add);

await Assert.That(asked).IsFalse();
await Assert.That(Remaining()).HasSingleItem();
await Assert.That(told).IsEmpty();
}

[Test]
public async Task SaysSoWhenThereIsNothingToPurge()
{
Write("Sample.cs");
var asked = false;

DesktopPurge.Run(() => directory, _ => asked = true, told.Add);

await Assert.That(asked).IsFalse();
await Assert.That(told).IsEquivalentTo([$"No *.verified.* or *.received.* files found in {directory}"]);
}

/// <summary>
/// Passed over and counted, and the rest still deleted. Made undeletable the way each platform
/// refuses a delete for: a read only file on Windows, and on Unix a folder that cannot be
/// written, since there a file's own mode says nothing about removing it.
/// </summary>
[Test]
public async Task CountsAFileThatCouldNotBeDeleted()
{
Write("Sample.Test.verified.txt");
var locked = Write(Path.Combine("locked", "Sample.Test.received.txt"));
var lockedDirectory = Path.GetDirectoryName(locked)!;
Refuse(locked, lockedDirectory, true);
try
{
DesktopPurge.Run(() => directory, _ => true, told.Add);
}
finally
{
Refuse(locked, lockedDirectory, false);
}

await Assert.That(Remaining()).IsEquivalentTo([locked]);
await Assert.That(told).HasSingleItem();
await Assert.That(told[0]).StartsWith($"Deleted 1 of 2 files from {directory}. 1 could not be deleted, the first being {locked}: ");
}

[Test]
public async Task ThereIsNoPurgeWithNothingToAskWith() =>
// As a bool, since an Action is taken as the thing to run and assert on
await Assert.That(DesktopPurge.For(null, told.Add) is null).IsTrue();

static void Refuse(string file, string fileDirectory, bool refuse)
{
if (OperatingSystem.IsWindows())
{
File.SetAttributes(file, refuse ? FileAttributes.ReadOnly : FileAttributes.Normal);
return;
}

var mode = UnixFileMode.UserRead | UnixFileMode.UserExecute;
if (!refuse)
{
mode |= UnixFileMode.UserWrite;
}

File.SetUnixFileMode(fileDirectory, mode);
}

string Write(string name)
{
var path = Path.Combine(directory, name);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, "content");
return path;
}

string[] Remaining() =>
Directory.GetFiles(directory, "*", SearchOption.AllDirectories);

string directory = Directory.CreateTempSubdirectory("DesktopPurgeTests").FullName;
List<string> told = [];

public void Dispose() =>
Directory.Delete(directory, true);
}
5 changes: 3 additions & 2 deletions src/DiffEngineTray.Core.Tests/TrayMenuTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -351,8 +351,9 @@ static void Render(StringBuilder builder, TrayMenu menu, IReadOnlyList<TrayMenuI
}

/// <summary>
/// A head with no options window and no folder picker, as on Linux: run at login and the
/// update are on the menu, run at login checked as the desktop says, and there is no purge.
/// A head with no options window and no folder picker, as on a Linux desktop with neither
/// zenity nor kdialog: run at login and the update are on the menu, run at login checked as
/// the desktop says, and there is no purge.
/// </summary>
[Test]
public async Task FixedForAHeadWithNoWindows() =>
Expand Down
Loading
Loading