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 src/DiffEngineTray.Tests/SettingsHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ await SettingsHelper.Write(
AlwaysKillLockingProcesses = true
});

var result = await SettingsHelper.Read();
var result = SettingsHelper.Read();

await Verify(result);
}
Expand Down
91 changes: 53 additions & 38 deletions src/DiffEngineTray/Images/Images.cs
Original file line number Diff line number Diff line change
@@ -1,43 +1,58 @@
/// <summary>
/// Decoded on first use rather than all together.
/// <para>
/// A static constructor decoded all eleven the moment anything touched one, and the first thing to
/// touch one is the tray icon - before the icon exists, so the cost is time the user spends looking
/// at an empty notification area. Only <see cref="Default" /> is wanted then. Of the nine menu
/// images, four are on the fixed menu items and five are only ever shown when something is pending,
/// so most of that decoding was for a menu that may never have anything in it.
/// </para>
/// <para>
/// The <see cref="Lazy{T}" /> fields still initialise together, which is fine: constructing one
/// allocates and nothing more. Thread safe by default, and it has to be, because the icon is set
/// from the scan timer as well as from the UI thread.
/// </para>
/// </summary>
public static class Images
{
static Stream GetStream(string name) =>
Assembly.GetExecutingAssembly().GetManifestResourceStream($"DiffEngineTray.Images.{name}")!;
public static Icon Active => active.Value;
public static Icon Default => defaultIcon.Value;
public static Image Exit => exit.Value;
public static Image Delete => delete.Value;
public static Image AcceptAll => acceptAll.Value;
public static Image Accept => accept.Value;
public static Image Discard => discard.Value;
public static Image VisualStudio => visualStudio.Value;
public static Image Folder => folder.Value;
public static Image Options => options.Value;
public static Image Link => link.Value;

static Lazy<Icon> active = LazyIcon("active.ico");
static Lazy<Icon> defaultIcon = LazyIcon("default.ico");
static Lazy<Image> exit = LazyImage("exit.png");
static Lazy<Image> delete = LazyImage("delete.png");
static Lazy<Image> acceptAll = LazyImage("acceptAll.png");
static Lazy<Image> accept = LazyImage("accept.png");
static Lazy<Image> discard = LazyImage("discard.png");
static Lazy<Image> visualStudio = LazyImage("vs.png");
static Lazy<Image> folder = LazyImage("folder.png");
static Lazy<Image> options = LazyImage("cogs.png");
static Lazy<Image> link = LazyImage("link.png");

static Images()
{
using var activeStream = GetStream("active.ico");
Active = new(activeStream);
using var defaultStream = GetStream("default.ico");
Default = new(defaultStream);
using var exitStream = GetStream("exit.png");
Exit = Image.FromStream(exitStream);
using var deleteStream = GetStream("delete.png");
Delete = Image.FromStream(deleteStream);
using var acceptAllStream = GetStream("acceptAll.png");
AcceptAll = Image.FromStream(acceptAllStream);
using var acceptStream = GetStream("accept.png");
Accept = Image.FromStream(acceptStream);
using var discardStream = GetStream("discard.png");
Discard = Image.FromStream(discardStream);
using var vsStream = GetStream("vs.png");
VisualStudio = Image.FromStream(vsStream);
using var folderStream = GetStream("folder.png");
Folder = Image.FromStream(folderStream);
using var optionsStream = GetStream("cogs.png");
Options = Image.FromStream(optionsStream);
using var linkStream = GetStream("link.png");
Link = Image.FromStream(linkStream);
}
static Lazy<Icon> LazyIcon(string name) =>
new(() =>
{
using var stream = GetStream(name);
return new Icon(stream);
});

public static Image VisualStudio { get; }
public static Image Link { get; }
public static Image Discard { get; }
public static Image Accept { get; }
public static Image AcceptAll { get; }
public static Image Delete { get; }
public static Image Exit { get; }
public static Image Folder { get; }
public static Image Options { get; }
public static Icon Active { get; }
public static Icon Default { get; }
static Lazy<Image> LazyImage(string name) =>
new(() =>
{
using var stream = GetStream(name);
return Image.FromStream(stream);
});

static Stream GetStream(string name) =>
Assembly.GetExecutingAssembly().GetManifestResourceStream($"DiffEngineTray.Images.{name}")!;
}
2 changes: 1 addition & 1 deletion src/DiffEngineTray/LockedFilesHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ static void Persist() =>
{
try
{
var settings = await SettingsHelper.Read();
var settings = SettingsHelper.Read();
settings.AlwaysKillLockingProcesses = true;
await SettingsHelper.Write(settings);
}
Expand Down
26 changes: 14 additions & 12 deletions src/DiffEngineTray/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,6 @@ static async Task Main()

static async Task Inner()
{
var settings = await GetSettings();
if (settings == null)
{
return;
}

LockedFilesHandler.AlwaysKill = settings.AlwaysKillLockingProcesses;

Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var tokenSource = new CancelSource();
var cancel = tokenSource.Token;
using var mutex = new Mutex(true, "DiffEngine", out var createdNew);
Expand All @@ -52,6 +42,10 @@ static async Task Inner()
Log.Error(exception, "Failed to write the tray version marker");
}

// Before the settings file rather than after it, because nothing between here and the
// first use of a setting needs one, and until this line the user is looking at an empty
// notification area. Still after the mutex, so a second instance exits without ever
// showing an icon.
using var icon = new NotifyIcon
{
Icon = Images.Default,
Expand All @@ -62,6 +56,14 @@ static async Task Inner()
void Warn(string message) =>
icon.ShowBalloonTip(10000, "DiffEngineTray", message, ToolTipIcon.Warning);

var settings = GetSettings();
if (settings == null)
{
return;
}

LockedFilesHandler.AlwaysKill = settings.AlwaysKillLockingProcesses;

// Ownership of the inline queue is decided here, once, by whether the bind succeeds, and
// never transfers. Usually the tray wins, because it starts at login. A viewer that was
// already running keeps the queue for as long as it lives, and this tray drives it
Expand Down Expand Up @@ -179,11 +181,11 @@ internal static IEnumerable<KeyBinding> BuildKeyBindings(Settings settings, Trac

internal record KeyBinding(int Id, HotKey HotKey, Action Action);

static async Task<Settings?> GetSettings()
static Settings? GetSettings()
{
try
{
return await SettingsHelper.Read();
return SettingsHelper.Read();
}
catch (Exception exception)
{
Expand Down
2 changes: 1 addition & 1 deletion src/DiffEngineTray/Settings/OptionsFormLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public static async Task Launch(KeyRegister keyRegister, Tracker tracker)
return;
}

var settings = await SettingsHelper.Read();
var settings = SettingsHelper.Read();
using var form = new OptionsForm(
settings,
newSettings => Save(keyRegister, tracker, settings, newSettings));
Expand Down
20 changes: 20 additions & 0 deletions src/DiffEngineTray/Settings/SettingsContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <summary>
/// Source generated serialisation for <see cref="Settings" />, rather than the reflection based
/// serialiser.
/// <para>
/// Worth less than it looks. Reading the settings file cost 31ms of a 200ms start, and moving the
/// metadata to compile time took 4ms off that. Almost all of the rest is System.Text.Json being
/// loaded and jitted for the first time, which no amount of generated code avoids - the file
/// itself is under 200 bytes. Kept because it is strictly cheaper, and because it keeps the
/// reflection based serialiser off the startup path altogether, which is the part that grows on a
/// cold start.
/// </para>
/// <para>
/// Both directions go through it, so a save does not reach the reflection based serialiser either.
/// The written JSON is unchanged: the generator's defaults are the same defaults, which is what
/// <c>SettingsHelperTests.ReadWrite</c> pins.
/// </para>
/// </summary>
[JsonSerializable(typeof(Settings))]
partial class SettingsContext :
JsonSerializerContext;
15 changes: 10 additions & 5 deletions src/DiffEngineTray/Settings/SettingsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@ static SettingsHelper()
FilePath = Path.Combine(directory, "settings.json");
}

public static async Task<Settings> Read()
public static Settings Read()
{
Settings settings;
if (File.Exists(FilePath))
{
await using var stream = File.OpenRead(FilePath);
settings = (await JsonSerializer.DeserializeAsync<Settings>(stream))!;
// Read whole and parsed in one go rather than deserialised off an async FileStream.
// The file is under 200 bytes, so the async machinery cost more than the read did:
// 27ms for the call against 21ms without it. What is left is System.Text.Json being
// loaded and jitted for the first time, which nothing here can avoid - see
// <see cref="SettingsContext"/>.
var json = File.ReadAllBytes(FilePath);
settings = JsonSerializer.Deserialize(json, SettingsContext.Default.Settings)!;
}
else
{
await File.WriteAllTextAsync(FilePath, "{}");
File.WriteAllText(FilePath, "{}");
settings = new();
}

Expand Down Expand Up @@ -52,7 +57,7 @@ internal static async Task WriteFile(Settings settings)

await using (var stream = File.Create(temp))
{
await JsonSerializer.SerializeAsync(stream, settings);
await JsonSerializer.SerializeAsync(stream, settings, SettingsContext.Default.Settings);
}

await Swap(temp);
Expand Down
Loading