<|start_filename|>src/DiffEngine/Logging.cs<|end_filename|>
﻿namespace DiffEngine;

public static class Logging
{
    internal static bool enabled;

    public static void Enable()
    {
        enabled = true;
    }

    internal static void Write(string message)
    {
        if (enabled)
        {
            Trace.WriteLine(message);
        }
    }
}

<|start_filename|>src/DiffEngine/OsSettings.cs<|end_filename|>
﻿namespace DiffEngine;

[DebuggerDisplay("ExePaths={ExePaths}")]
public class OsSettings
{
    public BuildArguments TargetLeftArguments { get; }
    public BuildArguments TargetRightArguments { get; }
    public string[] ExePaths { get; }

    public OsSettings(
        BuildArguments targetLeftArguments,
        BuildArguments targetRightArguments,
        params string[] exePaths)
    {
        TargetLeftArguments = targetLeftArguments;
        TargetRightArguments = targetRightArguments;
        ExePaths = exePaths;
    }
}

<|start_filename|>src/DiffEngine/OrderReader.cs<|end_filename|>
﻿using DiffEngine;

static class OrderReader
{
    public readonly struct Result
    {
        public bool FoundInEnvVar { get; }
        public IEnumerable<DiffTool> Order { get; }

        public Result(in bool foundInEnvVar, IEnumerable<DiffTool> order)
        {
            FoundInEnvVar = foundInEnvVar;
            Order = order;
        }
    }

    public static Result ReadToolOrder()
    {
        var diffOrder = Environment.GetEnvironmentVariable("DiffEngine_ToolOrder");

        var found = !string.IsNullOrWhiteSpace(diffOrder);
        IEnumerable<DiffTool> order;
        if (found)
        {
            order = ParseEnvironment(diffOrder!);
        }
        else
        {
            order = Enum.GetValues(typeof(DiffTool)).Cast<DiffTool>();
        }

        return new(found, order);
    }

    internal static IEnumerable<DiffTool> ParseEnvironment(string diffOrder)
    {
        foreach (var toolString in diffOrder
            .Split(new[] {',', '|', ' '}, StringSplitOptions.RemoveEmptyEntries))
        {
            if (!Enum.TryParse<DiffTool>(toolString, out var diffTool))
            {
                throw new($"Unable to parse tool from `DiffEngine_ToolOrder` environment variable: {toolString}");
            }

            yield return diffTool;
        }
    }
}

<|start_filename|>src/DiffEngine/Implementation/ExamDiff.cs<|end_filename|>
﻿using DiffEngine;

static partial class Implementation
{
    public static Definition ExamDiff()
    {
        static string TargetLeftArguments(string temp, string target)
        {
            var tempTitle = Path.GetFileName(temp);
            var targetTitle = Path.GetFileName(target);
            return $"\"{target}\" \"{temp}\" /nh /diffonly /dn1:{targetTitle} /dn2:{tempTitle}";
        }

        static string TargetRightArguments(string temp, string target)
        {
            var tempTitle = Path.GetFileName(temp);
            var targetTitle = Path.GetFileName(target);
            return $"\"{temp}\" \"{target}\" /nh /diffonly /dn1:{tempTitle} /dn2:{targetTitle}";
        }

        return new(
            name: DiffTool.ExamDiff,
            url: "https://www.prestosoft.com/edp_examdiffpro.asp",
            autoRefresh: true,
            isMdi: false,
            supportsText: true,
            requiresTarget: true,
            cost: "Paid",
            binaryExtensions: Array.Empty<string>(),
            windows: new(
                TargetLeftArguments,
                TargetRightArguments,
                @"%ProgramFiles%\ExamDiff Pro\ExamDiff.exe"),
            notes: @"
 * [Command line reference](https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options)
 * `/nh`: do not add files or directories to comparison history
 * `/diffonly`: diff-only merge mode: hide the merge pane");
    }
}

<|start_filename|>src/DiffEngine/Implementation/P4MergeImage.cs<|end_filename|>
﻿using DiffEngine;

static partial class Implementation
{
    public static Definition P4MergeImage()
    {
        static string TargetLeftArguments(string temp, string target) =>
            $"\"{target}\" \"{temp}\"";

        static string TargetRightArguments(string temp, string target) =>
            $"\"{temp}\" \"{target}\"";

        return new(
            name: DiffTool.P4MergeImage,
            url: "https://www.perforce.com/products/helix-core-apps/merge-diff-tool-p4merge",
            autoRefresh: false,
            isMdi: false,
            supportsText: false,
            requiresTarget: true,
            cost: "Free",
            binaryExtensions: new[]
            {
                "bmp",
                "gif",
                "jpg",
                "jpeg",
                "png",
                "pbm",
                "pgm",
                "ppm",
                "tif",
                "tiff",
                "xbm",
                "xpm"
            },
            windows: new(
                TargetLeftArguments,
                TargetRightArguments,
                @"%ProgramFiles%\Perforce\p4merge.exe"),
            linux: new(
                TargetLeftArguments,
                TargetRightArguments,
                "/usr/bin/p4merge"),
            osx: new(
                TargetLeftArguments,
                TargetRightArguments,
                "/Applications/p4merge.app/Contents/MacOS/p4merge"));
    }
}

<|start_filename|>src/DiffEngineTray/ProcessEx.cs<|end_filename|>
using System.Diagnostics.CodeAnalysis;

static class ProcessEx
{
    public static bool TryGet(int id, [NotNullWhen(true)] out Process? process)
    {
        try
        {
            process = Process.GetProcessById(id);
            return true;
        }
        catch (ArgumentException)
        {
            //If process doesnt exists
            process = null;
            return false;
        }
    }

    public static void KillAndDispose(this Process process)
    {
        try
        {
            process.Kill();
        }
        catch (InvalidOperationException)
        {
            // Race condition can cause "No process is associated with this object"
        }
        catch (Exception exception)
        {
            ExceptionHandler.Handle($"Failed to kill process. Id:{process.Id} Name: {process.MainModule?.FileName}", exception);
        }
        finally
        {
            process.Dispose();
        }
    }
}

<|start_filename|>src/DiffEngine/ResolvedTool.cs<|end_filename|>
﻿namespace DiffEngine;

[DebuggerDisplay("{Name} {ExePath}, Refresh={AutoRefresh}, Mdi={IsMdi}, RequiresTarget={RequiresTarget}, SupportsText={SupportsText}")]
public class ResolvedTool
{
    public string Name { get; }
    public DiffTool? Tool { get; }
    public string ExePath { get; }
    public BuildArguments TargetRightArguments { get; }
    public BuildArguments TargetLeftArguments { get; }
    public bool IsMdi { get; }
    public bool AutoRefresh { get; }
    public IReadOnlyList<string> BinaryExtensions { get; }
    public bool RequiresTarget { get; }
    public bool SupportsText { get; }

    internal void CommandAndArguments(string tempFile, string targetFile, out string arguments, out string command)
    {
        arguments = GetArguments(tempFile, targetFile);
        command = $"\"{ExePath}\" {arguments}";
    }

    public string BuildCommand(string tempFile, string targetFile)
    {
        return $"\"{ExePath}\" {GetArguments(tempFile, targetFile)}";
    }

    public string GetArguments(string tempFile, string targetFile)
    {
        if (TargetPosition.TargetOnLeft)
        {
            return TargetLeftArguments(tempFile, targetFile);
        }

        return TargetRightArguments(tempFile, targetFile);
    }

    internal ResolvedTool(string name,
        DiffTool? tool,
        string exePath,
        BuildArguments targetRightArguments,
        BuildArguments targetLeftArguments,
        bool isMdi,
        bool autoRefresh,
        IReadOnlyList<string> binaryExtensions,
        bool requiresTarget,
        bool supportsText)
    {
        Name = name;
        Tool = tool;
        ExePath = exePath;
        TargetRightArguments = targetRightArguments;
        TargetLeftArguments = targetLeftArguments;
        IsMdi = isMdi;
        AutoRefresh = autoRefresh;
        BinaryExtensions = binaryExtensions;
        RequiresTarget = requiresTarget;
        SupportsText = supportsText;
    }

    public ResolvedTool(
        string name,
        string exePath,
        BuildArguments targetRightArguments,
        BuildArguments targetLeftArguments,
        bool isMdi,
        bool autoRefresh,
        string[] binaryExtensions,
        bool requiresTarget,
        bool supportsText)
    {
        Guard.FileExists(exePath, nameof(exePath));
        Guard.AgainstEmpty(name, nameof(name));
        Name = name;
        ExePath = exePath;
        TargetRightArguments = targetRightArguments;
        TargetLeftArguments = targetLeftArguments;
        IsMdi = isMdi;
        AutoRefresh = autoRefresh;
        BinaryExtensions = binaryExtensions;
        RequiresTarget = requiresTarget;
        SupportsText = supportsText;
    }
}

<|start_filename|>src/DiffEngineTray/Settings/Settings.cs<|end_filename|>
public class Settings
{
    public HotKey? AcceptAllHotKey { get; set; }
    public HotKey? DiscardAllHotKey { get; set; }
    public HotKey? AcceptOpenHotKey { get; set; }
    public bool RunAtStartup { get; set; }
    public bool TargetOnLeft { get; set; }
    public int MaxInstancesToLaunch { get; set; } = MaxInstance.MaxInstancesToLaunch;
}

<|start_filename|>src/DiffEngine/ToolsOrder.cs<|end_filename|>
﻿using DiffEngine;

static class ToolsOrder
{
    public static IEnumerable<Definition> Sort(bool throwForNoTool, IEnumerable<DiffTool> order)
    {
        var allTools = Definitions.Tools.ToList();
        foreach (var diffTool in order)
        {
            var definition = allTools.SingleOrDefault(x => x.Tool == diffTool);
            if (definition == null)
            {
                if (!throwForNoTool)
                {
                    continue;
                }

                throw new($"`DiffEngine_ToolOrder` is configured to use '{diffTool}' but it is not installed.");
            }

            yield return definition;
            allTools.Remove(definition);
        }

        foreach (var definition in allTools)
        {
            yield return definition;
        }
    }
}

<|start_filename|>src/DiffEngineTray/Payloads/DeletePayload.cs<|end_filename|>
﻿class DeletePayload
{
    public string File { get; set; } = null!;
}

<|start_filename|>src/DiffEngineTray/Settings/HotKeyControl.Designer.cs<|end_filename|>
﻿partial class HotKeyControl
{
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    #region Component Designer generated code

    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
            this.hotKey = new System.Windows.Forms.GroupBox();
            this.hotKeyEnabled = new System.Windows.Forms.CheckBox();
            this.keysSelectionPanel = new System.Windows.Forms.Panel();
            this.keyPanel = new System.Windows.Forms.Panel();
            this.keyCombo = new System.Windows.Forms.ComboBox();
            this.keyLabel = new System.Windows.Forms.Label();
            this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel();
            this.control = new System.Windows.Forms.CheckBox();
            this.alt = new System.Windows.Forms.CheckBox();
            this.shift = new System.Windows.Forms.CheckBox();
            this.helpLabel = new System.Windows.Forms.Label();
            this.hotKey.SuspendLayout();
            this.keysSelectionPanel.SuspendLayout();
            this.keyPanel.SuspendLayout();
            this.flowLayoutPanel1.SuspendLayout();
            this.SuspendLayout();
            // 
            // hotKey
            // 
            this.hotKey.AutoSize = true;
            this.hotKey.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
            this.hotKey.Controls.Add(this.hotKeyEnabled);
            this.hotKey.Controls.Add(this.keysSelectionPanel);
            this.hotKey.Controls.Add(this.helpLabel);
            this.hotKey.Dock = System.Windows.Forms.DockStyle.Top;
            this.hotKey.Location = new System.Drawing.Point(0, 0);
            this.hotKey.Name = "hotKey";
            this.hotKey.Size = new System.Drawing.Size(367, 108);
            this.hotKey.TabIndex = 3;
            this.hotKey.TabStop = false;
            // 
            // hotKeyEnabled
            // 
            this.hotKeyEnabled.AutoSize = true;
            this.hotKeyEnabled.Location = new System.Drawing.Point(6, -1);
            this.hotKeyEnabled.Name = "hotKeyEnabled";
            this.hotKeyEnabled.Size = new System.Drawing.Size(96, 19);
            this.hotKeyEnabled.TabIndex = 3;
            this.hotKeyEnabled.Text = "HotKey Label";
            this.hotKeyEnabled.UseVisualStyleBackColor = true;
            this.hotKeyEnabled.CheckedChanged += new System.EventHandler(this.hotKeyEnabled_CheckedChanged);
            // 
            // keysSelectionPanel
            // 
            this.keysSelectionPanel.AutoSize = true;
            this.keysSelectionPanel.Controls.Add(this.keyPanel);
            this.keysSelectionPanel.Controls.Add(this.flowLayoutPanel1);
            this.keysSelectionPanel.Dock = System.Windows.Forms.DockStyle.Top;
            this.keysSelectionPanel.Enabled = false;
            this.keysSelectionPanel.Location = new System.Drawing.Point(3, 40);
            this.keysSelectionPanel.Margin = new System.Windows.Forms.Padding(2);
            this.keysSelectionPanel.Name = "keysSelectionPanel";
            this.keysSelectionPanel.Padding = new System.Windows.Forms.Padding(5, 4, 5, 4);
            this.keysSelectionPanel.Size = new System.Drawing.Size(361, 65);
            this.keysSelectionPanel.TabIndex = 1;
            // 
            // keyPanel
            // 
            this.keyPanel.Controls.Add(this.keyCombo);
            this.keyPanel.Controls.Add(this.keyLabel);
            this.keyPanel.Dock = System.Windows.Forms.DockStyle.Top;
            this.keyPanel.Location = new System.Drawing.Point(5, 33);
            this.keyPanel.Name = "keyPanel";
            this.keyPanel.Padding = new System.Windows.Forms.Padding(3);
            this.keyPanel.Size = new System.Drawing.Size(351, 28);
            this.keyPanel.TabIndex = 6;
            // 
            // keyCombo
            // 
            this.keyCombo.Dock = System.Windows.Forms.DockStyle.Left;
            this.keyCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
            this.keyCombo.FormattingEnabled = true;
            this.keyCombo.Location = new System.Drawing.Point(38, 3);
            this.keyCombo.Name = "keyCombo";
            this.keyCombo.Size = new System.Drawing.Size(44, 23);
            this.keyCombo.TabIndex = 5;
            // 
            // keyLabel
            // 
            this.keyLabel.AutoSize = true;
            this.keyLabel.Dock = System.Windows.Forms.DockStyle.Left;
            this.keyLabel.Location = new System.Drawing.Point(3, 3);
            this.keyLabel.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
            this.keyLabel.Name = "keyLabel";
            this.keyLabel.Padding = new System.Windows.Forms.Padding(3);
            this.keyLabel.Size = new System.Drawing.Size(35, 21);
            this.keyLabel.TabIndex = 0;
            this.keyLabel.Text = "Key:";
            // 
            // flowLayoutPanel1
            // 
            this.flowLayoutPanel1.AutoSize = true;
            this.flowLayoutPanel1.Controls.Add(this.control);
            this.flowLayoutPanel1.Controls.Add(this.alt);
            this.flowLayoutPanel1.Controls.Add(this.shift);
            this.flowLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Top;
            this.flowLayoutPanel1.Location = new System.Drawing.Point(5, 4);
            this.flowLayoutPanel1.Name = "flowLayoutPanel1";
            this.flowLayoutPanel1.Padding = new System.Windows.Forms.Padding(3);
            this.flowLayoutPanel1.Size = new System.Drawing.Size(351, 29);
            this.flowLayoutPanel1.TabIndex = 4;
            // 
            // control
            // 
            this.control.AutoSize = true;
            this.control.Dock = System.Windows.Forms.DockStyle.Top;
            this.control.Location = new System.Drawing.Point(5, 5);
            this.control.Margin = new System.Windows.Forms.Padding(2);
            this.control.Name = "control";
            this.control.Size = new System.Drawing.Size(66, 19);
            this.control.TabIndex = 7;
            this.control.Text = "Control";
            this.control.UseVisualStyleBackColor = true;
            // 
            // alt
            // 
            this.alt.AutoSize = true;
            this.alt.Dock = System.Windows.Forms.DockStyle.Top;
            this.alt.Location = new System.Drawing.Point(75, 5);
            this.alt.Margin = new System.Windows.Forms.Padding(2);
            this.alt.Name = "alt";
            this.alt.Size = new System.Drawing.Size(41, 19);
            this.alt.TabIndex = 6;
            this.alt.Text = "Alt";
            this.alt.UseVisualStyleBackColor = true;
            // 
            // shift
            // 
            this.shift.AutoSize = true;
            this.shift.Dock = System.Windows.Forms.DockStyle.Top;
            this.shift.Location = new System.Drawing.Point(120, 5);
            this.shift.Margin = new System.Windows.Forms.Padding(2);
            this.shift.Name = "shift";
            this.shift.Size = new System.Drawing.Size(50, 19);
            this.shift.TabIndex = 5;
            this.shift.Text = "Shift";
            this.shift.UseVisualStyleBackColor = true;
            // 
            // helpLabel
            // 
            this.helpLabel.AutoSize = true;
            this.helpLabel.Dock = System.Windows.Forms.DockStyle.Top;
            this.helpLabel.Location = new System.Drawing.Point(3, 19);
            this.helpLabel.Name = "helpLabel";
            this.helpLabel.Padding = new System.Windows.Forms.Padding(3);
            this.helpLabel.Size = new System.Drawing.Size(38, 21);
            this.helpLabel.TabIndex = 2;
            this.helpLabel.Text = "Help";
            // 
            // HotKeyControl
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.Controls.Add(this.hotKey);
            this.Margin = new System.Windows.Forms.Padding(2);
            this.Name = "HotKeyControl";
            this.Size = new System.Drawing.Size(367, 122);
            this.hotKey.ResumeLayout(false);
            this.hotKey.PerformLayout();
            this.keysSelectionPanel.ResumeLayout(false);
            this.keysSelectionPanel.PerformLayout();
            this.keyPanel.ResumeLayout(false);
            this.keyPanel.PerformLayout();
            this.flowLayoutPanel1.ResumeLayout(false);
            this.flowLayoutPanel1.PerformLayout();
            this.ResumeLayout(false);
            this.PerformLayout();

    }

    #endregion

    private System.Windows.Forms.GroupBox hotKey;
    private System.Windows.Forms.Panel keysSelectionPanel;
    private System.Windows.Forms.Panel keyPanel;
    private System.Windows.Forms.ComboBox keyCombo;
    private System.Windows.Forms.Label keyLabel;
    private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel1;
    private System.Windows.Forms.CheckBox control;
    private System.Windows.Forms.CheckBox alt;
    private System.Windows.Forms.CheckBox shift;
    private System.Windows.Forms.Label helpLabel;
    private System.Windows.Forms.CheckBox hotKeyEnabled;
}

<|start_filename|>src/DiffEngineTray/Settings/OptionsFormLauncher.cs<|end_filename|>
static class OptionsFormLauncher
{
    static OptionsForm? instance;

    static EventHandler formOnClosed = (_, _) =>
    {
        instance!.Closed -= formOnClosed;
        instance = null;
    };

    public static async Task Launch(KeyRegister keyRegister, Tracker tracker)
    {
        if (instance != null)
        {
            instance.BringToFront();
            return;
        }

        var settings = await SettingsHelper.Read();
        using var form = new OptionsForm(settings, newSettings => Save(keyRegister, tracker, newSettings));
        instance = form;
        form.Closed += formOnClosed;
        form.ShowDialog();
    }

    static async Task<IReadOnlyList<string>> Save(KeyRegister keyRegister, Tracker tracker, Settings settings)
    {
        if (!settings.IsValidate(out var errors))
        {
            return errors;
        }

        var saveErrors = new List<string>();

        AddHotKey(keyRegister, settings.AcceptAllHotKey, KeyBindingIds.AcceptAll, tracker.AcceptAll, saveErrors);
        AddHotKey(keyRegister, settings.DiscardAllHotKey, KeyBindingIds.DiscardAll, tracker.Clear, saveErrors);
        AddHotKey(keyRegister, settings.AcceptOpenHotKey, KeyBindingIds.AcceptOpen, tracker.AcceptOpen, saveErrors);

        if (saveErrors.Any())
        {
            return saveErrors;
        }

        if (settings.RunAtStartup)
        {
            Startup.Add();
        }
        else
        {
            Startup.Remove();
        }

        await SettingsHelper.Write(settings);
        return new List<string>();
    }

    static void AddHotKey(KeyRegister keyRegister, HotKey? hotKey, int id, Action action, List<string> saveErrors)
    {
        keyRegister.ClearBinding(id);
        if (hotKey == null)
        {
            return;
        }

        if (!keyRegister.TryAddBinding(id, hotKey.Shift, hotKey.Control, hotKey.Alt, hotKey.Key, action))
        {
            saveErrors.Add("Binding already registered");
        }
    }
}

<|start_filename|>src/DiffEngine/Implementation/KDiff3.cs<|end_filename|>
﻿using DiffEngine;

static partial class Implementation
{
    public static Definition KDiff3()
    {
        static string TargetLeftArguments(string temp, string target) =>
            $"\"{target}\" \"{temp}\" --cs CreateBakFiles=0";

        static string TargetRightArguments(string temp, string target) =>
            $"\"{temp}\" \"{target}\" --cs CreateBakFiles=0";

        return new(
            name: DiffTool.KDiff3,
            url: "https://github.com/KDE/kdiff3",
            autoRefresh: false,
            isMdi: false,
            supportsText: true,
            requiresTarget: true,
            cost: "Free",
            binaryExtensions: Array.Empty<string>(),
            windows: new(
                TargetLeftArguments,
                TargetRightArguments, @"%ProgramFiles%\KDiff3\kdiff3.exe"),
            osx: new(
                TargetLeftArguments, 
                TargetRightArguments,
                "/Applications/kdiff3.app/Contents/MacOS/kdiff3"),
            notes: @"
 * `--cs CreateBakFiles=0` to not save a `.orig` file when merging");
    }
}

<|start_filename|>src/DiffEngine/Implementation/SublimeMerge.cs<|end_filename|>
﻿using DiffEngine;

static partial class Implementation
{
    public static Definition SublimeMerge()
    {
        static string TargetLeftArguments(string temp, string target) =>
            $"mergetool \"{target}\" \"{temp}\"";

        static string TargetRightArguments(string temp, string target) =>
            $"mergetool \"{temp}\" \"{target}\"";

        return new(
            name: DiffTool.SublimeMerge,
            url: "https://www.sublimemerge.com/",
            autoRefresh: false,
            isMdi: true,
            supportsText: true,
            requiresTarget: true,
            cost: "Paid",
            binaryExtensions: Array.Empty<string>(),
            windows: new(
                TargetLeftArguments,
                TargetRightArguments,
                @"%ProgramFiles%\Sublime Merge\smerge.exe"),
            linux: new(
                TargetLeftArguments,
                TargetRightArguments, 
                "/usr/bin/smerge"),
            osx: new(
                TargetLeftArguments, 
                TargetRightArguments,
                "/Applications/smerge.app/Contents/MacOS/smerge"),
            notes: "While SublimeMerge is not MDI, it is treated as MDI since it uses a single shared process to managing multiple windows. As such it is not possible to close a Sublime merge process for a specific diff. [Vote for this feature](https://github.com/sublimehq/sublime_merge/issues/1168)");
    }
}

<|start_filename|>src/DiffEngineTray/AssemblyLocation.cs<|end_filename|>
static class AssemblyLocation
{
    static AssemblyLocation()
    {
        var assembly = typeof(AssemblyLocation).Assembly;

        var path = assembly.Location
            .Replace("file:///", "")
            .Replace("file://", "")
            .Replace(@"file:\\\", "")
            .Replace(@"file:\\", "");

        CurrentDirectory = Path.GetDirectoryName(path)!;
    }

    public static readonly string CurrentDirectory;
}

<|start_filename|>src/DiffEngine/TargetPosition.cs<|end_filename|>
﻿static class TargetPosition
{
    public static bool TargetOnLeft { get; private set; }

    static TargetPosition()
    {
        TargetOnLeft = ReadTargetOnLeft().GetValueOrDefault(false);
    }

    static bool? ReadTargetOnLeft()
    {
        var value = Environment.GetEnvironmentVariable("DiffEngine_TargetOnLeft");

        if (value == null)
        {
            return null;
        }

        if (value == "true")
        {
            return true;
        }

        if (value == "false")
        {
            return false;
        }

        throw new($"Unable to parse Position from `DiffEngine_TargetOnLeft`. Must be `true` or `false`. Environment variable: {value}");
    }

    public static void SetTargetOnLeft(bool value)
    {
        if (TargetOnLeft == value)
        {
            return;
        }

        TargetOnLeft = value;
        string? envVariable;
        if (value)
        {
            envVariable = "true";
        }
        else
        {
            envVariable = null;
        }

        Environment.SetEnvironmentVariable("DiffEngine_TargetOnLeft", envVariable, EnvironmentVariableTarget.User);
    }
}

<|start_filename|>src/DiffEngineTray/HotKey/KeyBindingIds.cs<|end_filename|>
static class KeyBindingIds
{
    public static int AcceptAll = 1;
    public static int AcceptOpen = 2;
    public static int DiscardAll = 3;
}

<|start_filename|>src/DiffEngine/BuildArguments.cs<|end_filename|>
namespace DiffEngine;

public delegate string BuildArguments(string tempFile, string targetFile);

<|start_filename|>src/DiffEngineTray/TrackedDelete.cs<|end_filename|>
class TrackedDelete
{
    public TrackedDelete(string file, string? group)
    {
        File = file;
        Group = group;
        Name = Path.GetFileName(file);
    }

    public string Name { get; }
    public string File { get; }
    public string? Group { get; }
}

<|start_filename|>src/DiffEngineTray/Settings/SettingsValidator.cs<|end_filename|>
public static class SettingsValidator
{
    public static bool IsValidate(this Settings settings, out List<string> errors)
    {
        errors = new();

        ValidateHotKey(errors, settings.AcceptAllHotKey);
        ValidateHotKey(errors, settings.DiscardAllHotKey);
        ValidateHotKey(errors, settings.AcceptOpenHotKey);

        return !errors.Any();
    }

    static void ValidateHotKey(List<string> errors, HotKey? hotKey)
    {
        if (hotKey == null)
        {
            return;
        }

        if (!hotKey.Alt && !hotKey.Shift && !hotKey.Control)
        {
            errors.Add("HotKey: At least one modifier is required");
        }

        if (string.IsNullOrWhiteSpace(hotKey.Key))
        {
            errors.Add("HotKey: key is required");
        }
    }
}

<|start_filename|>src/DiffEngine/LaunchResult.cs<|end_filename|>
﻿namespace DiffEngine;

public enum LaunchResult
{
    NoEmptyFileForExtension,
    AlreadyRunningAndSupportsRefresh,
    StartedNewInstance,
    TooManyRunningDiffTools,
    NoDiffToolFound,
    Disabled
}

<|start_filename|>src/DiffEngineTray/HotKey/HotKey.cs<|end_filename|>
public class HotKey
{
    public bool Alt { get; set; }
    public bool Shift { get; set; }
    public bool Control { get; set; }
    public string Key { get; set; } = null!;
}

<|start_filename|>src/DiffEngine/Tray/DiffEngineTray.cs<|end_filename|>
﻿namespace DiffEngine;

public static class DiffEngineTray
{
    static DiffEngineTray()
    {
        if (Mutex.TryOpenExisting("DiffEngine", out var mutex))
        {
            IsRunning = true;
            mutex.Dispose();
        }
    }

    public static bool IsRunning { get; }

    public static void AddDelete(string file)
    {
        if (!IsRunning)
        {
            return;
        }

        PiperClient.SendDelete(file);
    }

    public static void AddMove(
        string tempFile,
        string targetFile,
        string? exe,
        string? arguments,
        bool canKill,
        int? processId)
    {
        if (!IsRunning)
        {
            return;
        }

        PiperClient.SendMove(tempFile, targetFile, exe, arguments, canKill, processId);
    }

    public static Task AddDeleteAsync(string file, CancellationToken cancellation = default)
    {
        if (!IsRunning)
        {
            return Task.CompletedTask;
        }

        return PiperClient.SendDeleteAsync(file, cancellation);
    }

    public static Task AddMoveAsync(
        string tempFile,
        string targetFile,
        string? exe,
        string? arguments,
        bool canKill,
        int? processId,
        CancellationToken cancellation = default)
    {
        if (!IsRunning)
        {
            return Task.CompletedTask;
        }

        return PiperClient.SendMoveAsync(tempFile, targetFile, exe, arguments, canKill, processId, cancellation);
    }
}

<|start_filename|>src/DiffEngineTray/ExceptionHandler.cs<|end_filename|>
using Serilog;

static class ExceptionHandler
{
    public static void Handle(string message, Exception exception)
    {
        Log.Error(exception, message);
        IssueLauncher.LaunchForException(message, exception);
    }
}

<|start_filename|>src/DiffEngine/Implementation/AraxisMerge.cs<|end_filename|>
﻿using DiffEngine;

static partial class Implementation
{
    public static Definition AraxisMerge() =>
        new(
            name: DiffTool.AraxisMerge,
            url: "https://www.araxis.com/merge",
            autoRefresh: true,
            isMdi: true,
            cost: "Paid",
            supportsText: true,
            requiresTarget: true,
            binaryExtensions: new[]
            {
                "bmp",
                "dib",
                "emf",
                "gif",
                "jif",
                "j2c",
                "j2k",
                "jp2",
                "jpc",
                "jpeg",
                "jpg",
                "jpx",
                "pbm", //?
                "pcx",
                "pgm",
                "png",
                "ppm", //?
                "ras", //?
                "tif",
                "tiff",
                "tga",
                "wmf", //?
            },
            windows: new(
                (temp, target) => $"/nowait \"{target}\" \"{temp}\"",
                (temp, target) => $"/nowait \"{temp}\" \"{target}\"",
                @"%ProgramFiles%\Araxis\Araxis Merge\Compare.exe"),
            osx: new(
                (temp, target) => $"-nowait \"{target}\" \"{temp}\"",
                (temp, target) => $"-nowait \"{temp}\" \"{target}\"",
                "/Applications/Araxis Merge.app/Contents/Utilities/compare"),
            notes: @"
 * [Supported image files](https://www.araxis.com/merge/documentation-windows/comparing-image-files.en)
 * [Windows command line usage](https://www.araxis.com/merge/documentation-windows/command-line.en)
 * [MacOS command line usage](https://www.araxis.com/merge/documentation-os-x/command-line.en)
 * [Installing MacOS command line](https://www.araxis.com/merge/documentation-os-x/installing.en)");
    //TODO: add doco about auto refresh
}

<|start_filename|>src/DiffEngine.Tests/WildcardFileFinderTests.cs<|end_filename|>
﻿using Xunit;
using Xunit.Abstractions;

public class WildcardFileFinderTests :
    XunitContextBase
{
    [Fact]
    public void MultiMatchDir_order1()
    {
        var dir1 = Path.Combine(SourceDirectory, "DirForSearch", "dir1");
        var dir2 = Path.Combine(SourceDirectory, "DirForSearch", "dir2");
        Directory.SetLastWriteTime(dir2, DateTime.Now.AddDays(-1));
        Directory.SetLastWriteTime(dir1, DateTime.Now);
        var path = Path.Combine(SourceDirectory, "DirForSearch", "*", "TextFile1.txt");
        Assert.True(WildcardFileFinder.TryFind(path, out var result));
        Assert.True(File.Exists(result), result);
    }

    [Fact]
    public void MultiMatchDir_order2()
    {
        var dir1 = Path.Combine(SourceDirectory, "DirForSearch", "dir1");
        var dir2 = Path.Combine(SourceDirectory, "DirForSearch", "dir2");
        Directory.SetLastWriteTime(dir1, DateTime.Now.AddDays(-1));
        Directory.SetLastWriteTime(dir2, DateTime.Now);
        var path = Path.Combine(SourceDirectory, "DirForSearch", "*", "TextFile1.txt");
        Assert.True(WildcardFileFinder.TryFind(path, out var result));
        Assert.True(File.Exists(result), result);
    }

    [Fact]
    public void FullFilePath()
    {
        Assert.True(WildcardFileFinder.TryFind(SourceFile, out var result));
        Assert.True(File.Exists(result), result);
    }

    [Fact]
    public void FullFilePath_missing()
    {
        Assert.False(WildcardFileFinder.TryFind(SourceFile.Replace(".cs", ".foo"), out var result));
        Assert.Null(result);
    }

    //[Fact]
    //public void WildCardInFile()
    //{
    //    var path = Path.Combine(SourceDirectory, "WildcardFileFinder*.cs");
    //    Assert.True(WildcardFileFinder.TryFind(path, out var result));
    //    Assert.True(File.Exists(result));
    //}

    //[Fact]
    //public void WildCardInFile_missing()
    //{
    //    var path = Path.Combine(SourceDirectory, "WildcardFileFinder*.foo");
    //    Assert.False(WildcardFileFinder.TryFind(path, out var result));
    //    Assert.Null(result);
    //}

    [Fact]
    public void WildCardInDir()
    {
        var directory = SourceDirectory.Replace("Tests", "Test*");
        var path = Path.Combine(directory, "WildcardFileFinderTests.cs");
        Assert.True(WildcardFileFinder.TryFind(path, out var result));
        Assert.True(File.Exists(result), result);
    }

    [Fact]
    public void WildCardInDir_missing()
    {
        var directory = SourceDirectory.Replace("Tests", "Test*.Foo");
        var path = Path.Combine(directory, "WildcardFileFinderTests.cs");
        Assert.False(WildcardFileFinder.TryFind(path, out var result));
        Assert.Null(result);
    }

    public WildcardFileFinderTests(ITestOutputHelper output) :
        base(output)
    {
    }
}

<|start_filename|>src/DiffEngine/MaxInstance.cs<|end_filename|>
﻿static class MaxInstance
{
    public static int MaxInstancesToLaunch { get; private set; } = GetMaxInstances();
    static int launchedInstances;
    const int defaultMax = 5;

    static int GetMaxInstances()
    {
        var variable = Environment.GetEnvironmentVariable("DiffEngine_MaxInstances");

        if (string.IsNullOrEmpty(variable))
        {
            return defaultMax;
        }

        if (ushort.TryParse(variable, out var result))
        {
            return result;
        }

        throw new($"Could not parse the DiffEngine_MaxInstances environment variable: {variable}");
    }

    public static void SetForAppDomain(int value)
    {
        Guard.AgainstNegativeAndZero(value, nameof(value));
        MaxInstancesToLaunch = value;
    }

    public static void SetForUser(int value)
    {
        if (MaxInstancesToLaunch == value)
        {
            return;
        }
        MaxInstancesToLaunch = value;
        string? envVariable;
        if (value == defaultMax)
        {
            envVariable = null;
        }
        else
        {
            envVariable = value.ToString();
        }

        Environment.SetEnvironmentVariable("DiffEngine_MaxInstances", envVariable, EnvironmentVariableTarget.User);
    }

    public static bool Reached()
    {
        var instanceCount = Interlocked.Increment(ref launchedInstances);
        return instanceCount > MaxInstancesToLaunch;
    }
}

<|start_filename|>src/DiffEngine/ClsCompliant.cs<|end_filename|>
﻿[assembly:CLSCompliant(true)]

<|start_filename|>src/DiffEngine/DiffRunner_Kill.cs<|end_filename|>
﻿using EmptyFiles;

namespace DiffEngine;

public static partial class DiffRunner
{
    /// <summary>
    /// Find and kill a diff tool process.
    /// </summary>
    public static void Kill(string tempFile, string targetFile)
    {
        if (Disabled)
        {
            return;
        }

        var extension = Extensions.GetExtension(tempFile);
        if (!DiffTools.TryFind(extension, out var diffTool))
        {
            Logging.Write($"Extension not found. {extension}");
            return;
        }

        var command = diffTool.BuildCommand(tempFile, targetFile);

        if (diffTool.IsMdi)
        {
            Logging.Write($"DiffTool is Mdi so not killing. diffTool: {diffTool.ExePath}");
            return;
        }

        ProcessCleanup.Kill(command);
    }
}

<|start_filename|>src/DiffEngine/DiffTool.cs<|end_filename|>
namespace DiffEngine;

public enum DiffTool
{
    BeyondCompare,
    P4MergeText,
    P4MergeImage,
    AraxisMerge,
    Meld,
    SublimeMerge,
    Kaleidoscope,
    CodeCompare,
    DeltaWalker,
    WinMerge,
    DiffMerge,
    TortoiseMerge,
    TortoiseGitMerge,
    TortoiseIDiff,
    KDiff3,
    TkDiff,
    Guiffy,
    ExamDiff,
    Diffinity,
    VisualStudio,
    VisualStudioCode,
    Rider,
    Vim,
    Neovim
}

<|start_filename|>src/DiffEngine/Implementation/P4MergeText.cs<|end_filename|>
﻿using DiffEngine;

static partial class Implementation
{
    public static Definition P4MergeText()
    {
        static string TargetLeftArguments(string temp, string target) =>
            $"-C utf8-bom \"{temp}\" \"{target}\" \"{target}\" \"{target}\"";

        static string TargetRightArguments(string temp, string target) =>
            $"-C utf8-bom \"{target}\" \"{temp}\" \"{target}\" \"{target}\"";

        return new(
            name: DiffTool.P4MergeText,
            url: "https://www.perforce.com/products/helix-core-apps/merge-diff-tool-p4merge",
            autoRefresh: false,
            isMdi: false,
            supportsText: true,
            requiresTarget: true,
            cost: "Free",
            binaryExtensions: Array.Empty<string>(),
            windows: new(
                TargetLeftArguments,
                TargetRightArguments,
                @"%ProgramFiles%\Perforce\p4merge.exe"),
            linux: new(
                TargetLeftArguments,
                TargetRightArguments,
                "/usr/bin/p4merge"),
            osx: new(
                TargetLeftArguments,
                TargetRightArguments,
                "/Applications/p4merge.app/Contents/MacOS/p4merge"));
    }
}

<|start_filename|>src/DiffEngineTray/Settings/SettingsHelper.cs<|end_filename|>
using System.Text.Json;

static class SettingsHelper
{
    public static string FilePath;

    static SettingsHelper()
    {
        var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        var directory = Path.Combine(appData, "DiffEngine");
        Directory.CreateDirectory(directory);
        FilePath = Path.Combine(directory, "settings.json");
    }

    public static async Task<Settings> Read()
    {
        Settings settings;
        if (File.Exists(FilePath))
        {
            await using var stream = File.OpenRead(FilePath);
            settings = (await JsonSerializer.DeserializeAsync<Settings>(stream))!;
        }
        else
        {
            await File.WriteAllTextAsync(FilePath, "{}");
            settings = new();
        }

        settings.TargetOnLeft = TargetPosition.TargetOnLeft;
        settings.MaxInstancesToLaunch = MaxInstance.MaxInstancesToLaunch;
        return settings;
    }

    public static async Task Write(Settings settings)
    {
        TargetPosition.SetTargetOnLeft(settings.TargetOnLeft);
        MaxInstance.SetForUser(settings.MaxInstancesToLaunch);
        File.Delete(FilePath);
        await using var stream = File.OpenWrite(FilePath);
        await JsonSerializer.SerializeAsync(stream, settings);
    }
}