From 867dc37228d768dc87ed3e2e6ff26954705bfb41 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Mon, 1 Jun 2026 12:05:54 +0200 Subject: [PATCH] refactor(installer): extract ShortcutFactory COM helper Co-Authored-By: Claude Sonnet 4.6 --- .../Core/ShortcutFactory.cs | 49 ++++++++++++++++++ .../Steps/CreateShortcutsStep.cs | 50 +------------------ .../ShortcutFactoryTests.cs | 25 ++++++++++ 3 files changed, 76 insertions(+), 48 deletions(-) create mode 100644 src/ClaudeDo.Installer/Core/ShortcutFactory.cs create mode 100644 tests/ClaudeDo.Installer.Tests/ShortcutFactoryTests.cs diff --git a/src/ClaudeDo.Installer/Core/ShortcutFactory.cs b/src/ClaudeDo.Installer/Core/ShortcutFactory.cs new file mode 100644 index 0000000..dedfde3 --- /dev/null +++ b/src/ClaudeDo.Installer/Core/ShortcutFactory.cs @@ -0,0 +1,49 @@ +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Text; + +namespace ClaudeDo.Installer.Core; + +public static class ShortcutFactory +{ + public static void CreateShortcut(string shortcutPath, string targetPath, string workingDir, string description) + { + var link = (IShellLink)new ShellLink(); + link.SetPath(targetPath); + link.SetWorkingDirectory(workingDir); + link.SetDescription(description); + link.SetIconLocation(targetPath, 0); + + var file = (IPersistFile)link; + file.Save(shortcutPath, false); + } + + [ComImport] + [Guid("00021401-0000-0000-C000-000000000046")] + private class ShellLink { } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("000214F9-0000-0000-C000-000000000046")] + private interface IShellLink + { + void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, IntPtr pfd, int fFlags); + void GetIDList(out IntPtr ppidl); + void SetIDList(IntPtr pidl); + void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName); + void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); + void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath); + void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); + void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath); + void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); + void GetHotkey(out short pwHotkey); + void SetHotkey(short wHotkey); + void GetShowCmd(out int piShowCmd); + void SetShowCmd(int iShowCmd); + void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon); + void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); + void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved); + void Resolve(IntPtr hwnd, int fFlags); + void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile); + } +} diff --git a/src/ClaudeDo.Installer/Steps/CreateShortcutsStep.cs b/src/ClaudeDo.Installer/Steps/CreateShortcutsStep.cs index 86774cd..78682af 100644 --- a/src/ClaudeDo.Installer/Steps/CreateShortcutsStep.cs +++ b/src/ClaudeDo.Installer/Steps/CreateShortcutsStep.cs @@ -1,7 +1,4 @@ using System.IO; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; -using System.Text; using ClaudeDo.Installer.Core; namespace ClaudeDo.Installer.Steps; @@ -23,7 +20,7 @@ public sealed class CreateShortcutsStep : IInstallStep "Programs"); Directory.CreateDirectory(startMenuDir); var startMenuPath = Path.Combine(startMenuDir, "ClaudeDo.lnk"); - CreateShortcut(startMenuPath, appExe, workingDir, "ClaudeDo Task Manager"); + ShortcutFactory.CreateShortcut(startMenuPath, appExe, workingDir, "ClaudeDo Task Manager"); progress.Report($"Created Start Menu shortcut: {startMenuPath}"); // Desktop shortcut (optional) @@ -32,7 +29,7 @@ public sealed class CreateShortcutsStep : IInstallStep var desktopPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), "ClaudeDo.lnk"); - CreateShortcut(desktopPath, appExe, workingDir, "ClaudeDo Task Manager"); + ShortcutFactory.CreateShortcut(desktopPath, appExe, workingDir, "ClaudeDo Task Manager"); progress.Report($"Created Desktop shortcut: {desktopPath}"); } @@ -44,48 +41,5 @@ public sealed class CreateShortcutsStep : IInstallStep } } - private static void CreateShortcut(string shortcutPath, string targetPath, string workingDir, string description) - { - var link = (IShellLink)new ShellLink(); - link.SetPath(targetPath); - link.SetWorkingDirectory(workingDir); - link.SetDescription(description); - link.SetIconLocation(targetPath, 0); - var file = (IPersistFile)link; - file.Save(shortcutPath, false); - } - - #region COM Interop for IShellLink - - [ComImport] - [Guid("00021401-0000-0000-C000-000000000046")] - private class ShellLink { } - - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("000214F9-0000-0000-C000-000000000046")] - private interface IShellLink - { - void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, IntPtr pfd, int fFlags); - void GetIDList(out IntPtr ppidl); - void SetIDList(IntPtr pidl); - void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName); - void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); - void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath); - void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); - void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath); - void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); - void GetHotkey(out short pwHotkey); - void SetHotkey(short wHotkey); - void GetShowCmd(out int piShowCmd); - void SetShowCmd(int iShowCmd); - void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon); - void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); - void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved); - void Resolve(IntPtr hwnd, int fFlags); - void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile); - } - - #endregion } diff --git a/tests/ClaudeDo.Installer.Tests/ShortcutFactoryTests.cs b/tests/ClaudeDo.Installer.Tests/ShortcutFactoryTests.cs new file mode 100644 index 0000000..ffe6e4f --- /dev/null +++ b/tests/ClaudeDo.Installer.Tests/ShortcutFactoryTests.cs @@ -0,0 +1,25 @@ +using System.IO; +using ClaudeDo.Installer.Core; + +namespace ClaudeDo.Installer.Tests; + +public class ShortcutFactoryTests +{ + [Fact] + public void CreateShortcut_writes_lnk_file() + { + var dir = Path.Combine(Path.GetTempPath(), "cdshortcut-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + try + { + var target = Path.Combine(dir, "fake.exe"); + File.WriteAllText(target, ""); + var lnk = Path.Combine(dir, "x.lnk"); + + ShortcutFactory.CreateShortcut(lnk, target, dir, "desc"); + + Assert.True(File.Exists(lnk)); + } + finally { Directory.Delete(dir, recursive: true); } + } +}