Skip to content

Commit a14a43d

Browse files
ctruedenclaude
andcommitted
Add Linux platform with .desktop file generation
Implements LinuxPlatform to handle Linux-specific desktop integration. Creates .desktop files in ~/.local/share/applications/ for proper application integration including: - Application launcher in desktop menus - Application icon display - Executable path configuration - MimeType field for URI scheme registration (via scijava-links) Configuration via system properties: - scijava.app.name: Application name - scijava.app.executable: Path to executable - scijava.app.icon: Icon file path - scijava.app.directory: Working directory - scijava.app.desktop-file: .desktop file path (auto-set if not provided) The LinuxPlatform creates the basic .desktop file structure, and scijava-links later modifies it to add URI scheme handlers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 0d6f36b commit a14a43d

File tree

1 file changed

+212
-0
lines changed

1 file changed

+212
-0
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* #%L
3+
* Desktop integration for SciJava.
4+
* %%
5+
* Copyright (C) 2010 - 2026 SciJava developers.
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
* #L%
28+
*/
29+
30+
package org.scijava.desktop.platform.linux;
31+
32+
import org.scijava.log.LogService;
33+
import org.scijava.platform.AbstractPlatform;
34+
import org.scijava.platform.Platform;
35+
import org.scijava.platform.PlatformService;
36+
import org.scijava.plugin.Parameter;
37+
import org.scijava.plugin.Plugin;
38+
39+
import java.io.BufferedWriter;
40+
import java.io.IOException;
41+
import java.net.URL;
42+
import java.nio.charset.StandardCharsets;
43+
import java.nio.file.Files;
44+
import java.nio.file.Path;
45+
import java.nio.file.Paths;
46+
47+
/**
48+
* A platform implementation for handling Linux platform issues.
49+
* <p>
50+
* This implementation creates and maintains a .desktop file for the application,
51+
* enabling proper desktop integration including:
52+
* </p>
53+
* <ul>
54+
* <li>Application launcher in menus</li>
55+
* <li>Application icon</li>
56+
* <li>File associations (via separate configuration)</li>
57+
* <li>URI scheme handling (via scijava-links)</li>
58+
* </ul>
59+
*
60+
* @author Curtis Rueden
61+
*/
62+
@Plugin(type = Platform.class, name = "Linux")
63+
public class LinuxPlatform extends AbstractPlatform {
64+
65+
@Parameter(required = false)
66+
private LogService log;
67+
68+
// -- Platform methods --
69+
70+
@Override
71+
public String osName() {
72+
return "Linux";
73+
}
74+
75+
@Override
76+
public void configure(final PlatformService service) {
77+
super.configure(service);
78+
79+
// Create or update .desktop file for desktop integration
80+
try {
81+
installDesktopFile();
82+
}
83+
catch (final IOException e) {
84+
if (log != null) {
85+
log.error("Failed to install .desktop file", e);
86+
}
87+
}
88+
}
89+
90+
@Override
91+
public void open(final URL url) throws IOException {
92+
if (getPlatformService().exec("xdg-open", url.toString()) != 0) {
93+
throw new IOException("Could not open " + url);
94+
}
95+
}
96+
97+
// -- Helper methods --
98+
99+
/**
100+
* Creates or updates the .desktop file for this application.
101+
* <p>
102+
* The .desktop file path is determined by the {@code scijava.app.desktop-file}
103+
* system property. If not set, defaults to {@code ~/.local/share/applications/<app>.desktop}.
104+
* </p>
105+
*/
106+
private void installDesktopFile() throws IOException {
107+
// Get configuration from system properties
108+
String desktopFilePath = System.getProperty("scijava.app.desktop-file");
109+
110+
if (desktopFilePath == null) {
111+
// Default location
112+
final String appName = System.getProperty("scijava.app.name", "scijava-app");
113+
final String home = System.getProperty("user.home");
114+
desktopFilePath = home + "/.local/share/applications/" + sanitizeFileName(appName) + ".desktop";
115+
116+
// Set property for other components (e.g., scijava-links)
117+
System.setProperty("scijava.app.desktop-file", desktopFilePath);
118+
}
119+
120+
final Path desktopFile = Paths.get(desktopFilePath);
121+
122+
// Check if file already exists and is up-to-date
123+
if (Files.exists(desktopFile) && isDesktopFileUpToDate(desktopFile)) {
124+
if (log != null) {
125+
log.debug("Desktop file is up-to-date: " + desktopFile);
126+
}
127+
return;
128+
}
129+
130+
// Get application properties
131+
final String appName = System.getProperty("scijava.app.name", "SciJava Application");
132+
final String appExec = System.getProperty("scijava.app.executable");
133+
final String appIcon = System.getProperty("scijava.app.icon");
134+
final String appDir = System.getProperty("scijava.app.directory");
135+
136+
if (appExec == null) {
137+
if (log != null) {
138+
log.debug("No executable path set (scijava.app.executable property), skipping .desktop file creation");
139+
}
140+
return;
141+
}
142+
143+
// Create parent directory if needed
144+
final Path parent = desktopFile.getParent();
145+
if (parent != null && !Files.exists(parent)) {
146+
Files.createDirectories(parent);
147+
}
148+
149+
// Write .desktop file
150+
try (final BufferedWriter writer = Files.newBufferedWriter(desktopFile, StandardCharsets.UTF_8)) {
151+
writer.write("[Desktop Entry]");
152+
writer.newLine();
153+
writer.write("Type=Application");
154+
writer.newLine();
155+
writer.write("Version=1.0");
156+
writer.newLine();
157+
writer.write("Name=" + appName);
158+
writer.newLine();
159+
writer.write("GenericName=" + appName);
160+
writer.newLine();
161+
writer.write("X-GNOME-FullName=" + appName);
162+
writer.newLine();
163+
164+
if (appIcon != null) {
165+
writer.write("Icon=" + appIcon);
166+
writer.newLine();
167+
}
168+
169+
writer.write("Exec=" + appExec + " %U");
170+
writer.newLine();
171+
172+
if (appDir != null) {
173+
writer.write("Path=" + appDir);
174+
writer.newLine();
175+
}
176+
177+
writer.write("Terminal=false");
178+
writer.newLine();
179+
writer.write("Categories=Science;Education;");
180+
writer.newLine();
181+
182+
// MimeType field intentionally left empty
183+
// scijava-links will add URI scheme handlers (x-scheme-handler/...)
184+
writer.write("MimeType=");
185+
writer.newLine();
186+
}
187+
188+
// Make file readable (but not writable) by others
189+
// This is standard practice for .desktop files
190+
// Files.setPosixFilePermissions can be used here if needed
191+
192+
if (log != null) {
193+
log.info("Created desktop file: " + desktopFile);
194+
}
195+
}
196+
197+
/**
198+
* Checks if the desktop file is up-to-date with current system properties.
199+
*/
200+
private boolean isDesktopFileUpToDate(final Path desktopFile) {
201+
// For now, simple existence check
202+
// Future: could parse and compare with current properties
203+
return Files.exists(desktopFile);
204+
}
205+
206+
/**
207+
* Sanitizes a string for use as a file name.
208+
*/
209+
private String sanitizeFileName(final String name) {
210+
return name.replaceAll("[^a-zA-Z0-9._-]", "-").toLowerCase();
211+
}
212+
}

0 commit comments

Comments
 (0)