Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

Commit

Permalink
Add shortcut helper
Browse files Browse the repository at this point in the history
  • Loading branch information
caesay committed Dec 26, 2023
1 parent ad4fdb4 commit b021ac5
Showing 1 changed file with 290 additions and 0 deletions.
290 changes: 290 additions & 0 deletions src/Squirrel/Windows/Shortcuts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using Microsoft.Extensions.Logging;
using Squirrel.Locators;
using Squirrel.NuGet;

namespace Squirrel.Windows
{
/// <summary>
/// Specifies several common places where shortcuts can be installed on a user's system
/// </summary>
[Flags]
public enum ShortcutLocation
{
/// <summary>
/// A shortcut in ProgramFiles within a publisher sub-directory
/// </summary>
StartMenu = 1 << 0,

/// <summary>
/// A shortcut on the current user desktop
/// </summary>
Desktop = 1 << 1,

/// <summary>
/// A shortcut in Startup/Run folder will cause the app to be automatially started on user login.
/// </summary>
Startup = 1 << 2,

/// <summary>
/// A shortcut in the application folder, useful for portable applications.
/// </summary>
AppRoot = 1 << 3,

/// <summary>
/// A shortcut in ProgramFiles root folder (not in a company/publisher sub-directory). This is commonplace as of more recent versions of windows.
/// </summary>
StartMenuRoot = 1 << 4,
}

/// <summary>
/// A helper class to create or delete windows shortcuts.
/// </summary>
[SupportedOSPlatform("windows")]
public class Shortcuts
{
/// <summary> Log for diagnostic messages. </summary>
protected ILogger Log { get; }

/// <summary> Locator to use for finding important application paths </summary>
protected ISquirrelLocator Locator { get; }

/// <inheritdoc cref="Shortcuts"/>
public Shortcuts(ILogger logger, ISquirrelLocator locator)
{
Log = logger;
Locator = locator;
}

/// <summary>
/// Create a shortcut to the currently running executable at the specified locations.
/// See <see cref="CreateShortcut"/> to create a shortcut to a different program
/// </summary>
public void CreateShortcutForThisExe(ShortcutLocation location = ShortcutLocation.Desktop | ShortcutLocation.StartMenuRoot)
{
CreateShortcut(
Locator.ThisExeRelativePath,
location,
Environment.CommandLine.Contains("squirrel-install") == false,
null, // shortcut arguments
null); // shortcut icon
}

/// <summary>
/// Removes a shortcut for the currently running executable at the specified locations
/// </summary>
public void RemoveShortcutForThisExe(ShortcutLocation location = ShortcutLocation.Desktop | ShortcutLocation.StartMenu | ShortcutLocation.StartMenuRoot)
{
DeleteShortcuts(
Locator.ThisExeRelativePath,
location);
}

/// <summary>
/// Searches for existing shortcuts to an executable inside the current package.
/// </summary>
/// <param name="relativeExeName">The relative path or filename of the executable (from the current app dir).</param>
/// <param name="locations">The locations to search.</param>
public Dictionary<ShortcutLocation, ShellLink> FindShortcuts(string relativeExeName, ShortcutLocation locations)
{
var release = Locator.GetLatestLocalFullPackage();
var pkgDir = Locator.PackagesDir;
var currentDir = Locator.AppContentDir;
var rootAppDirectory = Locator.RootAppDir;
Log.Info($"About to create shortcuts for {relativeExeName}, rootAppDir {rootAppDirectory}");

var pkgPath = Path.Combine(pkgDir, release.OriginalFilename);
var zf = new ZipPackage(pkgPath);
var exePath = Path.Combine(currentDir, relativeExeName);
var fileVerInfo = FileVersionInfo.GetVersionInfo(exePath);

var ret = new Dictionary<ShortcutLocation, ShellLink>();
foreach (var f in (ShortcutLocation[]) Enum.GetValues(typeof(ShortcutLocation))) {
if (!locations.HasFlag(f)) continue;

var file = LinkTargetForVersionInfo(f, zf, fileVerInfo, rootAppDirectory);
//var appUserModelId = Utility.GetAppUserModelId(zf.Id, exeName);
//var toastActivatorCLSDID = Utility.CreateGuidFromHash(appUserModelId).ToString();

Log.Info($"Creating shortcut for {relativeExeName} => {file}");
//Log.Info($"appUserModelId: {appUserModelId} | toastActivatorCLSID: {toastActivatorCLSDID}");

var target = Path.Combine(rootAppDirectory, relativeExeName);
var sl = new ShellLink {
Target = target,
IconPath = target,
IconIndex = 0,
WorkingDirectory = Path.GetDirectoryName(exePath),
Description = zf.ProductDescription,
};

//if (!String.IsNullOrWhiteSpace(programArguments)) {
// sl.Arguments += String.Format(" -a \"{0}\"", programArguments);
//}

//sl.SetAppUserModelId(appUserModelId);
//sl.SetToastActivatorCLSID(toastActivatorCLSDID);

ret.Add(f, sl);
}

return ret;
}

/// <summary>
/// Creates new shortcuts to the specified executable at the specified locations.
/// </summary>
/// <param name="relativeExeName">The relative path or filename of the executable (from the current app dir).</param>
/// <param name="locations">The locations to create shortcuts.</param>
/// <param name="updateOnly">If true, shortcuts will be updated instead of created.</param>
/// <param name="programArguments">The arguments the application should be launched with</param>
/// <param name="icon">Path to a specific icon to use instead of the exe icon.</param>
public void CreateShortcut(string relativeExeName, ShortcutLocation locations, bool updateOnly, string programArguments, string icon = null)
{
var release = Locator.GetLatestLocalFullPackage();
var pkgDir = Locator.PackagesDir;
var currentDir = Locator.AppContentDir;
var rootAppDirectory = Locator.RootAppDir;
Log.Info($"About to create shortcuts for {relativeExeName}, rootAppDir {rootAppDirectory}");

var pkgPath = Path.Combine(pkgDir, release.OriginalFilename);
var zf = new ZipPackage(pkgPath);
var exePath = Path.Combine(currentDir, relativeExeName);
var fileVerInfo = FileVersionInfo.GetVersionInfo(exePath);

foreach (var f in (ShortcutLocation[]) Enum.GetValues(typeof(ShortcutLocation))) {
if (!locations.HasFlag(f)) continue;

var file = LinkTargetForVersionInfo(f, zf, fileVerInfo, rootAppDirectory);
var fileExists = File.Exists(file);

// NB: If we've already installed the app, but the shortcut
// is no longer there, we have to assume that the user didn't
// want it there and explicitly deleted it, so we shouldn't
// annoy them by recreating it.
if (!fileExists && updateOnly) {
Log.Warn($"Wanted to update shortcut {file} but it appears user deleted it");
continue;
}

Log.Info($"Creating shortcut for {relativeExeName} => {file}");

ShellLink sl;
Utility.Retry(() => {
File.Delete(file);
var target = Path.Combine(rootAppDirectory, relativeExeName);
sl = new ShellLink {
Target = target,
IconPath = icon ?? target,
IconIndex = 0,
WorkingDirectory = Path.GetDirectoryName(exePath),
Description = zf.ProductDescription,
};
if (!String.IsNullOrWhiteSpace(programArguments)) {
sl.Arguments += String.Format(" -a \"{0}\"", programArguments);
}
//var appUserModelId = Utility.GetAppUserModelId(zf.Id, exeName);
//var toastActivatorCLSID = Utility.CreateGuidFromHash(appUserModelId).ToString();
//sl.SetAppUserModelId(appUserModelId);
//sl.SetToastActivatorCLSID(toastActivatorCLSID);
Log.Info($"About to save shortcut: {file} (target {sl.Target}, workingDir {sl.WorkingDirectory}, args {sl.Arguments})");
sl.Save(file);
});
}
}

/// <summary>
/// Delete all the shortcuts for the specified executable in the specified locations.
/// </summary>
/// <param name="relativeExeName">The relative path or filename of the executable (from the current app dir).</param>
/// <param name="locations">The locations to create shortcuts.</param>
public void DeleteShortcuts(string relativeExeName, ShortcutLocation locations)
{
var release = Locator.GetLatestLocalFullPackage();
var pkgDir = Locator.PackagesDir;
var currentDir = Locator.AppContentDir;
var rootAppDirectory = Locator.RootAppDir;
Log.Info($"About to delete shortcuts for {relativeExeName}, rootAppDir {rootAppDirectory}");

var pkgPath = Path.Combine(pkgDir, release.OriginalFilename);
var zf = new ZipPackage(pkgPath);
var exePath = Path.Combine(currentDir, relativeExeName);
var fileVerInfo = FileVersionInfo.GetVersionInfo(exePath);

foreach (var f in (ShortcutLocation[]) Enum.GetValues(typeof(ShortcutLocation))) {
if (!locations.HasFlag(f)) continue;
var file = LinkTargetForVersionInfo(f, zf, fileVerInfo, rootAppDirectory);
Log.Info($"Removing shortcut for {relativeExeName} => {file}");
try {
if (File.Exists(file)) File.Delete(file);
} catch (Exception ex) {
Log.Error(ex, "Couldn't delete shortcut: " + file);
}
}
}

/// <summary>
/// Given an <see cref="IPackage"/> and <see cref="FileVersionInfo"/> return the target shortcut path.
/// </summary>
protected virtual string LinkTargetForVersionInfo(ShortcutLocation location, IPackage package, FileVersionInfo versionInfo, string rootdir)
{
var possibleProductNames = new[] {
versionInfo.ProductName,
package.ProductName,
versionInfo.FileDescription,
Path.GetFileNameWithoutExtension(versionInfo.FileName)
};

var possibleCompanyNames = new[] {
versionInfo.CompanyName,
package.ProductCompany,
};

var prodName = possibleCompanyNames.First(x => !String.IsNullOrWhiteSpace(x));
var pkgName = possibleProductNames.First(x => !String.IsNullOrWhiteSpace(x));

return GetLinkTarget(location, pkgName, prodName, rootdir);
}

/// <summary>
/// Given the application info, return the shortcut target path.
/// </summary>
protected virtual string GetLinkTarget(ShortcutLocation location, string title, string applicationName, string rootdir, bool createDirectoryIfNecessary = true)
{
var dir = default(string);

switch (location) {
case ShortcutLocation.Desktop:
dir = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
break;
case ShortcutLocation.StartMenu:
dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", applicationName);
break;
case ShortcutLocation.StartMenuRoot:
dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs");
break;
case ShortcutLocation.Startup:
dir = Environment.GetFolderPath(Environment.SpecialFolder.Startup);
break;
case ShortcutLocation.AppRoot:
dir = rootdir;
break;
}

if (createDirectoryIfNecessary && !Directory.Exists(dir)) {
Directory.CreateDirectory(dir);
}

return Path.Combine(dir, title + ".lnk");
}
}
}

0 comments on commit b021ac5

Please sign in to comment.