diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index ba78d83..1aba38f 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -6,7 +6,7 @@ on:
jobs:
pre-commit:
- runs-on: ubuntu-latest
+ runs-on: windows-2025
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v3
diff --git a/PasteIntoFile.Core/PasteIntoFile.Core.csproj b/PasteIntoFile.Core/PasteIntoFile.Core.csproj
new file mode 100644
index 0000000..b7f60bb
--- /dev/null
+++ b/PasteIntoFile.Core/PasteIntoFile.Core.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net9.0-windows
+ enable
+ enable
+ PasteIntoFile.Core
+
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+ True
+ True
+ Settings.settings
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
diff --git a/PasteIntoFile.Core/Properties/Resources.Designer.cs b/PasteIntoFile.Core/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..734b04f
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.Designer.cs
@@ -0,0 +1,107 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace PasteIntoFile.Core.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ public Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PasteIntoFile.Core.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Paste into file.
+ ///
+ public static string str_contextentry {
+ get {
+ return ResourceManager.GetString("str_contextentry", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Append.
+ ///
+ public static string str_contextentry_append {
+ get {
+ return ResourceManager.GetString("str_contextentry_append", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Copy file content.
+ ///
+ public static string str_contextentry_copyfromfile {
+ get {
+ return ResourceManager.GetString("str_contextentry_copyfromfile", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Paste into this file.
+ ///
+ public static string str_contextentry_paste_into_existing_file {
+ get {
+ return ResourceManager.GetString("str_contextentry_paste_into_existing_file", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Replace.
+ ///
+ public static string str_contextentry_replace {
+ get {
+ return ResourceManager.GetString("str_contextentry_replace", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/PasteIntoFile.Core/Properties/Resources.de.resx b/PasteIntoFile.Core/Properties/Resources.de.resx
new file mode 100644
index 0000000..dc137cb
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.de.resx
@@ -0,0 +1,33 @@
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Dateiinhalt kopieren
+ Context menu entry for copying
+
+
+
+ Einfügen in diese Datei
+
+
+ Einfügen als Datei
+ Context menu entry for pasting
+
+
+ Anfügen
+
+
+ Ersetzen
+
+
diff --git a/PasteIntoFile.Core/Properties/Resources.es-es.resx b/PasteIntoFile.Core/Properties/Resources.es-es.resx
new file mode 100644
index 0000000..0220df3
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.es-es.resx
@@ -0,0 +1,33 @@
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Copiar contenido del archivo
+ Context menu entry for copying
+
+
+
+ Pegar en este archivo
+
+
+ Pegar en fichero
+ Context menu entry for pasting
+
+
+ Anexar
+
+
+ Reemplazar
+
+
diff --git a/PasteIntoFile.Core/Properties/Resources.fr.resx b/PasteIntoFile.Core/Properties/Resources.fr.resx
new file mode 100644
index 0000000..0b3615f
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.fr.resx
@@ -0,0 +1,33 @@
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Copier le contenu du fichier
+ Context menu entry for copying
+
+
+
+ Coller dans ce fichier
+
+
+ Coller dans un fichier
+ Context menu entry for pasting
+
+
+ Ajouter
+
+
+ Remplacer
+
+
diff --git a/PasteIntoFile.Core/Properties/Resources.it.resx b/PasteIntoFile.Core/Properties/Resources.it.resx
new file mode 100644
index 0000000..b60db7e
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.it.resx
@@ -0,0 +1,33 @@
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Copia il contenuto del file
+ Context menu entry for copying
+
+
+
+ Incolla in questo file
+
+
+ Incolla nel file
+ Context menu entry for pasting
+
+
+ Aggiungi
+
+
+ Sostituisci
+
+
diff --git a/PasteIntoFile.Core/Properties/Resources.ja-jp.resx b/PasteIntoFile.Core/Properties/Resources.ja-jp.resx
new file mode 100644
index 0000000..31c0b32
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.ja-jp.resx
@@ -0,0 +1,33 @@
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ ファイルの内容をコピー
+ Context menu entry for copying
+
+
+
+ このファイルに貼り付け
+
+
+ ファイルに貼り付け
+ Context menu entry for pasting
+
+
+ 追記
+
+
+ 置き換え
+
+
diff --git a/PasteIntoFile.Core/Properties/Resources.ko.resx b/PasteIntoFile.Core/Properties/Resources.ko.resx
new file mode 100644
index 0000000..b7080d2
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.ko.resx
@@ -0,0 +1,33 @@
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 파일 내용 복사
+ Context menu entry for copying
+
+
+
+ 이 파일에 붙여넣기
+
+
+ 파일에 붙여넣기
+ Context menu entry for pasting
+
+
+ 추가
+
+
+ 바꾸기
+
+
diff --git a/PasteIntoFile.Core/Properties/Resources.pt-br.resx b/PasteIntoFile.Core/Properties/Resources.pt-br.resx
new file mode 100644
index 0000000..652144d
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.pt-br.resx
@@ -0,0 +1,33 @@
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Copiar conteúdo do arquivo
+ Context menu entry for copying
+
+
+
+ Colar nesse arquivo
+
+
+ Colar no arquivo
+ Context menu entry for pasting
+
+
+ Acrescentar
+
+
+ Substituir
+
+
diff --git a/PasteIntoFile.Core/Properties/Resources.pt-pt.resx b/PasteIntoFile.Core/Properties/Resources.pt-pt.resx
new file mode 100644
index 0000000..e7620a2
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.pt-pt.resx
@@ -0,0 +1,33 @@
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Copiar conteúdo do ficheiro
+ Context menu entry for copying
+
+
+
+ Colar neste ficheiro
+
+
+ Colar no ficheiro
+ Context menu entry for pasting
+
+
+ Acrescentar
+
+
+ Substituir
+
+
diff --git a/PasteIntoFile.Core/Properties/Resources.resx b/PasteIntoFile.Core/Properties/Resources.resx
new file mode 100644
index 0000000..7d9a4ac
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.resx
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Copy file content
+ Context menu entry for copying
+
+
+
+ Paste into this file
+
+
+ Paste into file
+ Context menu entry for pasting
+
+
+ Append
+
+
+ Replace
+
+
diff --git a/PasteIntoFile.Core/Properties/Resources.ru-ru.resx b/PasteIntoFile.Core/Properties/Resources.ru-ru.resx
new file mode 100644
index 0000000..541a3c8
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.ru-ru.resx
@@ -0,0 +1,33 @@
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Копировать содержимое файла
+ Context menu entry for copying
+
+
+
+ Вставить в этот файл
+
+
+ Вставить в файл
+ Context menu entry for pasting
+
+
+ Дополнить
+
+
+ Заменить
+
+
diff --git a/PasteIntoFile.Core/Properties/Resources.uk-ua.resx b/PasteIntoFile.Core/Properties/Resources.uk-ua.resx
new file mode 100644
index 0000000..ae40532
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.uk-ua.resx
@@ -0,0 +1,19 @@
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Вставити до файлу
+ Context menu entry for pasting
+
+
diff --git a/PasteIntoFile.Core/Properties/Resources.zh-CN.resx b/PasteIntoFile.Core/Properties/Resources.zh-CN.resx
new file mode 100644
index 0000000..24a58d2
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.zh-CN.resx
@@ -0,0 +1,33 @@
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 复制文件内容
+ Context menu entry for copying
+
+
+
+ 粘贴到此文件中
+
+
+ Paste Into File
+ Context menu entry for pasting
+
+
+ 追加
+
+
+ 替换
+
+
diff --git a/PasteIntoFile.Core/Properties/Resources.zh-TW.resx b/PasteIntoFile.Core/Properties/Resources.zh-TW.resx
new file mode 100644
index 0000000..f895455
--- /dev/null
+++ b/PasteIntoFile.Core/Properties/Resources.zh-TW.resx
@@ -0,0 +1,33 @@
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 複製檔案內容
+ Context menu entry for copying
+
+
+
+ 貼入此檔案
+
+
+ Paste Into File
+ Context menu entry for pasting
+
+
+ 追加
+
+
+ 取代
+
+
diff --git a/PasteIntoFile/Properties/Settings.Designer.cs b/PasteIntoFile.Core/Properties/Settings.Designer.cs
similarity index 98%
rename from PasteIntoFile/Properties/Settings.Designer.cs
rename to PasteIntoFile.Core/Properties/Settings.Designer.cs
index 7188d72..98e496a 100644
--- a/PasteIntoFile/Properties/Settings.Designer.cs
+++ b/PasteIntoFile.Core/Properties/Settings.Designer.cs
@@ -7,12 +7,12 @@
//
//------------------------------------------------------------------------------
-namespace PasteIntoFile.Properties {
+namespace PasteIntoFile.Core {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.9.0.0")]
- internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
+ public sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
@@ -25,36 +25,36 @@ public static Settings Default {
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
- public bool clrClipboard {
+ public bool autoSave {
get {
- return ((bool)(this["clrClipboard"]));
+ return ((bool)(this["autoSave"]));
}
set {
- this["clrClipboard"] = value;
+ this["autoSave"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Configuration.DefaultSettingValueAttribute("True")]
- public bool firstLaunch {
+ [global::System.Configuration.DefaultSettingValueAttribute("False")]
+ public bool clrClipboard {
get {
- return ((bool)(this["firstLaunch"]));
+ return ((bool)(this["clrClipboard"]));
}
set {
- this["firstLaunch"] = value;
+ this["clrClipboard"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Configuration.DefaultSettingValueAttribute("False")]
- public bool autoSave {
+ [global::System.Configuration.DefaultSettingValueAttribute("True")]
+ public bool firstLaunch {
get {
- return ((bool)(this["autoSave"]));
+ return ((bool)(this["firstLaunch"]));
}
set {
- this["autoSave"] = value;
+ this["firstLaunch"] = value;
}
}
diff --git a/PasteIntoFile/Properties/Settings.settings b/PasteIntoFile.Core/Properties/Settings.settings
similarity index 89%
rename from PasteIntoFile/Properties/Settings.settings
rename to PasteIntoFile.Core/Properties/Settings.settings
index 5104419..5a7de8b 100644
--- a/PasteIntoFile/Properties/Settings.settings
+++ b/PasteIntoFile.Core/Properties/Settings.settings
@@ -1,17 +1,16 @@
-
-
-
+
+
+
+
+ False
+
False
True
-
- False
-
{0:yyyy-MM-dd HH-mm-ss}
@@ -34,10 +33,10 @@
False
-
+
-
+
2000-01-01
@@ -52,7 +51,7 @@
False
-
+
True
diff --git a/PasteIntoFile.Core/Settings.cs b/PasteIntoFile.Core/Settings.cs
new file mode 100644
index 0000000..bb215ad
--- /dev/null
+++ b/PasteIntoFile.Core/Settings.cs
@@ -0,0 +1,9 @@
+namespace PasteIntoFile.Core;
+
+///
+/// Shared settings class that both the main app and shell extension use.
+/// Properties are generated by Settings.Designer.cs from Settings.settings.
+/// Call SettingsUtil.Apply(Settings.Default) before accessing any settings.
+///
+public sealed partial class Settings {
+}
diff --git a/PasteIntoFile.Core/SettingsUtil.cs b/PasteIntoFile.Core/SettingsUtil.cs
new file mode 100644
index 0000000..d0931c8
--- /dev/null
+++ b/PasteIntoFile.Core/SettingsUtil.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Configuration;
+using System.IO;
+using Bluegrams.Application;
+
+namespace PasteIntoFile.Core;
+
+///
+/// Utility to configure and apply PortableSettingsProvider with the correct
+/// settings file path. In installer mode, settings are stored in
+/// %APPDATA%\PasteIntoFile\user.config. In portable mode (#if PORTABLE),
+/// settings are stored next to the executable.
+///
+public static class SettingsUtil {
+ private const string SettingsFileName = "user.config";
+ private const string AppDataFolderName = "PasteIntoFile";
+
+ ///
+ /// Applies the PortableSettingsProvider to the given settings instance,
+ /// configuring it to use the shared settings file path.
+ /// Call this once at application startup before accessing any settings.
+ ///
+ public static void Apply(ApplicationSettingsBase settings) {
+ PortableSettingsProvider.SettingsFileName = SettingsFileName;
+ PortableSettingsProvider.SettingsDirectory = GetSettingsDirectory();
+ PortableSettingsProvider.ApplyProvider(settings);
+ }
+
+ ///
+ /// Returns the full path to the shared settings file.
+ ///
+ public static string SettingsFilePath => Path.Combine(GetSettingsDirectory(), SettingsFileName);
+
+ private static string GetSettingsDirectory() {
+#if PORTABLE
+ var assemblyPath = typeof(SettingsUtil).Assembly.Location;
+ return Path.GetDirectoryName(assemblyPath) ?? AppContext.BaseDirectory;
+#else
+ var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ var settingsDir = Path.Combine(appData, AppDataFolderName);
+ if (!Directory.Exists(settingsDir)) {
+ Directory.CreateDirectory(settingsDir);
+ }
+ return settingsDir;
+#endif
+ }
+
+ ///
+ /// Reloads the settings from disk only if the settings file has been modified
+ /// since the last successful reload. This avoids expensive file I/O on slow
+ /// storage (USB flash drives, network shares) when the file hasn't changed.
+ /// Returns true if a reload was performed, false if the cached value was still valid.
+ ///
+ public static bool ReloadIfChanged(ApplicationSettingsBase settings) {
+ var filePath = SettingsFilePath;
+ try {
+ if (!File.Exists(filePath)) {
+ // File doesn't exist — nothing to reload
+ return false;
+ }
+
+ var lastWriteTime = File.GetLastWriteTimeUtc(filePath);
+
+ if (lastWriteTime <= sLastReloadTime) {
+ // File hasn't changed since last reload — skip the expensive Reload()
+ return false;
+ }
+
+ settings.Reload();
+ sLastReloadTime = lastWriteTime;
+ return true;
+ } catch {
+ // If we can't check the file, fall back to always reloading
+ // to ensure we get the latest value
+ settings.Reload();
+ return true;
+ }
+ }
+
+ private static DateTime sLastReloadTime;
+}
diff --git a/PasteIntoFile.ShellExtension/AppendCommand.cs b/PasteIntoFile.ShellExtension/AppendCommand.cs
new file mode 100644
index 0000000..20d1fae
--- /dev/null
+++ b/PasteIntoFile.ShellExtension/AppendCommand.cs
@@ -0,0 +1,66 @@
+using System.Runtime.InteropServices;
+using PasteIntoFile.Core.Properties;
+
+namespace PasteIntoFileShellExtension;
+
+[ComVisible(true)]
+[Guid(Clsids.AppendCommand)]
+[ProgId("PasteIntoFile.ShellExtension.AppendCommand")]
+[ClassInterface(ClassInterfaceType.None)]
+[ComDefaultInterface(typeof(IExplorerCommand))]
+public sealed class AppendCommand : BaseExplorerCommand {
+
+ public override string Clsid => Clsids.AppendCommand;
+
+ public override int GetTitle(IShellItemArray psiItemArray, out IntPtr ppszName) {
+ ppszName = Marshal.StringToCoTaskMemUni(Resources.str_contextentry_append);
+ return HResult.S_OK;
+ }
+
+ // File extensions that do NOT support appending (binary / image formats)
+ private static readonly string[] AppendNotSupportedExtensions = new[] {
+ "bmp", "gif", "ico", "jpeg", "jpg", "png", "tif", "tiff", "webp", // images
+ "svg", // vector
+ "url" // shortcuts
+ };
+
+ // Check if appending is supported for the given file path.
+ private static bool IsAppendSupported(string? path) {
+ if (string.IsNullOrEmpty(path)) {
+ return true;
+ }
+ var ext = Path.GetExtension(path)?.TrimStart('.').ToLowerInvariant();
+ return string.IsNullOrEmpty(ext) || Array.IndexOf(AppendNotSupportedExtensions, ext) < 0;
+ }
+
+ // Check if exactly one file is selected and append is supported for it.
+ // Returns ECS_ENABLED if so, otherwise ECS_HIDDEN.
+ private static EXPCMDSTATE CheckSingleFileSelectedWithAppendSupport(IShellItemArray psiItemArray) {
+ if (psiItemArray != null && psiItemArray.GetCount(out var count) == HResult.S_OK && count == 1) {
+ psiItemArray.GetItemAt(0, out IShellItem? shellItem);
+ if (shellItem != null) {
+ try {
+ if (shellItem.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out var pathPtr) == HResult.S_OK && pathPtr != IntPtr.Zero) {
+ var path = Marshal.PtrToStringUni(pathPtr);
+ Marshal.FreeCoTaskMem(pathPtr);
+ if (!string.IsNullOrEmpty(path) && IsAppendSupported(path)) {
+ return EXPCMDSTATE.ECS_ENABLED;
+ }
+ }
+ } finally {
+ Marshal.ReleaseComObject(shellItem);
+ }
+ }
+ }
+ return EXPCMDSTATE.ECS_HIDDEN;
+ }
+
+ public override int GetState(IShellItemArray psiItemArray, bool fOkToBeSlow, out EXPCMDSTATE pCmdState) {
+ pCmdState = CheckSingleFileSelectedWithAppendSupport(psiItemArray);
+ return HResult.S_OK;
+ }
+
+ public override int Invoke(IShellItemArray psiItemArray, IntPtr pbc) {
+ return InvokePasteIntoFile(psiItemArray, PasteMode.Append);
+ }
+}
diff --git a/PasteIntoFile.ShellExtension/AppxManifest.xml b/PasteIntoFile.ShellExtension/AppxManifest.xml
new file mode 100644
index 0000000..db8507a
--- /dev/null
+++ b/PasteIntoFile.ShellExtension/AppxManifest.xml
@@ -0,0 +1,44 @@
+
+
+
+
+ PasteIntoFile
+ PasteIntoFile Dev
+ icon.png
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PasteIntoFile.ShellExtension/BaseExplorerCommand.cs b/PasteIntoFile.ShellExtension/BaseExplorerCommand.cs
new file mode 100644
index 0000000..4b7716f
--- /dev/null
+++ b/PasteIntoFile.ShellExtension/BaseExplorerCommand.cs
@@ -0,0 +1,393 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using PasteIntoFile.Core;
+
+namespace PasteIntoFileShellExtension;
+
+
+// Shared COM class IDs — must match AppxManifest.xml registrations
+static class Clsids {
+ public const string PasteCommand = "A6F24F95-0E3D-4A37-8E58-1F05D66E76A5";
+ public const string CopyCommand = "41C9024D-4DCC-4CFE-B19B-C267B2525F63";
+ public const string ReplaceCommand = "90220591-C089-4812-AF4B-510694581EF4";
+ public const string AppendCommand = "668F5257-E4B7-4DE7-84AE-B1AA710F38F5";
+}
+
+
+// ─── Abstract base for all explorer commands ──────────────────────────────
+// Implements the common IExplorerCommand boilerplate (GetIcon, GetToolTip,
+// GetCanonicalName, GetFlags, EnumSubCommands) so each derived command only
+// provides its title, state check, and invoke logic.
+
+public enum PasteMode { Paste, Copy, Replace, Append }
+
+public abstract class BaseExplorerCommand : IExplorerCommand {
+ static BaseExplorerCommand() {
+ // Configure settings provider to use the shared settings file path
+ SettingsUtil.Apply(Settings.Default);
+ }
+
+ public abstract string Clsid { get; }
+
+ // ─── Shared helper methods ───
+
+ // Get the path to the PasteIntoFile executable relative to the assembly location.
+ protected static string? GetPasteIntoFileExecutablePath() {
+ var extensionDirectory = Path.GetDirectoryName(typeof(BaseExplorerCommand).Assembly.Location);
+ if (string.IsNullOrWhiteSpace(extensionDirectory)) {
+ extensionDirectory = AppContext.BaseDirectory;
+ }
+ if (string.IsNullOrWhiteSpace(extensionDirectory)) {
+ return null;
+ }
+ return Path.Combine(extensionDirectory, "PasteIntoFile.exe");
+ }
+
+ // Check if exactly one file is selected and it exists on disk.
+ // Returns ECS_ENABLED if so, otherwise ECS_HIDDEN.
+ protected static EXPCMDSTATE CheckSingleFileSelected(IShellItemArray psiItemArray) {
+ if (psiItemArray != null && psiItemArray.GetCount(out var count) == HResult.S_OK && count == 1) {
+ psiItemArray.GetItemAt(0, out IShellItem? shellItem);
+ if (shellItem != null) {
+ try {
+ if (shellItem.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out var pathPtr) == HResult.S_OK && pathPtr != IntPtr.Zero) {
+ var path = Marshal.PtrToStringUni(pathPtr);
+ Marshal.FreeCoTaskMem(pathPtr);
+ if (!string.IsNullOrEmpty(path) && File.Exists(path)) {
+ return EXPCMDSTATE.ECS_ENABLED;
+ }
+ }
+ } finally {
+ Marshal.ReleaseComObject(shellItem);
+ }
+ }
+ }
+ return EXPCMDSTATE.ECS_HIDDEN;
+ }
+
+ // Extract the file system path of the first item in the array.
+ // Returns null if the array is empty or the path cannot be retrieved.
+ protected static string? GetSingleItemPath(IShellItemArray? psiItemArray) {
+ if (psiItemArray?.GetCount(out var count) != HResult.S_OK || count == 0) {
+ return null;
+ }
+ psiItemArray.GetItemAt(0, out IShellItem? shellItem);
+ if (shellItem == null) {
+ return null;
+ }
+ try {
+ if (shellItem.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out var pathPtr) != HResult.S_OK || pathPtr == IntPtr.Zero) {
+ return null;
+ }
+ var path = Marshal.PtrToStringUni(pathPtr);
+ Marshal.FreeCoTaskMem(pathPtr);
+ return path;
+ } finally {
+ Marshal.ReleaseComObject(shellItem);
+ }
+ }
+
+ // Invoke PasteIntoFile with the specified mode.
+ // Handles all four command types: paste to directory, copy file contents, replace, and append.
+ protected static int InvokePasteIntoFile(IShellItemArray psiItemArray, PasteMode mode) {
+ try {
+ var exePath = GetPasteIntoFileExecutablePath();
+ if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath)) {
+ return HResult.E_FAIL;
+ }
+
+ var startInfo = new ProcessStartInfo {
+ FileName = exePath,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ var path = GetSingleItemPath(psiItemArray);
+
+ switch (mode) {
+ case PasteMode.Paste:
+ startInfo.ArgumentList.Add("paste");
+ if (!string.IsNullOrWhiteSpace(path)) {
+ startInfo.ArgumentList.Add(path);
+ }
+ break;
+
+ case PasteMode.Copy:
+ if (string.IsNullOrEmpty(path)) {
+ return HResult.E_FAIL;
+ }
+ startInfo.ArgumentList.Add("copy");
+ startInfo.ArgumentList.Add(path);
+ break;
+
+ case PasteMode.Replace:
+ case PasteMode.Append:
+ var directory = Path.GetDirectoryName(path);
+ var file = Path.GetFileName(path);
+ if (string.IsNullOrEmpty(directory) || string.IsNullOrEmpty(file)) {
+ return HResult.E_FAIL;
+ }
+ startInfo.ArgumentList.Add("paste");
+ startInfo.ArgumentList.Add(directory);
+ startInfo.ArgumentList.Add("--filename");
+ startInfo.ArgumentList.Add(file);
+ startInfo.ArgumentList.Add("--autosave=true");
+ startInfo.ArgumentList.Add(mode == PasteMode.Replace ? "--overwrite=true" : "--append");
+ break;
+ }
+
+ Process.Start(startInfo);
+ return HResult.S_OK;
+ } catch {
+ return HResult.E_FAIL;
+ }
+ }
+
+ // ─── IExplorerCommand implementation ───
+
+ public abstract int GetTitle(IShellItemArray psiItemArray, out IntPtr ppszName);
+
+ public int GetIcon(IShellItemArray psiItemArray, out IntPtr ppszIcon) {
+ var exePath = GetPasteIntoFileExecutablePath();
+ if (!string.IsNullOrWhiteSpace(exePath) && File.Exists(exePath)) {
+ ppszIcon = Marshal.StringToCoTaskMemUni(exePath + ",0");
+ return HResult.S_OK;
+ }
+ ppszIcon = IntPtr.Zero;
+ return HResult.E_NOTIMPL;
+ }
+
+ public int GetToolTip(IShellItemArray psiItemArray, out IntPtr ppszInfotip) {
+ ppszInfotip = IntPtr.Zero;
+ return HResult.E_NOTIMPL;
+ }
+
+ public int GetCanonicalName(out Guid pguidCommandName) {
+ pguidCommandName = new Guid(Clsid);
+ return HResult.S_OK;
+ }
+
+ public abstract int GetState(IShellItemArray psiItemArray, bool fOkToBeSlow, out EXPCMDSTATE pCmdState);
+
+ public abstract int Invoke(IShellItemArray psiItemArray, IntPtr pbc);
+
+ public virtual int GetFlags(out EXPCMDFLAGS pFlags) {
+ pFlags = EXPCMDFLAGS.ECF_DEFAULT;
+ return HResult.S_OK;
+ }
+
+ public virtual int EnumSubCommands(out IEnumExplorerCommand ppEnum) {
+ ppEnum = null!;
+ return HResult.E_NOTIMPL;
+ }
+}
+
+[ComVisible(true)]
+[Guid("A08CE4D0-FA25-44AB-B57C-C7B1C323E0B9")]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+public interface IExplorerCommand {
+ [PreserveSig]
+ int GetTitle(
+ [MarshalAs(UnmanagedType.Interface)] IShellItemArray psiItemArray,
+ out IntPtr ppszName
+ );
+
+ [PreserveSig]
+ int GetIcon(
+ [MarshalAs(UnmanagedType.Interface)] IShellItemArray psiItemArray,
+ out IntPtr ppszIcon
+ );
+
+ [PreserveSig]
+ int GetToolTip(
+ [MarshalAs(UnmanagedType.Interface)] IShellItemArray psiItemArray,
+ out IntPtr ppszInfotip
+ );
+
+ [PreserveSig]
+ int GetCanonicalName(out Guid pguidCommandName);
+
+ [PreserveSig]
+ int GetState(
+ [MarshalAs(UnmanagedType.Interface)] IShellItemArray psiItemArray,
+ [MarshalAs(UnmanagedType.Bool)] bool fOkToBeSlow,
+ out EXPCMDSTATE pCmdState
+ );
+
+ [PreserveSig]
+ int Invoke(
+ [MarshalAs(UnmanagedType.Interface)] IShellItemArray psiItemArray,
+ IntPtr pbc
+ );
+
+ [PreserveSig]
+ int GetFlags(out EXPCMDFLAGS pFlags);
+
+ [PreserveSig]
+ int EnumSubCommands(
+ [MarshalAs(UnmanagedType.Interface)] out IEnumExplorerCommand ppEnum
+ );
+}
+
+[ComImport]
+[Guid("B63EA76D-1F85-456F-A19C-48159EFA858B")]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+public interface IShellItemArray {
+ [PreserveSig]
+ int BindToHandler(
+ IntPtr pbc,
+ ref Guid bhid,
+ ref Guid riid,
+ out IntPtr ppvOut
+ );
+
+ [PreserveSig]
+ int GetPropertyStore(
+ GETPROPERTYSTOREFLAGS flags,
+ ref Guid riid,
+ out IntPtr ppv
+ );
+
+ [PreserveSig]
+ int GetPropertyDescriptionList(
+ ref PROPERTYKEY keyType,
+ ref Guid riid,
+ out IntPtr ppv
+ );
+
+ [PreserveSig]
+ int GetAttributes(
+ SIATTRIBFLAGS attribFlags,
+ uint sfgaoMask,
+ out uint psfgaoAttribs
+ );
+
+ [PreserveSig]
+ int GetCount(out uint pdwNumItems);
+
+ [PreserveSig]
+ int GetItemAt(
+ uint dwIndex,
+ [MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi
+ );
+
+ [PreserveSig]
+ int EnumItems(out IntPtr ppenumShellItems);
+}
+
+[ComImport]
+[Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+public interface IShellItem {
+ [PreserveSig]
+ int BindToHandler(
+ IntPtr pbc,
+ ref Guid bhid,
+ ref Guid riid,
+ out IntPtr ppv
+ );
+
+ [PreserveSig]
+ int GetParent(
+ [MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi
+ );
+
+ [PreserveSig]
+ int GetDisplayName(
+ SIGDN sigdnName,
+ out IntPtr ppszName
+ );
+
+ [PreserveSig]
+ int GetAttributes(
+ uint sfgaoMask,
+ out uint psfgaoAttribs
+ );
+
+ [PreserveSig]
+ int Compare(
+ [MarshalAs(UnmanagedType.Interface)] IShellItem psi,
+ uint hint,
+ out int piOrder
+ );
+}
+
+[ComImport]
+[Guid("A88826F8-186F-4987-AADE-EA0CEF8FBFE8")]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+public interface IEnumExplorerCommand {
+ [PreserveSig]
+ int Next(uint celt, out IExplorerCommand rgelt, out uint pceltFetched);
+
+ [PreserveSig]
+ int Skip(uint celt);
+
+ [PreserveSig]
+ int Reset();
+
+ [PreserveSig]
+ int Clone(out IEnumExplorerCommand ppEnum);
+}
+
+
+static class HResult {
+ public const int S_OK = 0;
+ public const int S_FALSE = 1;
+ public const int E_NOTIMPL = unchecked((int)0x80004001);
+ public const int E_FAIL = unchecked((int)0x80004005);
+}
+
+public enum EXPCMDSTATE {
+ ECS_ENABLED = 0,
+ ECS_DISABLED = 1,
+ ECS_HIDDEN = 2,
+ ECS_CHECKBOX = 4,
+ ECS_CHECKED = 8,
+ ECS_RADIOCHECK = 16
+}
+
+[Flags]
+public enum EXPCMDFLAGS {
+ ECF_DEFAULT = 0,
+ ECF_HASSUBCOMMANDS = 1,
+ ECF_HASSPLITBUTTON = 2,
+ ECF_HIDELABEL = 4,
+ ECF_ISSEPARATOR = 8,
+ ECF_HASLUASHIELD = 16,
+ ECF_SEPARATORBEFORE = 32,
+ ECF_SEPARATORAFTER = 64,
+ ECF_ISDROPDOWN = 128,
+ ECF_TOGGLEABLE = 256,
+ ECF_AUTOMENUICONS = 512
+}
+
+public enum SIGDN : uint {
+ SIGDN_NORMALDISPLAY = 0x00000000,
+ SIGDN_PARENTRELATIVEPARSING = 0x80018001,
+ SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000,
+ SIGDN_PARENTRELATIVEEDITING = 0x80031001,
+ SIGDN_DESKTOPABSOLUTEEDITING = 0x8004C000,
+ SIGDN_FILESYSPATH = 0x80058000,
+ SIGDN_URL = 0x80068000,
+ SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8007C001,
+ SIGDN_PARENTRELATIVE = 0x80080001,
+ SIGDN_PARENTRELATIVEFORUI = 0x80094001
+}
+
+public enum GETPROPERTYSTOREFLAGS {
+ GPS_DEFAULT = 0
+}
+
+public enum SIATTRIBFLAGS {
+ SIATTRIBFLAGS_AND = 1,
+ SIATTRIBFLAGS_OR = 2,
+ SIATTRIBFLAGS_APPCOMPAT = 3
+}
+
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+public struct PROPERTYKEY {
+ public Guid fmtid;
+ public uint pid;
+}
diff --git a/PasteIntoFile.ShellExtension/CopyCommand.cs b/PasteIntoFile.ShellExtension/CopyCommand.cs
new file mode 100644
index 0000000..91fbd6c
--- /dev/null
+++ b/PasteIntoFile.ShellExtension/CopyCommand.cs
@@ -0,0 +1,33 @@
+using System.Runtime.InteropServices;
+using PasteIntoFile.Core.Properties;
+
+namespace PasteIntoFileShellExtension;
+
+[ComVisible(true)]
+[Guid(Clsids.CopyCommand)]
+[ProgId("PasteIntoFile.ShellExtension.CopyCommand")]
+[ClassInterface(ClassInterfaceType.None)]
+[ComDefaultInterface(typeof(IExplorerCommand))]
+public sealed class CopyCommand : BaseExplorerCommand {
+
+ public override string Clsid => Clsids.CopyCommand;
+
+ public override int GetTitle(IShellItemArray psiItemArray, out IntPtr ppszName) {
+ ppszName = Marshal.StringToCoTaskMemUni(Resources.str_contextentry_copyfromfile);
+ return HResult.S_OK;
+ }
+
+ public override int GetState(IShellItemArray psiItemArray, bool fOkToBeSlow, out EXPCMDSTATE pCmdState) {
+ // Show if exactly one item is selected
+ if (psiItemArray != null && psiItemArray.GetCount(out var count) == HResult.S_OK && count == 1) {
+ pCmdState = EXPCMDSTATE.ECS_ENABLED;
+ } else {
+ pCmdState = EXPCMDSTATE.ECS_HIDDEN;
+ }
+ return HResult.S_OK;
+ }
+
+ public override int Invoke(IShellItemArray psiItemArray, IntPtr pbc) {
+ return InvokePasteIntoFile(psiItemArray, PasteMode.Copy);
+ }
+}
diff --git a/PasteIntoFile.ShellExtension/ExplorerCommandEnumerator.cs b/PasteIntoFile.ShellExtension/ExplorerCommandEnumerator.cs
new file mode 100644
index 0000000..bc87eae
--- /dev/null
+++ b/PasteIntoFile.ShellExtension/ExplorerCommandEnumerator.cs
@@ -0,0 +1,60 @@
+using System.Collections;
+using System.Runtime.InteropServices;
+
+namespace PasteIntoFileShellExtension;
+
+///
+/// COM-visible enumerator for IExplorerCommand subcommands.
+/// Wraps a list of IExplorerCommand instances and exposes them via IEnumExplorerCommand.
+///
+[ComVisible(true)]
+[Guid("B2E4F8A1-3D5C-4B9E-A7F1-9C8D6E5A4B3C")]
+[ClassInterface(ClassInterfaceType.None)]
+[ComDefaultInterface(typeof(IEnumExplorerCommand))]
+public class ExplorerCommandEnumerator : IEnumExplorerCommand {
+ private readonly IEnumerator _enumerator;
+ private IExplorerCommand? _current;
+
+ public ExplorerCommandEnumerator(IList commands) {
+ _enumerator = commands.GetEnumerator();
+ }
+
+ public int Next(uint celt, out IExplorerCommand rgelt, out uint pceltFetched) {
+ rgelt = null!;
+ pceltFetched = 0;
+
+ if (celt == 0) {
+ return HResult.S_OK;
+ }
+
+ if (_enumerator.MoveNext()) {
+ _current = (IExplorerCommand)_enumerator.Current;
+ rgelt = _current;
+ pceltFetched = 1;
+ return HResult.S_OK;
+ }
+
+ return HResult.S_FALSE;
+ }
+
+ public int Skip(uint celt) {
+ for (uint i = 0; i < celt; i++) {
+ if (!_enumerator.MoveNext()) {
+ return HResult.S_FALSE;
+ }
+ }
+ return HResult.S_OK;
+ }
+
+ public int Reset() {
+ _enumerator.Reset();
+ _current = null;
+ return HResult.S_OK;
+ }
+
+ public int Clone(out IEnumExplorerCommand ppEnum) {
+ // Clone is not supported; return E_NOTIMPL
+ ppEnum = null!;
+ return HResult.E_NOTIMPL;
+ }
+}
diff --git a/PasteIntoFile.ShellExtension/FileGroupCommand.cs b/PasteIntoFile.ShellExtension/FileGroupCommand.cs
new file mode 100644
index 0000000..46da7ae
--- /dev/null
+++ b/PasteIntoFile.ShellExtension/FileGroupCommand.cs
@@ -0,0 +1,61 @@
+using System.Runtime.InteropServices;
+using PasteIntoFile.Core.Properties;
+
+namespace PasteIntoFileShellExtension;
+
+// Shared COM class ID for the file group command
+static class FileGroupClsid {
+ public const string Value = "109EFE3F-786B-4DFA-B639-4B673C05B932";
+}
+
+///
+/// Parent group command for file context menus.
+/// Displays "Paste Into File" with a right-arrow submenu containing Copy, Replace, and Append.
+///
+[ComVisible(true)]
+[Guid(FileGroupClsid.Value)]
+[ProgId("PasteIntoFile.ShellExtension.FileGroupCommand")]
+[ClassInterface(ClassInterfaceType.None)]
+[ComDefaultInterface(typeof(IExplorerCommand))]
+public sealed class FileGroupCommand : BaseExplorerCommand {
+
+ public override string Clsid => FileGroupClsid.Value;
+
+ public override int GetTitle(IShellItemArray psiItemArray, out IntPtr ppszName) {
+ ppszName = Marshal.StringToCoTaskMemUni(Resources.str_contextentry);
+ return HResult.S_OK;
+ }
+
+ public override int GetState(IShellItemArray psiItemArray, bool fOkToBeSlow, out EXPCMDSTATE pCmdState) {
+ // Show the group if exactly one item is selected
+ if (psiItemArray != null && psiItemArray.GetCount(out var count) == HResult.S_OK && count == 1) {
+ pCmdState = EXPCMDSTATE.ECS_ENABLED;
+ } else {
+ pCmdState = EXPCMDSTATE.ECS_HIDDEN;
+ }
+ return HResult.S_OK;
+ }
+
+ public override int Invoke(IShellItemArray psiItemArray, IntPtr pbc) {
+ // The group itself is not invokable; only its subcommands are.
+ return HResult.E_NOTIMPL;
+ }
+
+ public override int GetFlags(out EXPCMDFLAGS pFlags) {
+ // Mark this command as having subcommands so Explorer shows the right-arrow indicator
+ pFlags = EXPCMDFLAGS.ECF_HASSUBCOMMANDS;
+ return HResult.S_OK;
+ }
+
+ public override int EnumSubCommands(out IEnumExplorerCommand ppEnum) {
+ // Return the subcommands in the desired order: Copy, Replace, Append
+ var subCommands = new List {
+ new ReplaceCommand(),
+ new AppendCommand(),
+ new CopyCommand()
+ };
+
+ ppEnum = new ExplorerCommandEnumerator(subCommands);
+ return HResult.S_OK;
+ }
+}
diff --git a/PasteIntoFile.ShellExtension/PasteCommand.cs b/PasteIntoFile.ShellExtension/PasteCommand.cs
new file mode 100644
index 0000000..f3b0c76
--- /dev/null
+++ b/PasteIntoFile.ShellExtension/PasteCommand.cs
@@ -0,0 +1,37 @@
+using System.Runtime.InteropServices;
+using PasteIntoFile.Core;
+using PasteIntoFile.Core.Properties;
+
+namespace PasteIntoFileShellExtension;
+
+[ComVisible(true)]
+[Guid(Clsids.PasteCommand)]
+[ProgId("PasteIntoFile.ShellExtension.Command")]
+[ClassInterface(ClassInterfaceType.None)]
+[ComDefaultInterface(typeof(IExplorerCommand))]
+public sealed class PasteCommand : BaseExplorerCommand {
+
+ public override string Clsid => Clsids.PasteCommand;
+
+ public override int GetTitle(IShellItemArray psiItemArray, out IntPtr ppszName) {
+ // Reload if the settings file has changed to reflect current autoSave mode
+ // while avoiding expensive file I/O on slow storage.
+ SettingsUtil.ReloadIfChanged(Settings.Default);
+
+ var title = Resources.str_contextentry;
+ if (!Settings.Default.autoSave) {
+ title += "…"; // opens a dialog
+ }
+ ppszName = Marshal.StringToCoTaskMemUni(title);
+ return HResult.S_OK;
+ }
+
+ public override int GetState(IShellItemArray psiItemArray, bool fOkToBeSlow, out EXPCMDSTATE pCmdState) {
+ pCmdState = EXPCMDSTATE.ECS_ENABLED;
+ return HResult.S_OK;
+ }
+
+ public override int Invoke(IShellItemArray psiItemArray, IntPtr pbc) {
+ return InvokePasteIntoFile(psiItemArray, PasteMode.Paste);
+ }
+}
diff --git a/PasteIntoFile.ShellExtension/PasteIntoFile.ShellExtension.csproj b/PasteIntoFile.ShellExtension/PasteIntoFile.ShellExtension.csproj
new file mode 100644
index 0000000..e8b9c62
--- /dev/null
+++ b/PasteIntoFile.ShellExtension/PasteIntoFile.ShellExtension.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net9.0-windows
+ enable
+ enable
+
+ true
+
+ x64
+ win-x64
+
+ true
+ PasteIntoFileShellExtension
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PasteIntoFile.ShellExtension/ReplaceCommand.cs b/PasteIntoFile.ShellExtension/ReplaceCommand.cs
new file mode 100644
index 0000000..e57a823
--- /dev/null
+++ b/PasteIntoFile.ShellExtension/ReplaceCommand.cs
@@ -0,0 +1,33 @@
+using System.Runtime.InteropServices;
+using PasteIntoFile.Core.Properties;
+
+namespace PasteIntoFileShellExtension;
+
+[ComVisible(true)]
+[Guid(Clsids.ReplaceCommand)]
+[ProgId("PasteIntoFile.ShellExtension.ReplaceCommand")]
+[ClassInterface(ClassInterfaceType.None)]
+[ComDefaultInterface(typeof(IExplorerCommand))]
+public sealed class ReplaceCommand : BaseExplorerCommand {
+
+ public override string Clsid => Clsids.ReplaceCommand;
+
+ public override int GetTitle(IShellItemArray psiItemArray, out IntPtr ppszName) {
+ ppszName = Marshal.StringToCoTaskMemUni(Resources.str_contextentry_replace);
+ return HResult.S_OK;
+ }
+
+ public override int GetState(IShellItemArray psiItemArray, bool fOkToBeSlow, out EXPCMDSTATE pCmdState) {
+ // Show if exactly one item is selected
+ if (psiItemArray != null && psiItemArray.GetCount(out var count) == HResult.S_OK && count == 1) {
+ pCmdState = EXPCMDSTATE.ECS_ENABLED;
+ } else {
+ pCmdState = EXPCMDSTATE.ECS_HIDDEN;
+ }
+ return HResult.S_OK;
+ }
+
+ public override int Invoke(IShellItemArray psiItemArray, IntPtr pbc) {
+ return InvokePasteIntoFile(psiItemArray, PasteMode.Replace);
+ }
+}
diff --git a/PasteIntoFile.sln b/PasteIntoFile.sln
index 07216d8..97b0c3e 100644
--- a/PasteIntoFile.sln
+++ b/PasteIntoFile.sln
@@ -3,7 +3,18 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28729.10
MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PasteIntoFile.Core", "PasteIntoFile.Core\PasteIntoFile.Core.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PasteIntoFile", "PasteIntoFile\PasteIntoFile.csproj", "{F6F4215C-6CD7-4279-9C4C-C2DA9A823D0C}"
+ ProjectSection(ProjectDependencies) = postProject
+ {3FF1060D-CD0D-43BE-8E4F-61D2C0475AF2} = {3FF1060D-CD0D-43BE-8E4F-61D2C0475AF2}
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PasteIntoFile.ShellExtension", "PasteIntoFile.ShellExtension\PasteIntoFile.ShellExtension.csproj", "{3FF1060D-CD0D-43BE-8E4F-61D2C0475AF2}"
+ ProjectSection(ProjectDependencies) = postProject
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
+ EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,10 +22,18 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{F6F4215C-6CD7-4279-9C4C-C2DA9A823D0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F6F4215C-6CD7-4279-9C4C-C2DA9A823D0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F6F4215C-6CD7-4279-9C4C-C2DA9A823D0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F6F4215C-6CD7-4279-9C4C-C2DA9A823D0C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3FF1060D-CD0D-43BE-8E4F-61D2C0475AF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3FF1060D-CD0D-43BE-8E4F-61D2C0475AF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3FF1060D-CD0D-43BE-8E4F-61D2C0475AF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3FF1060D-CD0D-43BE-8E4F-61D2C0475AF2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/PasteIntoFile/App.config b/PasteIntoFile/App.config
index caa4704..fcc0bcf 100644
--- a/PasteIntoFile/App.config
+++ b/PasteIntoFile/App.config
@@ -5,12 +5,6 @@
-
-
-
-
-
-
@@ -69,4 +63,4 @@
-
\ No newline at end of file
+
diff --git a/PasteIntoFile/ClipboardContents.cs b/PasteIntoFile/ClipboardContents.cs
index 33c1c2d..1e8271a 100644
--- a/PasteIntoFile/ClipboardContents.cs
+++ b/PasteIntoFile/ClipboardContents.cs
@@ -14,10 +14,9 @@
using System.Text.RegularExpressions;
using System.Windows.Forms;
using Ical.Net.DataTypes;
-using LINQtoCSV;
+using Csv;
+using PasteIntoFile.Core;
using PasteIntoFile.Properties;
-using PdfSharp.Drawing;
-using PdfSharp.Pdf;
namespace PasteIntoFile {
@@ -173,7 +172,7 @@ public abstract class ImageLikeContent : BaseContent {
public class ImageContent : ImageLikeContent {
- public static readonly string[] EXTENSIONS = { "png", "bmp", "gif", "jpg", "pdf", "tif", "ico" };
+ public static readonly string[] EXTENSIONS = { "png", "bmp", "gif", "jpg", "tif", "ico" };
public ImageContent(Image image) {
Data = image;
}
@@ -188,24 +187,6 @@ public override void SaveAs(string path, string extension, bool append = false)
throw new FormatException(string.Format(Resources.str_error_cliboard_format_missmatch, extension));
switch (NormalizeExtension(extension)) {
- case "pdf":
- // convert image to ximage
- var stream = new MemoryStream();
- image.Save(stream, image.RawFormat);
- stream.Position = 0;
- XImage img = XImage.FromStream(stream);
- // create pdf document
- PdfDocument document = new PdfDocument();
- document.Info.Creator = Resources.app_title;
- PdfPage page = document.AddPage();
- page.Width = XUnit.FromPoint(img.PointWidth);
- page.Height = XUnit.FromPoint(img.PointHeight);
- // insert image and save
- XGraphics gfx = XGraphics.FromPdfPage(page);
- gfx.DrawImage(img, 0, 0);
- document.Save(path);
- return;
-
case "ico":
using (var fs = new FileStream(path, FileMode.Create)) {
ImageAsIcon.Save(fs);
@@ -227,7 +208,6 @@ public override PreviewHolder Preview(string extension) {
extension = NormalizeExtension(extension);
// Special formats with intermediate conversion types
switch (extension) {
- case "pdf": extension = "png"; break;
case "ico": return PreviewHolder.ForImage(ImageAsIcon.ToBitmap());
}
// Find suitable codec and convert image
@@ -279,7 +259,7 @@ public Icon ImageAsIcon {
/// Like ImageContent, but only for formats which support alpha channel
///
public class TransparentImageContent : ImageContent {
- public static new readonly string[] EXTENSIONS = { "png", "gif", "pdf", "tif", "ico" };
+ public static new readonly string[] EXTENSIONS = { "png", "gif", "tif", "ico" };
public TransparentImageContent(Image image) : base(image) { }
public override string[] Extensions => EXTENSIONS; // Note: gif has only alpha 100% or 0%
}
@@ -535,19 +515,21 @@ public char Delimiter {
}
}
- public class Row : List, IDataRow { }
-
///
/// Parse the CVS data
///
/// List of rows
- public IEnumerable Parse() {
- var context = new CsvContext();
- var readConfig = new CsvFileDescription();
- readConfig.FirstLineHasColumnNames = false;
- readConfig.SeparatorChar = Delimiter;
- var readStream = new StreamReader(new MemoryStream(readConfig.TextEncoding.GetBytes(Text)));
- return context.Read(readStream, readConfig);
+ public IEnumerable Parse() {
+ var options = new CsvOptions {
+ HeaderMode = HeaderMode.HeaderAbsent,
+ AllowNewLineInEnclosedFieldValues = true,
+ AllowBackSlashToEscapeQuote = true,
+ };
+ return CsvReader.ReadFromText(Text, options).Select(o => o.Values);
+ }
+
+ public string WithDelimeter(char delimeter) {
+ return CsvWriter.WriteToText(Parse(), delimeter);
}
///
@@ -559,9 +541,9 @@ private string AsMarkdown() {
var markdown = "";
foreach (var row in Parse()) {
- ncol = Math.Max(ncol, row.Count);
+ ncol = Math.Max(ncol, row.Length);
foreach (var item in row) {
- markdown += "|" + (item.Value ?? "").PadRight(10);
+ markdown += "|" + (item ?? "").PadRight(10);
}
markdown += "|\n";
}
@@ -576,6 +558,10 @@ public override string TextFor(string extension) {
switch (NormalizeExtension(extension)) {
case "md":
return AsMarkdown();
+ case "tsv":
+ case "tab":
+ return WithDelimeter('\t');
+ case "csv":
default:
return base.TextFor(extension);
}
@@ -632,7 +618,7 @@ public override PreviewHolder Preview(string extension) {
+ "\n