Skip to content

Commit 0d6f36b

Browse files
ctruedenclaude
andcommitted
Add Linux URI scheme registration
Extends URI scheme registration to Linux via .desktop file manipulation. The LinuxSchemeInstaller modifies the MimeType field in the .desktop file to add x-scheme-handler entries, then registers them using xdg-mime. Key components: - DesktopFile: Simple parser/writer for .desktop files - LinuxSchemeInstaller: Adds URI schemes to existing .desktop files - Updates DefaultLinkService to support both Windows and Linux The .desktop file path is specified via the scijava.app.desktop-file system property. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4517654 commit 0d6f36b

File tree

4 files changed

+729
-6
lines changed

4 files changed

+729
-6
lines changed

src/main/java/org/scijava/desktop/links/DefaultLinkService.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.scijava.event.ContextCreatedEvent;
3232
import org.scijava.event.EventHandler;
3333
import org.scijava.desktop.links.SchemeInstaller;
34+
import org.scijava.desktop.platform.linux.LinuxSchemeInstaller;
3435
import org.scijava.desktop.platform.windows.WindowsSchemeInstaller;
3536
import org.scijava.log.LogService;
3637
import org.scijava.plugin.AbstractHandlerService;
@@ -64,15 +65,15 @@ private void onEvent(final ContextCreatedEvent evt) {
6465
}
6566
}
6667

67-
// Register URI schemes with the operating system (Windows only).
68+
// Register URI schemes with the operating system.
6869
installSchemes();
6970
}
7071

7172
/**
7273
* Installs URI schemes with the operating system.
7374
* <p>
7475
* This method collects all schemes supported by registered {@link LinkHandler}
75-
* plugins and registers them with the OS (currently Windows only).
76+
* plugins and registers them with the OS (Windows and Linux supported).
7677
* </p>
7778
*/
7879
private void installSchemes() {
@@ -111,13 +112,23 @@ private void installSchemes() {
111112
/**
112113
* Creates the appropriate {@link SchemeInstaller} for the current platform.
113114
* <p>
114-
* Currently only Windows is supported. macOS uses Info.plist in the .app bundle
115-
* (configured at build time), and Linux .desktop file management belongs in
116-
* scijava-plugins-platforms.
115+
* Windows and Linux are supported via runtime registration. macOS uses Info.plist
116+
* in the .app bundle (configured at build time, not at runtime).
117117
* </p>
118118
*/
119119
private SchemeInstaller createInstaller() {
120-
return new WindowsSchemeInstaller(log);
120+
final String os = System.getProperty("os.name");
121+
if (os == null) return null;
122+
123+
final String osLower = os.toLowerCase();
124+
if (osLower.contains("linux")) {
125+
return new LinuxSchemeInstaller(log);
126+
}
127+
else if (osLower.contains("win")) {
128+
return new WindowsSchemeInstaller(log);
129+
}
130+
131+
return null; // macOS or other unsupported platforms
121132
}
122133

123134
/**
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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 java.io.BufferedReader;
33+
import java.io.BufferedWriter;
34+
import java.io.IOException;
35+
import java.nio.charset.StandardCharsets;
36+
import java.nio.file.Files;
37+
import java.nio.file.Path;
38+
import java.util.ArrayList;
39+
import java.util.LinkedHashMap;
40+
import java.util.List;
41+
import java.util.Map;
42+
43+
/**
44+
* Simple parser and writer for Linux .desktop files.
45+
* <p>
46+
* Supports reading and writing key-value pairs within the [Desktop Entry] section.
47+
* This implementation is minimal and focused on URI scheme registration needs.
48+
* </p>
49+
*
50+
* @author Curtis Rueden
51+
*/
52+
public class DesktopFile {
53+
54+
private final Map<String, String> entries;
55+
private final List<String> comments;
56+
57+
public DesktopFile() {
58+
this.entries = new LinkedHashMap<>();
59+
this.comments = new ArrayList<>();
60+
}
61+
62+
/**
63+
* Parses a .desktop file from disk.
64+
*
65+
* @param path Path to the .desktop file
66+
* @return Parsed DesktopFile
67+
* @throws IOException if reading fails
68+
*/
69+
public static DesktopFile parse(final Path path) throws IOException {
70+
final DesktopFile df = new DesktopFile();
71+
boolean inDesktopEntry = false;
72+
73+
try (final BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
74+
String line;
75+
while ((line = reader.readLine()) != null) {
76+
final String trimmed = line.trim();
77+
78+
// Track section
79+
if (trimmed.equals("[Desktop Entry]")) {
80+
inDesktopEntry = true;
81+
continue;
82+
}
83+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
84+
inDesktopEntry = false;
85+
continue;
86+
}
87+
88+
// Only process [Desktop Entry] section
89+
if (!inDesktopEntry) continue;
90+
91+
// Skip empty lines and comments
92+
if (trimmed.isEmpty() || trimmed.startsWith("#")) {
93+
df.comments.add(line);
94+
continue;
95+
}
96+
97+
// Parse key=value
98+
final int equals = line.indexOf('=');
99+
if (equals > 0) {
100+
final String key = line.substring(0, equals).trim();
101+
final String value = line.substring(equals + 1);
102+
df.entries.put(key, value);
103+
}
104+
}
105+
}
106+
107+
return df;
108+
}
109+
110+
/**
111+
* Writes the .desktop file to disk.
112+
*
113+
* @param path Path to write to
114+
* @throws IOException if writing fails
115+
*/
116+
public void writeTo(final Path path) throws IOException {
117+
// Ensure parent directory exists
118+
final Path parent = path.getParent();
119+
if (parent != null && !Files.exists(parent)) {
120+
Files.createDirectories(parent);
121+
}
122+
123+
try (final BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
124+
writer.write("[Desktop Entry]");
125+
writer.newLine();
126+
127+
// Write key-value pairs
128+
for (final Map.Entry<String, String> entry : entries.entrySet()) {
129+
writer.write(entry.getKey());
130+
writer.write('=');
131+
writer.write(entry.getValue());
132+
writer.newLine();
133+
}
134+
135+
// Write comments at the end
136+
for (final String comment : comments) {
137+
writer.write(comment);
138+
writer.newLine();
139+
}
140+
}
141+
}
142+
143+
/**
144+
* Gets the value for a key.
145+
*
146+
* @param key The key
147+
* @return The value, or null if not present
148+
*/
149+
public String get(final String key) {
150+
return entries.get(key);
151+
}
152+
153+
/**
154+
* Sets a key-value pair.
155+
*
156+
* @param key The key
157+
* @param value The value
158+
*/
159+
public void set(final String key, final String value) {
160+
entries.put(key, value);
161+
}
162+
163+
/**
164+
* Checks if a MimeType entry contains a specific MIME type.
165+
*
166+
* @param mimeType The MIME type to check (e.g., "x-scheme-handler/fiji")
167+
* @return true if the MimeType field contains this type
168+
*/
169+
public boolean hasMimeType(final String mimeType) {
170+
final String mimeTypes = entries.get("MimeType");
171+
if (mimeTypes == null || mimeTypes.isEmpty()) return false;
172+
173+
final String[] types = mimeTypes.split(";");
174+
for (final String type : types) {
175+
if (type.trim().equals(mimeType)) {
176+
return true;
177+
}
178+
}
179+
return false;
180+
}
181+
182+
/**
183+
* Adds a MIME type to the MimeType field.
184+
* <p>
185+
* The MimeType field is a semicolon-separated list. This method appends
186+
* the new type if it's not already present.
187+
* </p>
188+
*
189+
* @param mimeType The MIME type to add (e.g., "x-scheme-handler/fiji")
190+
*/
191+
public void addMimeType(final String mimeType) {
192+
if (hasMimeType(mimeType)) return; // Already present
193+
194+
String mimeTypes = entries.get("MimeType");
195+
if (mimeTypes == null || mimeTypes.isEmpty()) {
196+
// Create new MimeType field
197+
entries.put("MimeType", mimeType + ";");
198+
}
199+
else {
200+
// Append to existing
201+
if (!mimeTypes.endsWith(";")) {
202+
mimeTypes += ";";
203+
}
204+
entries.put("MimeType", mimeTypes + mimeType + ";");
205+
}
206+
}
207+
208+
/**
209+
* Removes a MIME type from the MimeType field.
210+
*
211+
* @param mimeType The MIME type to remove
212+
*/
213+
public void removeMimeType(final String mimeType) {
214+
final String mimeTypes = entries.get("MimeType");
215+
if (mimeTypes == null || mimeTypes.isEmpty()) return;
216+
217+
final List<String> types = new ArrayList<>();
218+
for (final String type : mimeTypes.split(";")) {
219+
final String trimmed = type.trim();
220+
if (!trimmed.isEmpty() && !trimmed.equals(mimeType)) {
221+
types.add(trimmed);
222+
}
223+
}
224+
225+
if (types.isEmpty()) {
226+
entries.remove("MimeType");
227+
}
228+
else {
229+
entries.put("MimeType", String.join(";", types) + ";");
230+
}
231+
}
232+
}

0 commit comments

Comments
 (0)