diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 918ac0f87e..5ce78926d8 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/1917[#1917]: Allow the creation of a desktop shortcut for the GUI The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/47?closed=1[milestone 2026.07.002]. diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/IdeasyCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/IdeasyCommandlet.java index a762cd2c2b..ebd6d2f9a8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/IdeasyCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/IdeasyCommandlet.java @@ -1,5 +1,6 @@ package com.devonfw.tools.ide.tool; +import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.nio.file.Files; @@ -295,6 +296,7 @@ public void installIdeasy(Path cwd) { addToShellRc(BASHRC, ideRoot, null); addToShellRc(ZSHRC, ideRoot, "autoload -U +X bashcompinit && bashcompinit"); installIdeasyWindowsEnv(ideRoot, installationPath); + installDesktopShortcut(installationPath); IdeLogLevel.SUCCESS.log(LOG, "IDEasy has been installed successfully on your system."); LOG.warn("IDEasy has been setup for new shells but it cannot work in your current shell(s).\n" + "To use it here, run 'source ~/.bashrc' (or your shell config). Otherwise, open a new terminal or reboot."); @@ -330,6 +332,113 @@ private void installIdeasyWindowsEnv(Path ideRoot, Path installationPath) { setGitLongpaths(); } } + private void installDesktopShortcut(Path installationPath) { + + try { + if (this.context.getSystemInfo().isLinux()) { + installLinuxDesktopShortcut(installationPath); + } else if (this.context.getSystemInfo().isMac()) { + installMacDesktopShortcut(installationPath); + } else if (this.context.getSystemInfo().isWindows()) { + installWindowsDesktopShortcut(installationPath); + } + } catch (Exception e) { + LOG.warn("Failed to create desktop shortcut.", e); + } + } + + private void installLinuxDesktopShortcut(Path installationPath) throws IOException { + + Path templateFile = installationPath.resolve("gui/linux/ideasy-gui.desktop"); + if (!Files.exists(templateFile)) { + LOG.warn("Desktop file template not found at {}. Skipping desktop shortcut creation.", templateFile); + return; + } + Path ideasyBin = installationPath.resolve("bin/ideasy"); + Path logoPath = installationPath.resolve("gui/logo.png"); + String content = Files.readString(templateFile) + .replace("@IDEASY_BIN@", ideasyBin.toString()) + .replace("@IDEASY_ICON@", logoPath.toString()); + + Path applicationsDir = this.context.getUserHome().resolve(".local/share/applications"); + this.context.getFileAccess().mkdirs(applicationsDir); + Path desktopFile = applicationsDir.resolve("ideasy-gui.desktop"); + this.context.getFileAccess().writeFileContent(content, desktopFile); + this.context.getFileAccess().makeExecutable(desktopFile); + + // without this, the new entry is only visible in application menus after the next login + try { + this.context.newProcess().executable("update-desktop-database").addArg(applicationsDir.toString()) + .run(ProcessMode.DEFAULT_CAPTURE); + } catch (Exception e) { + LOG.debug("update-desktop-database failed (optional): {}", e.getMessage()); + } + + // Mark as trusted for GNOME 3.28+ so the shortcut is executable without prompting + try { + this.context.newProcess().executable("gio") + .addArgs("set", desktopFile.toString(), "metadata::trusted", "true") + .run(ProcessMode.DEFAULT_CAPTURE); + } catch (Exception e) { + LOG.debug("gio set trusted failed (optional): {}", e.getMessage()); + } + + IdeLogLevel.SUCCESS.log(LOG, "Created desktop shortcut at {}", desktopFile); + } + + private void installMacDesktopShortcut(Path installationPath) { + + Path ideasyBin = installationPath.resolve("bin/ideasy"); + String escapedBin = ideasyBin.toString().replace("\"", "\\\""); + String content = "#!/bin/bash\n\"" + escapedBin + "\" gui\n"; + Path applicationsDir = this.context.getUserHome().resolve("Applications"); + this.context.getFileAccess().mkdirs(applicationsDir); + Path commandFile = applicationsDir.resolve("IDEasy GUI.command"); + this.context.getFileAccess().writeFileContent(content, commandFile); + this.context.getFileAccess().makeExecutable(commandFile); + IdeLogLevel.SUCCESS.log(LOG, "Created macOS launcher at {}", commandFile); + } + + private void installWindowsDesktopShortcut(Path installationPath) { + + Path ideasyExe = installationPath.resolve("bin\\ideasy.exe"); + Path icoPath = installationPath.resolve("gui\\logo.ico"); + // Shell Folders contains the already-expanded Desktop path, including OneDrive-redirected locations + WindowsHelper helper = WindowsHelper.get(this.context); + String desktopStr = helper.getRegistryValue( + "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", "Desktop"); + Path desktopPath = (desktopStr != null && !desktopStr.isBlank()) ? Path.of(desktopStr) : this.context.getUserHome().resolve("Desktop"); + this.context.getFileAccess().mkdirs(desktopPath); + createWindowsShortcut(desktopPath.resolve("IDEasy GUI.lnk"), ideasyExe, icoPath); + String startMenuStr = helper.getRegistryValue( + "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", "Programs"); + Path startMenu = (startMenuStr != null && !startMenuStr.isBlank()) ? Path.of(startMenuStr) + : this.context.getUserHome().resolve("AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs"); + if (Files.isDirectory(startMenu)) { + createWindowsShortcut(startMenu.resolve("IDEasy GUI.lnk"), ideasyExe, icoPath); + } + } + + private static String psEscapePath(Path path) { + return path.toString().replace("'", "''"); + } + + private void createWindowsShortcut(Path lnkPath, Path targetExe, Path icoPath) { + + String ps = "$ws = New-Object -ComObject WScript.Shell; " + + "$s = $ws.CreateShortcut('" + psEscapePath(lnkPath) + "'); " + + "$s.TargetPath = '" + psEscapePath(targetExe) + "'; " + + "$s.Arguments = 'gui'; " + + "$s.IconLocation = '" + psEscapePath(icoPath) + ",0'; " + + "$s.Save()"; + try { + this.context.newProcess().executable("powershell").addArgs("-Command", ps) + .run(ProcessMode.DEFAULT_CAPTURE); + IdeLogLevel.SUCCESS.log(LOG, "Created shortcut at {}", lnkPath); + } catch (Exception e) { + LOG.warn("Failed to create shortcut at {}.", lnkPath, e); + } + } private void setGitLongpaths() { this.context.getGitContext().findGitRequired(); diff --git a/cli/src/main/package/gui/linux/ideasy-gui.desktop b/cli/src/main/package/gui/linux/ideasy-gui.desktop new file mode 100644 index 0000000000..4f2a720d51 --- /dev/null +++ b/cli/src/main/package/gui/linux/ideasy-gui.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=$[project.version] +Type=Application +Name=IDEasy GUI +Comment=Launch IDEasy GUI +Exec=@IDEASY_BIN@ gui +Icon=@IDEASY_ICON@ +Terminal=false +Categories=Development; +StartupNotify=true diff --git a/cli/src/main/package/gui/logo.ico b/cli/src/main/package/gui/logo.ico new file mode 100644 index 0000000000..a5db696ec1 Binary files /dev/null and b/cli/src/main/package/gui/logo.ico differ diff --git a/cli/src/main/package/gui/logo.png b/cli/src/main/package/gui/logo.png new file mode 100644 index 0000000000..82bc932633 Binary files /dev/null and b/cli/src/main/package/gui/logo.png differ diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/IdeasyCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/IdeasyCommandletTest.java index 945e7fc192..76b354492c 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/IdeasyCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/IdeasyCommandletTest.java @@ -2,6 +2,7 @@ import java.nio.file.Path; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -112,6 +113,30 @@ void testInstallIdeasy(String os) { + "devon\n" + "source ~/.devon/autocomplete\n" + addedRcLines); + verifyDesktopShortcut(context, systemInfo, installationPath); + } + + private void verifyDesktopShortcut(IdeTestContext context, SystemInfo systemInfo, Path installationPath) { + + if (systemInfo.isLinux()) { + Path desktopFile = context.getUserHome().resolve(".local/share/applications/ideasy-gui.desktop"); + assertThat(desktopFile).exists(); + assertThat(desktopFile).content() + .contains("Exec=" + installationPath.resolve("bin/ideasy") + " gui") + .contains("Icon=" + installationPath.resolve("gui/logo.png")); + } else if (systemInfo.isMac()) { + Path commandFile = context.getUserHome().resolve("Applications/IDEasy GUI.command"); + assertThat(commandFile).exists(); + assertThat(commandFile).content() + .contains(installationPath.resolve("bin/ideasy").toString()) + .contains("gui"); + } else if (systemInfo.isWindows()) { + Assumptions.assumeTrue(System.getProperty("os.name", "").toLowerCase().startsWith("win"), + "Skipped: .lnk creation requires PowerShell, which is only available on Windows"); + assertThat(context.getUserHome().resolve("Desktop/IDEasy GUI.lnk")).exists(); + assertThat(context.getUserHome().resolve( + "AppData/Roaming/Microsoft/Windows/Start Menu/Programs/IDEasy GUI.lnk")).exists(); + } } /** diff --git a/cli/src/test/resources/ide-projects/install/project/home/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/.gitkeep b/cli/src/test/resources/ide-projects/install/project/home/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/src/test/resources/ide-projects/install/project/home/Downloads/ide-cli/gui/linux/ideasy-gui.desktop b/cli/src/test/resources/ide-projects/install/project/home/Downloads/ide-cli/gui/linux/ideasy-gui.desktop new file mode 100644 index 0000000000..997eed11b1 --- /dev/null +++ b/cli/src/test/resources/ide-projects/install/project/home/Downloads/ide-cli/gui/linux/ideasy-gui.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=test-SNAPSHOT +Type=Application +Name=IDEasy GUI +Comment=Launch IDEasy GUI +Exec=@IDEASY_BIN@ gui +Icon=@IDEASY_ICON@ +Terminal=false +Categories=Development; +StartupNotify=true diff --git a/cli/src/test/resources/ide-projects/install/project/home/Downloads/ide-cli/gui/logo.ico b/cli/src/test/resources/ide-projects/install/project/home/Downloads/ide-cli/gui/logo.ico new file mode 100644 index 0000000000..b746109bb2 --- /dev/null +++ b/cli/src/test/resources/ide-projects/install/project/home/Downloads/ide-cli/gui/logo.ico @@ -0,0 +1 @@ +logo.ico mock \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/install/project/home/Downloads/ide-cli/gui/logo.png b/cli/src/test/resources/ide-projects/install/project/home/Downloads/ide-cli/gui/logo.png new file mode 100644 index 0000000000..cf45109b9b --- /dev/null +++ b/cli/src/test/resources/ide-projects/install/project/home/Downloads/ide-cli/gui/logo.png @@ -0,0 +1 @@ +logo.png mock \ No newline at end of file