diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..9d36e22 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(git tag v1.1.1.15 fd3dc4312b4e1d6e6dc6998f9dcf15a35f006157)", + "Bash(git cliff --config cliff.toml --output CHANGELOG_NEW.md)", + "Bash(env)", + "Bash(if not exist .signpath mkdir .signpath)", + "Bash(powershell -Command:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..641ffbf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI + +on: + pull_request: + branches: [master] + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Build + run: dotnet build Symlinker/Symlinker.csproj --configuration Release diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..459bb7f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,131 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Symlinker is a Windows Forms application (.NET 8) that provides a GUI for creating symbolic links, hard links, and directory junctions on Windows. It wraps the Windows `mklink` command with an easy-to-use interface. + +**Target Platform:** Windows 10+ only (as of 2025 modernization) + +## Build & Development Commands + +### Building the Application + +```powershell +# Build in Debug configuration +dotnet build "Symlinker/Symlinker.csproj" --configuration Debug + +# Build in Release configuration +dotnet build "Symlinker/Symlinker.csproj" --configuration Release +``` + +### Publishing (ClickOnce) + +The application uses ClickOnce deployment and is published to GitHub Pages: + +```powershell +# Full build and publish to gh-pages branch +./release.ps1 + +# Build only (skip deployment) +./release.ps1 -OnlyBuild +``` + +The `release.ps1` script: +- Reads the Git tag to determine version (e.g., `v1.2.3` becomes `1.2.3.0`) +- Uses MSBuild to publish with ClickOnce profile +- Deploys to `gh-pages` branch for auto-update support + +### Running the Application + +```powershell +dotnet run --project "Symlinker/Symlinker.csproj" +``` + +## Architecture + +### Key Components + +1. **Program.cs**: Entry point with UAC elevation logic + - Automatically re-launches itself with administrator privileges using the `--engage` flag + - Uses Windows API (`BCM_SETSHIELD`) to display UAC shield icon + +2. **MainWindow.cs**: Main form and business logic + - Handles UI state for file vs. folder symlink modes + - Manages three link types: Symbolic Link, Hard Link, Directory Junction + - Executes `cmd.exe /c mklink` with appropriate parameters + - Supports drag-and-drop for file/folder paths + +3. **MainWindow.Designer.cs**: Auto-generated Windows Forms designer code + - Contains all UI control initialization + +### Link Creation Flow + +1. User selects link type (file/folder) via `typeSelectorComboBox` +2. `Switcher()` method updates UI labels and available link types +3. User inputs link location, link name, and destination +4. `CreateLink()` validates paths and checks for conflicts +5. `SendCommand()` builds and executes mklink command with proper escaping +6. Process output/error handlers display results via MessageBox + +### Important Patterns + +- **Command Building**: Uses `string.Format()` with `CultureInfo.InvariantCulture` for mklink command construction +- **Path Handling**: Wraps paths in quotes to support spaces +- **Link Type Mapping**: + - Symbolic Link: `/D` (directory) or empty (file) + - Hard Link: `/H` + - Directory Junction: `/J` (folders only) + +### Dependencies + +Managed via Central Package Management ([Directory.Packages.props](Directory.Packages.props)): + +- **Microsoft-WindowsAPICodePack-Core** (1.1.5): Common file dialogs +- **Microsoft-WindowsAPICodePack-Shell** (1.1.5): Shell integration + +## Project Structure + +``` +Symlinker/ +├── Symlinker/ # Main project directory +│ ├── MainWindow.cs # Core form logic +│ ├── MainWindow.Designer.cs # UI designer code +│ ├── MainWindow.resx # Form resources +│ ├── Program.cs # Entry point with UAC elevation +│ ├── Properties/ +│ │ ├── Resources.Designer.cs # Localized strings +│ │ └── Settings.Designer.cs # App settings +│ ├── icon.ico # Application icon +│ └── Symlinker.csproj +├── Symlinker.sln # Solution file +├── Directory.Packages.props # Central package versions +├── release.ps1 # ClickOnce build/publish script +└── .github/workflows/ + └── release.yml # GitHub Actions for tagged releases +``` + +## Release Process + +Releases are automated via GitHub Actions: + +1. Push a Git tag: `git tag v1.2.3 && git push origin v1.2.3` +2. GitHub Actions workflow ([.github/workflows/release.yml](.github/workflows/release.yml)) triggers +3. `release.ps1` executes on Windows runner +4. Application published to `gh-pages` branch +5. ClickOnce installer auto-updates existing installations + +## Known Limitations + +- **Drag & Drop**: Does not work when running as administrator (Windows security limitation) +- **UAC Required**: Creating symlinks requires administrator privileges on Windows +- **Platform**: Windows 10/11 only (uses .NET 8 Windows-specific APIs) + +## Localization + +UI strings are managed through resource files: +- [Symlinker/Properties/Resources.Designer.cs](Symlinker/Properties/Resources.Designer.cs) +- [Symlinker/MainWindow.resx](Symlinker/MainWindow.resx) + +When modifying UI text, update the resource files rather than hardcoding strings. diff --git a/README.md b/README.md index 5e3fb99..def1957 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ This project has been around since 2009, originally hosted on Google Code and bu ## Featured On +- [Softpedia](https://www.softpedia.com/get/PORTABLE-SOFTWARE/System/System-Enhancements/Portable-Symbolic-Link-Creator.shtml) - [addictivetips](http://www.addictivetips.com/windows-tips/symlinker-create-symlink-hardlink-and-directory-junction-in-windows/) - [TecFlap](https://web.archive.org/web/20150511235232/http://www.tecflap.com/2012/05/29/software-day-winautohide-symlinker-hyperdesktop/) (Archived) - [Zhacks](https://web.archive.org/web/20170512070430/http://www.zhacks.com/easily-create-symbolic-link-with-mklink-gui-symlinker) (Archived) @@ -68,16 +69,16 @@ Previous project link: https://code.google.com/p/symlinker/ See [CHANGELOG.md](CHANGELOG.md) -## Acknowledgments - -Free code signing provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/) - -Committers and reviewers: [Contributors](https://github.com/amd989/Symlinker/graphs/contributors) - ## License MIT — see [LICENSE](LICENSE) ## Privacy Policy -See [PRIVACY.md](PRIVACY.md) \ No newline at end of file +See [PRIVACY.md](PRIVACY.md) + +## Acknowledgments + +Free code signing provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/) + +Committers and reviewers: [Contributors](https://github.com/amd989/Symlinker/graphs/contributors) diff --git a/Symlinker/MainWindow.xaml b/Symlinker/MainWindow.xaml index 969be73..d8bbafb 100644 --- a/Symlinker/MainWindow.xaml +++ b/Symlinker/MainWindow.xaml @@ -1,227 +1,482 @@ - - - - - - - - - + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls" + xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks" + xmlns:p="clr-namespace:Symlinker.Properties" + Title="Symlinker" + Width="560" Height="280" + ResizeMode="NoResize" + WindowStartupLocation="CenterScreen" + Icon="icon.ico" + TitleCharacterCasing="Normal" + BorderThickness="0" + GlowBrush="Transparent" + NonActiveGlowBrush="Transparent" + TitleAlignment="Center" + ShowIconOnTitleBar="False" + TitleBarHeight="32" + WindowTitleBrush="{DynamicResource MahApps.Brushes.Gray10}" + NonActiveWindowTitleBrush="{DynamicResource MahApps.Brushes.Gray10}" + TitleForeground="{DynamicResource MahApps.Brushes.ThemeForeground}" + OverrideDefaultWindowCommandsBrush="{DynamicResource MahApps.Brushes.ThemeForeground}" + Background="{DynamicResource MahApps.Brushes.Control.Background}" + UseLayoutRounding="True" + SnapsToDevicePixels="True" + TextOptions.TextFormattingMode="Display" + TextOptions.TextRenderingMode="ClearType"> - 4 + 10 - - - - - - - - - - - + - + + - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - + HorizontalAlignment="Center" VerticalAlignment="Center" + FontFamily="Segoe UI"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + - - - + + - + + + + + + + + + + + + + + + - + + + - - + + + - - - - + + + + + diff --git a/Symlinker/MainWindow.xaml.cs b/Symlinker/MainWindow.xaml.cs index 3463fd2..c5850b5 100644 --- a/Symlinker/MainWindow.xaml.cs +++ b/Symlinker/MainWindow.xaml.cs @@ -3,15 +3,19 @@ namespace Symlinker using System; using System.Diagnostics; using System.Globalization; + using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; + using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Interop; using MahApps.Metro.Controls; + using MahApps.Metro.Controls.Dialogs; + using MahApps.Metro.IconPacks; using Res = Symlinker.Properties.Resources; @@ -24,197 +28,184 @@ public partial class MainWindow : MetroWindow [DllImport("dwmapi.dll")] private static extern int DwmSetWindowAttribute(nint hwnd, int attr, ref int attrValue, int attrSize); - private bool isFolder; + private const string PlaceholderBrushKey = "MahApps.Brushes.Gray3"; + private const string PrimaryTextBrushKey = "MahApps.Brushes.ThemeForeground"; + + private bool isFolder = true; + + private static readonly MetroDialogSettings FastDialog = new() + { + AnimateShow = false, + AnimateHide = false, + }; public MainWindow() { InitializeComponent(); - - linkTypeComboBox.SelectedIndex = 0; - typeSelectorComboBox.SelectedIndex = 0; - Loaded += OnLoaded; } private void OnLoaded(object sender, RoutedEventArgs e) { - // Request Windows 11 rounded corners for this window var hwnd = new WindowInteropHelper(this).Handle; var preference = DWMWCP_ROUND; DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, ref preference, sizeof(int)); + + UpdateMode(); } - private void TypeSelectorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + private void TypeRadio_Checked(object sender, RoutedEventArgs e) { - Switcher(); + UpdateMode(); } - private void Switcher() + private void UpdateMode() { - if (groupBox1Header == null || groupBox2Header == null) - return; + if (folderTypeRadio == null || junctionRadio == null || symLinkRadio == null || hardLinkRadio == null || sourceIcon == null) return; - if (typeSelectorComboBox.SelectedIndex == 0) - { - groupBox1Header.Text = Res.MainWindow_Switcher_Link_Folder; - groupBox2Header.Text = Res.MainWindow_Switcher_Destination_Folder; - label2.Text = Res.MainWindow_Switcher_Now_give_a_name_to_the_link_; - label3.Text = Res.MainWindow_Switcher_Please_select_the_path_to_the_real_folder_you_want_to_link_; - isFolder = true; - - // Add Directory Junction if not present - bool hasJunction = false; - foreach (ComboBoxItem item in linkTypeComboBox.Items) - { - if (item.Content as string == "Directory Junction") - { - hasJunction = true; - break; - } - } - if (!hasJunction) - linkTypeComboBox.Items.Add(new ComboBoxItem { Content = "Directory Junction" }); + isFolder = folderTypeRadio.IsChecked == true; - linkTypeComboBox.ToolTip = Res.TooltipLinkTypeFolderDescription; - } - else - { - groupBox1Header.Text = Res.MainWindow_Switcher_Link_File; - groupBox2Header.Text = Res.MainWindow_Switcher_Destination_File; - label2.Text = Res.MainWindow_Switcher_Now_give_a_name_to_your_file_; - label3.Text = Res.MainWindow_Switcher_Please_select_the_path_to_the_real_file_you_want_to_link_; - isFolder = false; - - // Remove Directory Junction for file mode - ComboBoxItem junctionItem = null; - foreach (ComboBoxItem item in linkTypeComboBox.Items) - { - if (item.Content as string == "Directory Junction") - { - junctionItem = item; - break; - } - } - if (junctionItem != null) - { - if (linkTypeComboBox.SelectedIndex == 2) - linkTypeComboBox.SelectedIndex = 0; - linkTypeComboBox.Items.Remove(junctionItem); - } + junctionRadio.Visibility = isFolder ? Visibility.Visible : Visibility.Collapsed; + hardLinkRadio.Visibility = isFolder ? Visibility.Collapsed : Visibility.Visible; - linkTypeComboBox.ToolTip = Res.TooltipLinkTypeFileDescription; + if (!isFolder && junctionRadio.IsChecked == true) + symLinkRadio.IsChecked = true; + + if (isFolder && hardLinkRadio.IsChecked == true) + symLinkRadio.IsChecked = true; + + sourceIcon.Kind = isFolder + ? PackIconMaterialKind.FolderOutline + : PackIconMaterialKind.FileOutline; + + var linkTypeTooltip = isFolder ? Res.TooltipLinkTypeFolderDescription : Res.TooltipLinkTypeFileDescription; + symLinkRadio.ToolTip = linkTypeTooltip; + hardLinkRadio.ToolTip = linkTypeTooltip; + junctionRadio.ToolTip = linkTypeTooltip; + + if (destinationLocationTextBox != null) + destinationLocationTextBox.Text = string.Empty; + + if (destinationFolderName != null) + { + destinationFolderName.Text = isFolder ? Res.PlaceholderTargetFolder : Res.PlaceholderTargetFile; + destinationFolderName.SetResourceReference(ForegroundProperty, PlaceholderBrushKey); } + } - typeSelectorComboBox.ToolTip = Res.TooltipTypeSelectorDescription; + private string GetLinkTypeFlag() + { + if (hardLinkRadio.IsChecked == true) return "/H "; + if (junctionRadio.IsChecked == true) return "/J "; + return string.Empty; } - private string ComboBoxSelection() + private void DestinationPath_TextChanged(object sender, TextChangedEventArgs e) { - switch (linkTypeComboBox.SelectedIndex) + var path = destinationLocationTextBox.Text; + if (string.IsNullOrEmpty(path)) { - case 1: - return "/H "; - case 2: - return "/J "; - default: - return string.Empty; + destinationFolderName.Text = isFolder ? Res.PlaceholderTargetFolder : Res.PlaceholderTargetFile; + destinationFolderName.SetResourceReference(ForegroundProperty, PlaceholderBrushKey); + return; } + + var trimmed = path.TrimEnd('\\', '/'); + var lastSep = trimmed.LastIndexOfAny(new[] { '\\', '/' }); + destinationFolderName.Text = lastSep >= 0 ? trimmed[(lastSep + 1)..] : trimmed; + destinationFolderName.SetResourceReference(ForegroundProperty, PrimaryTextBrushKey); } - private void CreateLink() + private async Task CreateLink() { try { - if (linkLocationTextBox.Text != string.Empty && linkNameTextBox.Text != string.Empty && destinationLocationTextBox.Text != string.Empty) + var linkLocation = linkLocationTextBox.Text.Trim(); + var linkName = linkNameTextBox.Text.Trim(); + var destination = destinationLocationTextBox.Text.Trim(); + + if (string.IsNullOrWhiteSpace(linkLocation) || string.IsNullOrWhiteSpace(linkName) || string.IsNullOrWhiteSpace(destination)) { - if (isFolder && Directory.Exists(linkLocationTextBox.Text) && Directory.Exists(destinationLocationTextBox.Text)) - { - var link = string.Format( - "\"{0}\\{1}\" ", linkLocationTextBox.Text, linkNameTextBox.Text); + await this.ShowMessageAsync(Res.MessageBoxErrorTitle, Res.FillBlanks, settings: FastDialog); + return; + } - var directories = Directory.GetDirectories(linkLocationTextBox.Text); + if (linkName.Contains('"') || linkLocation.Contains('"') || destination.Contains('"')) + { + await this.ShowMessageAsync(Res.MessageBoxErrorTitle, Res.InvalidQuoteInPath, settings: FastDialog); + return; + } - if (directories.Any(e => e.Split('\\').Last().Equals(linkNameTextBox.Text))) - { - var answer = MessageBox.Show( - Res.DialogFolderExists, - Res.DialogFolderExistsDialog, - MessageBoxButton.YesNo, - MessageBoxImage.Warning); - if (answer == MessageBoxResult.Yes) - { - var dir2Delete = directories.First(e => e.Split('\\').Last().Equals(linkNameTextBox.Text)); - Directory.Delete(dir2Delete); - SendCommand(link); - return; - } - - MessageBox.Show( - Res.LinkCreationAborted, - Res.LinkCreationAbortedWarning, - MessageBoxButton.OK, - MessageBoxImage.Stop); - } - else + var link = $"\"{linkLocation}\\{linkName}\""; + + if (isFolder && Directory.Exists(linkLocation) && Directory.Exists(destination)) + { + var directories = Directory.GetDirectories(linkLocation); + + if (directories.Any(e => Path.GetFileName(e).Equals(linkName, StringComparison.OrdinalIgnoreCase))) + { + var answer = await this.ShowMessageAsync( + Res.DialogFolderExistsDialog, + Res.DialogFolderExists, + MessageDialogStyle.AffirmativeAndNegative, + FastDialog); + if (answer == MessageDialogResult.Affirmative) { - SendCommand(link); + var dir2Delete = directories.First(e => Path.GetFileName(e).Equals(linkName, StringComparison.OrdinalIgnoreCase)); + Directory.Delete(dir2Delete, true); + await SendCommand(link, destination); + return; } + + await this.ShowMessageAsync(Res.LinkCreationAbortedWarning, Res.LinkCreationAborted, settings: FastDialog); } - else if (Directory.Exists(linkLocationTextBox.Text) && File.Exists(destinationLocationTextBox.Text)) + else { - var link = string.Format( - "\"{0}\\{1}\" ", linkLocationTextBox.Text, linkNameTextBox.Text); - - var files = Directory.GetFiles(linkLocationTextBox.Text); - if (files.Any(e => e.Split('\\').Last().Equals(linkNameTextBox.Text))) - { - var answer = MessageBox.Show( - Res.DialogDeleteFile, - Res.DialogDeleteFileWarning, - MessageBoxButton.YesNo, - MessageBoxImage.Warning); - if (answer == MessageBoxResult.Yes) - { - var file2Delete = files.First(e => e.Split('\\').Last().Equals(linkNameTextBox.Text)); - File.Delete(file2Delete); - SendCommand(link); - return; - } - MessageBox.Show( - Res.LinkCreationAborted, - Res.LinkCreationAbortedWarning, - MessageBoxButton.OK, - MessageBoxImage.Stop); - } - else + await SendCommand(link, destination); + } + } + else if (!isFolder && Directory.Exists(linkLocation) && File.Exists(destination)) + { + var files = Directory.GetFiles(linkLocation); + if (files.Any(e => Path.GetFileName(e).Equals(linkName, StringComparison.OrdinalIgnoreCase))) + { + var answer = await this.ShowMessageAsync( + Res.DialogDeleteFileWarning, + Res.DialogDeleteFile, + MessageDialogStyle.AffirmativeAndNegative, + FastDialog); + if (answer == MessageDialogResult.Affirmative) { - SendCommand(link); + var file2Delete = files.First(e => Path.GetFileName(e).Equals(linkName, StringComparison.OrdinalIgnoreCase)); + File.Delete(file2Delete); + await SendCommand(link, destination); + return; } + await this.ShowMessageAsync(Res.LinkCreationAbortedWarning, Res.LinkCreationAborted, settings: FastDialog); } else { - MessageBox.Show(Res.FilesOrFolderNotExists, Res.MessageBoxErrorTitle, MessageBoxButton.OK, MessageBoxImage.Warning); + await SendCommand(link, destination); } } else { - MessageBox.Show(Res.FillBlanks, Res.MessageBoxErrorTitle, MessageBoxButton.OK, MessageBoxImage.Error); + await this.ShowMessageAsync(Res.MessageBoxErrorTitle, Res.FilesOrFolderNotExists, settings: FastDialog); } } catch (Exception exception) { - MessageBox.Show(Res.MessageBoxExceptionOcurred + exception.Message, Res.MessageBoxErrorTitle, MessageBoxButton.OK, MessageBoxImage.Error); + await this.ShowMessageAsync(Res.MessageBoxErrorTitle, Res.MessageBoxExceptionOcurred + "\n" + exception.Message, settings: FastDialog); } } - private void SendCommand(string link) + private async Task SendCommand(string link, string destination) { try { - var target = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", destinationLocationTextBox.Text); - var typeLink = ComboBoxSelection(); + var typeLink = GetLinkTypeFlag(); var directory = isFolder ? "/D " : string.Empty; - var stringCommand = string.Format(CultureInfo.InvariantCulture, "/c mklink {0}{1}{2}{3}", directory, typeLink, link, target); + var stringCommand = $"/c mklink {directory}{typeLink}{link} \"{destination}\""; var processStartInfo = new ProcessStartInfo { FileName = "cmd", @@ -225,36 +216,37 @@ private void SendCommand(string link) RedirectStandardOutput = true }; + var gotOutput = false; var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true }; process.ErrorDataReceived += Process_ErrorDataReceived; - process.OutputDataReceived += Process_OutputDataReceived; + process.OutputDataReceived += (s, ev) => + { + if (!string.IsNullOrEmpty(ev.Data)) + { + gotOutput = true; + Dispatcher.InvokeAsync(() => this.ShowMessageAsync(Res.MessageBoxSuccessTitle, ev.Data, settings: FastDialog)); + } + }; process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); - process.WaitForExit(); - process.Close(); + await process.WaitForExitAsync(); + + if (process.ExitCode == 0 && !gotOutput) + await this.ShowMessageAsync(Res.MessageBoxSuccessTitle, Res.LinkSuccessfullyCreated, settings: FastDialog); + process.Dispose(); } catch (Exception) { - MessageBox.Show(Res.CmdNotFound, Res.MessageBoxErrorTitle, MessageBoxButton.OK, MessageBoxImage.Error); - } - } - - private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e) - { - if (!string.IsNullOrEmpty(e.Data)) - { - MessageBox.Show(e.Data, Res.MessageBoxSuccessTitle, MessageBoxButton.OK, MessageBoxImage.Information); + await this.ShowMessageAsync(Res.MessageBoxErrorTitle, Res.CmdNotFound, settings: FastDialog); } } private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e) { if (!string.IsNullOrEmpty(e.Data)) - { - MessageBox.Show(e.Data, Res.MessageBoxErrorTitle, MessageBoxButton.OK, MessageBoxImage.Error); - } + Dispatcher.InvokeAsync(() => this.ShowMessageAsync(Res.MessageBoxErrorTitle, e.Data, settings: FastDialog)); } private void ExploreButton1_Click(object sender, RoutedEventArgs e) @@ -280,30 +272,33 @@ private void ExploreButton2_Click(object sender, RoutedEventArgs e) } } - private void CreateLink_Click(object sender, RoutedEventArgs e) - { - CreateLink(); - } - - private void AboutButton_Click(object sender, RoutedEventArgs e) + private async void CreateLink_Click(object sender, RoutedEventArgs e) { - string version; + createLinkButton.IsEnabled = false; try { - version = Environment.GetEnvironmentVariable("ClickOnce_CurrentVersion"); + await CreateLink(); } - catch + finally + { + createLinkButton.IsEnabled = true; + } + } + + private async void AboutButton_Click(object sender, RoutedEventArgs e) + { + var version = Environment.GetEnvironmentVariable("ClickOnce_CurrentVersion"); + if (string.IsNullOrEmpty(version)) { var assembly = Assembly.GetExecutingAssembly(); var fvi = FileVersionInfo.GetVersionInfo(assembly.Location); version = fvi.FileVersion; } - MessageBox.Show( - string.Format(CultureInfo.CurrentCulture, Res.AboutDescription, version), + await this.ShowMessageAsync( Res.MessageBoxAboutTitle, - MessageBoxButton.OK, - MessageBoxImage.Information); + string.Format(CultureInfo.CurrentUICulture, Res.AboutDescription, version, DateTime.Now.Year), + settings: FastDialog); } private void TextBox_DragOver(object sender, DragEventArgs e) @@ -318,17 +313,35 @@ private void TextBox_PreviewDragEnter(object sender, DragEventArgs e) e.Handled = true; } - private void TextBox_Drop(object sender, DragEventArgs e) + private void SourceCard_Drop(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) { var files = (string[])e.Data.GetData(DataFormats.FileDrop); if (files != null && files.Length != 0) { - var textBox = (TextBox)sender; - textBox.Text = files[0]; + var path = files[0]; + var droppedIsFolder = Directory.Exists(path); + if (droppedIsFolder != isFolder) + { + if (droppedIsFolder) + folderTypeRadio.IsChecked = true; + else + fileTypeRadio.IsChecked = true; + } + destinationLocationTextBox.Text = path; } } } + + private void LinkCard_Drop(object sender, DragEventArgs e) + { + if (e.Data.GetDataPresent(DataFormats.FileDrop)) + { + var files = (string[])e.Data.GetData(DataFormats.FileDrop); + if (files != null && files.Length != 0 && Directory.Exists(files[0])) + linkLocationTextBox.Text = files[0]; + } + } } } diff --git a/Symlinker/Properties/PublishProfiles/ClickOnceProfile.pubxml b/Symlinker/Properties/PublishProfiles/ClickOnceProfile.pubxml index 9a7d656..1eaedec 100644 --- a/Symlinker/Properties/PublishProfiles/ClickOnceProfile.pubxml +++ b/Symlinker/Properties/PublishProfiles/ClickOnceProfile.pubxml @@ -3,7 +3,7 @@ 0 - 3.0.0.0 + 4.0.0.0 True Release True diff --git a/Symlinker/Properties/Resources.Designer.cs b/Symlinker/Properties/Resources.Designer.cs index 7796723..516bf44 100644 --- a/Symlinker/Properties/Resources.Designer.cs +++ b/Symlinker/Properties/Resources.Designer.cs @@ -19,10 +19,10 @@ namespace Symlinker.Properties { // 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", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { + public class Resources { private static global::System.Resources.ResourceManager resourceMan; @@ -36,7 +36,7 @@ internal Resources() { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Symlinker.Properties.Resources", typeof(Resources).Assembly); @@ -51,7 +51,7 @@ internal Resources() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -61,263 +61,360 @@ internal Resources() { } /// - /// Looks up a localized string similar to © 2010-2025 Alejandro Mora + /// Looks up a localized string similar to © 2010-{1} Alejandro Mora ///Version: {0} ///e-mail: mail@alejandro.md /// ///Thanks to Microsoft for the use of their shortcut arrow :). /// - internal static string AboutDescription { + public static string AboutDescription { get { return ResourceManager.GetString("AboutDescription", resourceCulture); } } /// - /// Looks up a localized string similar to Cannot find the file needed to create links, the creation stopped. + /// Looks up a localized string similar to About. /// - internal static string CmdNotFound { + public static string ButtonAbout { + get { + return ResourceManager.GetString("ButtonAbout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add another. + /// + public static string ButtonAddAnother { + get { + return ResourceManager.GetString("ButtonAddAnother", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create link. + /// + public static string ButtonCreateLink { + get { + return ResourceManager.GetString("ButtonCreateLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LINK LOCATION. + /// + public static string CardHeaderLinkLocation { + get { + return ResourceManager.GetString("CardHeaderLinkLocation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TARGET. + /// + public static string CardHeaderTarget { + get { + return ResourceManager.GetString("CardHeaderTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot find the command-line tool needed to create links.. + /// + public static string CmdNotFound { get { return ResourceManager.GetString("CmdNotFound", resourceCulture); } } /// - /// Looks up a localized string similar to The link name you are using already exists in the selected directory, would you like to DELETE the file and then create a new link?. + /// Looks up a localized string similar to A file with this link name already exists in the selected directory. Would you like to delete it and create a new link?. /// - internal static string DialogDeleteFile { + public static string DialogDeleteFile { get { return ResourceManager.GetString("DialogDeleteFile", resourceCulture); } } /// - /// Looks up a localized string similar to File already there.... + /// Looks up a localized string similar to File already exists. /// - internal static string DialogDeleteFileWarning { + public static string DialogDeleteFileWarning { get { return ResourceManager.GetString("DialogDeleteFileWarning", resourceCulture); } } /// - /// Looks up a localized string similar to The link name you are using already exists in the selected directory, would you like to DELETE the folder and then create a new link?. + /// Looks up a localized string similar to A folder with this link name already exists in the selected directory. Would you like to delete it and create a new link?. /// - internal static string DialogFolderExists { + public static string DialogFolderExists { get { return ResourceManager.GetString("DialogFolderExists", resourceCulture); } } /// - /// Looks up a localized string similar to Folder already there.... + /// Looks up a localized string similar to Folder already exists. /// - internal static string DialogFolderExistsDialog { + public static string DialogFolderExistsDialog { get { return ResourceManager.GetString("DialogFolderExistsDialog", resourceCulture); } } /// - /// Looks up a localized string similar to One of the directories/files does not exists, please provide valid directories/files. + /// Looks up a localized string similar to One of the specified directories or files does not exist. Please provide valid paths.. /// - internal static string FilesOrFolderNotExists { + public static string FilesOrFolderNotExists { get { return ResourceManager.GetString("FilesOrFolderNotExists", resourceCulture); } } + + /// + /// Looks up a localized string similar to Paths cannot contain quote characters. Please remove any quotes and try again.. + /// + public static string InvalidQuoteInPath { + get { + return ResourceManager.GetString("InvalidQuoteInPath", resourceCulture); + } + } /// - /// Looks up a localized string similar to Please fill all the blanks spaces with the indicated info. + /// Looks up a localized string similar to Please fill in all the required fields.. /// - internal static string FillBlanks { + public static string FillBlanks { get { return ResourceManager.GetString("FillBlanks", resourceCulture); } } - /// - /// Looks up a localized string similar to Link creation aborted. + /// Looks up a localized string similar to Link creation was aborted.. /// - internal static string LinkCreationAborted { + public static string LinkCreationAborted { get { return ResourceManager.GetString("LinkCreationAborted", resourceCulture); } } /// - /// Looks up a localized string similar to Aborted Operation. + /// Looks up a localized string similar to Operation aborted. /// - internal static string LinkCreationAbortedWarning { + public static string LinkCreationAbortedWarning { get { return ResourceManager.GetString("LinkCreationAbortedWarning", resourceCulture); } } /// - /// Looks up a localized string similar to Link successfully created. + /// Looks up a localized string similar to Link successfully created.. /// - internal static string LinkSuccessfullyCreated { + public static string LinkSuccessfullyCreated { get { return ResourceManager.GetString("LinkSuccessfullyCreated", resourceCulture); } } /// - /// Looks up a localized string similar to Destination File. + /// Looks up a localized string similar to About. + /// + public static string MessageBoxAboutTitle { + get { + return ResourceManager.GetString("MessageBoxAboutTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error. /// - internal static string MainWindow_Switcher_Destination_File { + public static string MessageBoxErrorTitle { get { - return ResourceManager.GetString("MainWindow_Switcher_Destination_File", resourceCulture); + return ResourceManager.GetString("MessageBoxErrorTitle", resourceCulture); } } /// - /// Looks up a localized string similar to Destination Folder. + /// Looks up a localized string similar to An error has occurred:. /// - internal static string MainWindow_Switcher_Destination_Folder { + public static string MessageBoxExceptionOcurred { get { - return ResourceManager.GetString("MainWindow_Switcher_Destination_Folder", resourceCulture); + return ResourceManager.GetString("MessageBoxExceptionOcurred", resourceCulture); } } /// - /// Looks up a localized string similar to Link File. + /// Looks up a localized string similar to Success. /// - internal static string MainWindow_Switcher_Link_File { + public static string MessageBoxSuccessTitle { get { - return ResourceManager.GetString("MainWindow_Switcher_Link_File", resourceCulture); + return ResourceManager.GetString("MessageBoxSuccessTitle", resourceCulture); } } /// - /// Looks up a localized string similar to Link Folder. + /// Looks up a localized string similar to File. /// - internal static string MainWindow_Switcher_Link_Folder { + public static string PillFile { get { - return ResourceManager.GetString("MainWindow_Switcher_Link_Folder", resourceCulture); + return ResourceManager.GetString("PillFile", resourceCulture); } } /// - /// Looks up a localized string similar to Now give a name to the link:. + /// Looks up a localized string similar to Folder. /// - internal static string MainWindow_Switcher_Now_give_a_name_to_the_link_ { + public static string PillFolder { get { - return ResourceManager.GetString("MainWindow_Switcher_Now_give_a_name_to_the_link_", resourceCulture); + return ResourceManager.GetString("PillFolder", resourceCulture); } } /// - /// Looks up a localized string similar to Now give a name to your file:. + /// Looks up a localized string similar to Hard link. /// - internal static string MainWindow_Switcher_Now_give_a_name_to_your_file_ { + public static string PillHardLink { get { - return ResourceManager.GetString("MainWindow_Switcher_Now_give_a_name_to_your_file_", resourceCulture); + return ResourceManager.GetString("PillHardLink", resourceCulture); } } /// - /// Looks up a localized string similar to Please select the path to the real file you want to link:. + /// Looks up a localized string similar to Junction. /// - internal static string MainWindow_Switcher_Please_select_the_path_to_the_real_file_you_want_to_link_ { + public static string PillJunction { get { - return ResourceManager.GetString("MainWindow_Switcher_Please_select_the_path_to_the_real_file_you_want_to_link_", resourceCulture); + return ResourceManager.GetString("PillJunction", resourceCulture); } } /// - /// Looks up a localized string similar to Please select the path to the real folder you want to link:. + /// Looks up a localized string similar to Symbolic link. /// - internal static string MainWindow_Switcher_Please_select_the_path_to_the_real_folder_you_want_to_link_ { + public static string PillSymbolicLink { get { - return ResourceManager.GetString("MainWindow_Switcher_Please_select_the_path_to_the_real_folder_you_want_to_link_", resourceCulture); + return ResourceManager.GetString("PillSymbolicLink", resourceCulture); } } /// - /// Looks up a localized string similar to About. + /// Looks up a localized string similar to Link name. /// - internal static string MessageBoxAboutTitle { + public static string PlaceholderLinkName { get { - return ResourceManager.GetString("MessageBoxAboutTitle", resourceCulture); + return ResourceManager.GetString("PlaceholderLinkName", resourceCulture); } } /// - /// Looks up a localized string similar to Error. + /// Looks up a localized string similar to C:\path\to\location. /// - internal static string MessageBoxErrorTitle { + public static string PlaceholderLinkPath { get { - return ResourceManager.GetString("MessageBoxErrorTitle", resourceCulture); + return ResourceManager.GetString("PlaceholderLinkPath", resourceCulture); } } /// - /// Looks up a localized string similar to An error has ocurred:. + /// Looks up a localized string similar to Target file. /// - internal static string MessageBoxExceptionOcurred { + public static string PlaceholderTargetFile { get { - return ResourceManager.GetString("MessageBoxExceptionOcurred", resourceCulture); + return ResourceManager.GetString("PlaceholderTargetFile", resourceCulture); } } /// - /// Looks up a localized string similar to Success. + /// Looks up a localized string similar to Target folder. /// - internal static string MessageBoxSuccessTitle { + public static string PlaceholderTargetFolder { get { - return ResourceManager.GetString("MessageBoxSuccessTitle", resourceCulture); + return ResourceManager.GetString("PlaceholderTargetFolder", resourceCulture); } } /// - /// Looks up a localized string similar to This option allows you to select the style of your symbolic link, either - ///you choose to use symbolic links or hard links. - ///Use symbolic links as a default.. + /// Looks up a localized string similar to D:\path\to\target. /// - internal static string TooltipLinkTypeFileDescription { + public static string PlaceholderTargetPath { + get { + return ResourceManager.GetString("PlaceholderTargetPath", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to About Symlinker. + /// + public static string TooltipAbout { + get { + return ResourceManager.GetString("TooltipAbout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Choose where the link will be created and give it a name. + ///Click the icon or drag and drop a folder here.. + /// + public static string TooltipLinkLocationCard { + get { + return ResourceManager.GetString("TooltipLinkLocationCard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Choose the link type: symbolic link or hard link. + ///Use symbolic links as the default.. + /// + public static string TooltipLinkTypeFileDescription { get { return ResourceManager.GetString("TooltipLinkTypeFileDescription", resourceCulture); } } /// - /// Looks up a localized string similar to This option allows you to select the style of your symbolic link, either - ///you choose to use symbolic links, hard links or directory junctions. - ///Use symbolic links as a default.. + /// Looks up a localized string similar to Choose the link type: symbolic link, hard link, or directory junction. + ///Use symbolic links as the default.. /// - internal static string TooltipLinkTypeFolderDescription { + public static string TooltipLinkTypeFolderDescription { get { return ResourceManager.GetString("TooltipLinkTypeFolderDescription", resourceCulture); } } /// - /// Looks up a localized string similar to Symbolic Link types. + /// Looks up a localized string similar to Symbolic link types. /// - internal static string TooltipLinkTypeTitle { + public static string TooltipLinkTypeTitle { get { return ResourceManager.GetString("TooltipLinkTypeTitle", resourceCulture); } } /// - /// Looks up a localized string similar to With this option you can choose between creating file symbolic links; - ///this is using a file to point to another file, or folder symbolic links; - ///this is using folders that point to other folders.. + /// Looks up a localized string similar to Select the real file or folder the link will point to. + ///Click the icon or drag and drop a file/folder here.. + /// + public static string TooltipTargetCard { + get { + return ResourceManager.GetString("TooltipTargetCard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Choose between file mode (link a file to another file) + ///or folder mode (link a folder to another folder).. /// - internal static string TooltipTypeSelectorDescription { + public static string TooltipTypeSelectorDescription { get { return ResourceManager.GetString("TooltipTypeSelectorDescription", resourceCulture); } } /// - /// Looks up a localized string similar to Symbolic Link type selector. + /// Looks up a localized string similar to Symbolic link type selector. /// - internal static string TooltipTypeSelectorTitle { + public static string TooltipTypeSelectorTitle { get { return ResourceManager.GetString("TooltipTypeSelectorTitle", resourceCulture); } diff --git a/Symlinker/Properties/Resources.resx b/Symlinker/Properties/Resources.resx index b9ed390..e3b66f5 100644 --- a/Symlinker/Properties/Resources.resx +++ b/Symlinker/Properties/Resources.resx @@ -118,34 +118,37 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - The link name you are using already exists in the selected directory, would you like to DELETE the file and then create a new link? + A file with this link name already exists in the selected directory. Would you like to delete it and create a new link? - File already there... + File already exists - Link creation aborted + Link creation was aborted. - Aborted Operation + Operation aborted - One of the directories/files does not exists, please provide valid directories/files + One of the specified directories or files does not exist. Please provide valid paths. + + + Paths cannot contain quote characters. Please remove any quotes and try again. - Please fill all the blanks spaces with the indicated info + Please fill in all the required fields. - Cannot find the file needed to create links, the creation stopped + Cannot find the command-line tool needed to create links. - Link successfully created + Link successfully created. - The link name you are using already exists in the selected directory, would you like to DELETE the folder and then create a new link? + A folder with this link name already exists in the selected directory. Would you like to delete it and create a new link? - Folder already there... + Folder already exists Success @@ -153,62 +156,91 @@ Error - - Link Folder + + LINK LOCATION + + + TARGET + + + Link name + + + C:\path\to\location + + + D:\path\to\target + + + Target folder - - Destination Folder + + Target file - - Now give a name to the link: + + Folder - - Please select the path to the real folder you want to link: + + File - - Link File + + Symbolic link - - Destination File + + Hard link - - Now give a name to your file: + + Junction - - Please select the path to the real file you want to link: + + Create link + + + Add another About - © 2010-2025 Alejandro Mora + © 2010-{1} Alejandro Mora Version: {0} e-mail: mail@alejandro.md Thanks to Microsoft for the use of their shortcut arrow :) - An error has ocurred: + An error has occurred: + + + Choose where the link will be created and give it a name. +Click the icon or drag and drop a folder here. + + + Select the real file or folder the link will point to. +Click the icon or drag and drop a file/folder here. - This option allows you to select the style of your symbolic link, either -you choose to use symbolic links or hard links. -Use symbolic links as a default. + Choose the link type: symbolic link or hard link. +Use symbolic links as the default. - This option allows you to select the style of your symbolic link, either -you choose to use symbolic links, hard links or directory junctions. -Use symbolic links as a default. + Choose the link type: symbolic link, hard link, or directory junction. +Use symbolic links as the default. - Symbolic Link types + Symbolic link types - With this option you can choose between creating file symbolic links; -this is using a file to point to another file, or folder symbolic links; -this is using folders that point to other folders. + Choose between file mode (link a file to another file) +or folder mode (link a folder to another folder). - Symbolic Link type selector + Symbolic link type selector + + + About + + + About Symlinker \ No newline at end of file diff --git a/Symlinker/Properties/app.manifest b/Symlinker/Properties/app.manifest index 2856403..f13c900 100644 --- a/Symlinker/Properties/app.manifest +++ b/Symlinker/Properties/app.manifest @@ -31,6 +31,12 @@ + + + true/pm + PerMonitorV2 + +