crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.45k stars 1.62k forks source link

Opening a shortcut on Windows #14783

Open straight-shoota opened 4 months ago

straight-shoota commented 4 months ago

This has come up in Gitter chat.

Windows has shortcut files for launching programs or opening files. They have the extension .lnk and contain a reference to the target as well as some other properties (such as icon path, working directory etc.). A similar concept in Unix land are Desktop Entries (.desktop).

It's very common to use shortcuts on Windows (e.g. the Desktop is usually full of them) and users expect that they can open a shortcut to execute whatever it points to.

Shortcut files are not directly executable with CreateProcessW, so you cannot open a shortcut with Process.run (not even with shell: true, which doesn't really do anything on Windows; ref #9030).

Process.run("shortcut.lnk") # raises File::BadExecutableError
Process.run("shortcut.lnk", shell: true) # raises File::BadExecutableError

They can be opened in cmd or powershell

Process.run("cmd /c shortcut.lnk")

So going though cmd /c provides a solution, but it feels a bit hacky.

The Win32 API has a native way to open files, which supports shortcuts: ShellExecuteA & co. These functions also provide other operations.

The following example shows how the ShellExecuteExW function can be used to open a file (which may be a shortcut):

def open_file(path)
    shell_exec_info = LibC::SHELLEXECUTEINFOW.new(
      cbSize: sizeof(LibC::SHELLEXECUTEINFOW),
      fMask: 0,
      hwnd: nil,
      lpVerb: nil,
      lpFile: Crystal::System.to_wstr(path),
      lpParameters: nil,
      lpDirectory: nil,
      nShow: LibC::SW_NORMAL,
      hInstApp: nil
    )
    LibC.ShellExecuteExW(pointerof(shell_exec_info))
end

lib LibC
  struct SHELLEXECUTEINFOW
    cbSize: DWORD
    fMask: ULong
    hwnd: Void*
    lpVerb: LPWSTR
    lpFile: LPWSTR
    lpParameters: LPWSTR
    lpDirectory: LPWSTR
    nShow: Int
    hInstApp: Void*
    lpIDList: Void*
    lpClass: LPWSTR
    hkeyClass: HKEY
    dwHotKey: DWORD
    hIconOrMonitor : HANDLE
    hProcess: HANDLE
  end

  fun ShellExecuteExW(
    pExecInfo : SHELLEXECUTEINFOW*
  ) : BOOL

  SW_NORMAL = 1
end

I'm not sure what we could make out of this. Whether there can be a good and simple solution, or a one better than prefixing the file path with cmd /c. Maybe this issue can draw some ideas on the matter.

devnote-dev commented 3 months ago

Given that shortcuts can be opened in cmd and powershell, I think it makes sense to wait for #13567 to be merged so that shell: true actually works on Windows and subsequently opening shortcuts via Process.run.