Skip to content

Commit 72c60f6

Browse files
authored
fix: open URLs on Windows (#116)
The URLs on Windows failed to be opened because the cmd executed via ProcessExecutor was not correctly constructed. We were calling `exec("cmd", "start \"$url\"")` but in Windows `/c` is also needed to the `cmd`. We originally used native commands to open URLs because Toolbox didn’t support it. Now that LocalDesktopManager provides an API for launching the browser, we no longer need to fix the command-line logic — we can just use the Toolbox API instead.
1 parent 3737aa8 commit 72c60f6

File tree

9 files changed

+34
-81
lines changed

9 files changed

+34
-81
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
### Fixed
1111

1212
- `Open web terminal` action is no longer displayed when the workspace is stopped.
13+
- URL links can now be opened in Windows
1314

1415
## 0.2.1 - 2025-05-05
1516

src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.coder.toolbox
22

3-
import com.coder.toolbox.browser.BrowserUtil
3+
import com.coder.toolbox.browser.browse
44
import com.coder.toolbox.cli.CoderCLIManager
55
import com.coder.toolbox.cli.SshCommandProcessHandle
66
import com.coder.toolbox.models.WorkspaceAndAgentStatus
@@ -74,7 +74,7 @@ class CoderRemoteEnvironment(
7474
if (wsRawStatus.canStop()) {
7575
actions.add(Action(context.i18n.ptrl("Open web terminal")) {
7676
context.cs.launch {
77-
BrowserUtil.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) {
77+
context.desktop.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) {
7878
context.ui.showErrorInfoPopup(it)
7979
}
8080
}
@@ -83,15 +83,17 @@ class CoderRemoteEnvironment(
8383
actions.add(
8484
Action(context.i18n.ptrl("Open in dashboard")) {
8585
context.cs.launch {
86-
BrowserUtil.browse(client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()) {
86+
context.desktop.browse(
87+
client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()
88+
) {
8789
context.ui.showErrorInfoPopup(it)
8890
}
8991
}
9092
})
9193

9294
actions.add(Action(context.i18n.ptrl("View template")) {
9395
context.cs.launch {
94-
BrowserUtil.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) {
96+
context.desktop.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) {
9597
context.ui.showErrorInfoPopup(it)
9698
}
9799
}

src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.coder.toolbox
22

3-
import com.coder.toolbox.browser.BrowserUtil
3+
import com.coder.toolbox.browser.browse
44
import com.coder.toolbox.cli.CoderCLIManager
55
import com.coder.toolbox.sdk.CoderRestClient
66
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
@@ -190,7 +190,7 @@ class CoderRemoteProvider(
190190
listOf(
191191
Action(context.i18n.ptrl("Create workspace")) {
192192
context.cs.launch {
193-
BrowserUtil.browse(client?.url?.withPath("/templates").toString()) {
193+
context.desktop.browse(client?.url?.withPath("/templates").toString()) {
194194
context.ui.showErrorInfoPopup(it)
195195
}
196196
}

src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.coder.toolbox.store.CoderSecretsStore
55
import com.coder.toolbox.store.CoderSettingsStore
66
import com.coder.toolbox.util.toURL
77
import com.jetbrains.toolbox.api.core.diagnostics.Logger
8+
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
89
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
910
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
1011
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
@@ -18,6 +19,7 @@ data class CoderToolboxContext(
1819
val envPageManager: EnvironmentUiPageManager,
1920
val envStateColorPalette: EnvironmentStateColorPalette,
2021
val ideOrchestrator: ClientHelper,
22+
val desktop: LocalDesktopManager,
2123
val cs: CoroutineScope,
2224
val logger: Logger,
2325
val i18n: LocalizableStringFactory,
@@ -62,5 +64,4 @@ data class CoderToolboxContext(
6264
} else null
6365
}
6466
}
65-
6667
}

src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.jetbrains.toolbox.api.core.PluginSettingsStore
88
import com.jetbrains.toolbox.api.core.ServiceLocator
99
import com.jetbrains.toolbox.api.core.diagnostics.Logger
1010
import com.jetbrains.toolbox.api.core.getService
11+
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
1112
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
1213
import com.jetbrains.toolbox.api.remoteDev.RemoteDevExtension
1314
import com.jetbrains.toolbox.api.remoteDev.RemoteProvider
@@ -31,6 +32,7 @@ class CoderToolboxExtension : RemoteDevExtension {
3132
serviceLocator.getService<EnvironmentUiPageManager>(),
3233
serviceLocator.getService<EnvironmentStateColorPalette>(),
3334
serviceLocator.getService<ClientHelper>(),
35+
serviceLocator.getService<LocalDesktopManager>(),
3436
serviceLocator.getService<CoroutineScope>(),
3537
serviceLocator.getService<Logger>(),
3638
serviceLocator.getService<LocalizableStringFactory>(),
Lines changed: 13 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,19 @@
11
package com.coder.toolbox.browser
22

3-
import com.coder.toolbox.util.OS
4-
import com.coder.toolbox.util.getOS
5-
import org.zeroturnaround.exec.ProcessExecutor
3+
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
4+
import java.net.URI
65

7-
class BrowserUtil {
8-
companion object {
9-
suspend fun browse(url: String, errorHandler: suspend (BrowserException) -> Unit) {
10-
val os = getOS()
11-
if (os == null) {
12-
errorHandler(BrowserException("Failed to open the URL because we can't detect the OS"))
13-
return
14-
}
15-
when (os) {
16-
OS.LINUX -> linuxBrowse(url, errorHandler)
17-
OS.MAC -> macBrowse(url, errorHandler)
18-
OS.WINDOWS -> windowsBrowse(url, errorHandler)
19-
}
20-
}
216

22-
private suspend fun linuxBrowse(url: String, errorHandler: suspend (BrowserException) -> Unit) {
23-
try {
24-
if (OS.LINUX.getDesktopEnvironment()?.uppercase()?.contains("GNOME") == true) {
25-
exec("gnome-open", url)
26-
} else {
27-
exec("xdg-open", url)
28-
}
29-
} catch (e: Exception) {
30-
errorHandler(
31-
BrowserException(
32-
"Failed to open URL because an error was encountered. Please make sure xdg-open from package xdg-utils is available!",
33-
e
34-
)
35-
)
36-
}
37-
}
38-
39-
private suspend fun macBrowse(url: String, errorHandler: suspend (BrowserException) -> Unit) {
40-
try {
41-
exec("open", url)
42-
} catch (e: Exception) {
43-
errorHandler(BrowserException("Failed to open URL because an error was encountered.", e))
44-
}
45-
}
46-
47-
private suspend fun windowsBrowse(url: String, errorHandler: suspend (BrowserException) -> Unit) {
48-
try {
49-
exec("cmd", "start \"$url\"")
50-
} catch (e: Exception) {
51-
errorHandler(BrowserException("Failed to open URL because an error was encountered.", e))
52-
}
53-
}
54-
55-
private fun exec(vararg args: String): String {
56-
val stdout =
57-
ProcessExecutor()
58-
.command(*args)
59-
.exitValues(0)
60-
.readOutput(true)
61-
.execute()
62-
.outputUTF8()
63-
return stdout
64-
}
7+
suspend fun LocalDesktopManager.browse(rawUrl: String, errorHandler: suspend (BrowserException) -> Unit) {
8+
try {
9+
val url = URI.create(rawUrl).toURL()
10+
this.openUrl(url)
11+
} catch (e: Exception) {
12+
errorHandler(
13+
BrowserException(
14+
"Failed to open $rawUrl because an error was encountered",
15+
e
16+
)
17+
)
6518
}
6619
}

src/main/kotlin/com/coder/toolbox/util/Dialogs.kt

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.coder.toolbox.util
22

33
import com.coder.toolbox.CoderToolboxContext
4-
import com.coder.toolbox.browser.BrowserUtil
4+
import com.coder.toolbox.browser.browse
55
import com.jetbrains.toolbox.api.localization.LocalizableString
66
import com.jetbrains.toolbox.api.ui.components.TextType
77
import java.net.URL
@@ -23,12 +23,7 @@ class DialogUi(private val context: CoderToolboxContext) {
2323
placeholder: LocalizableString? = null,
2424
): String? {
2525
return context.ui.showTextInputPopup(
26-
title,
27-
description,
28-
placeholder,
29-
TextType.General,
30-
context.i18n.ptrl("OK"),
31-
context.i18n.ptrl("Cancel")
26+
title, description, placeholder, TextType.General, context.i18n.ptrl("OK"), context.i18n.ptrl("Cancel")
3227
)
3328
}
3429

@@ -38,17 +33,12 @@ class DialogUi(private val context: CoderToolboxContext) {
3833
placeholder: LocalizableString? = null,
3934
): String? {
4035
return context.ui.showTextInputPopup(
41-
title,
42-
description,
43-
placeholder,
44-
TextType.Password,
45-
context.i18n.ptrl("OK"),
46-
context.i18n.ptrl("Cancel")
36+
title, description, placeholder, TextType.Password, context.i18n.ptrl("OK"), context.i18n.ptrl("Cancel")
4737
)
4838
}
4939

5040
private suspend fun openUrl(url: URL) {
51-
BrowserUtil.browse(url.toString()) {
41+
context.desktop.browse(url.toString()) {
5242
context.ui.showErrorInfoPopup(it)
5343
}
5444
}

src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import com.coder.toolbox.util.pluginTestSettingsStore
3131
import com.coder.toolbox.util.sha1
3232
import com.coder.toolbox.util.toURL
3333
import com.jetbrains.toolbox.api.core.diagnostics.Logger
34+
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
3435
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
3536
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
3637
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
@@ -66,6 +67,7 @@ internal class CoderCLIManagerTest {
6667
mockk<EnvironmentUiPageManager>(),
6768
mockk<EnvironmentStateColorPalette>(),
6869
mockk<ClientHelper>(),
70+
mockk<LocalDesktopManager>(),
6971
mockk<CoroutineScope>(),
7072
mockk<Logger>(relaxed = true),
7173
mockk<LocalizableStringFactory>(),

src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.coder.toolbox.store.TLS_CA_PATH
2121
import com.coder.toolbox.util.pluginTestSettingsStore
2222
import com.coder.toolbox.util.sslContextFromPEMs
2323
import com.jetbrains.toolbox.api.core.diagnostics.Logger
24+
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
2425
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
2526
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
2627
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
@@ -102,6 +103,7 @@ class CoderRestClientTest {
102103
mockk<EnvironmentUiPageManager>(),
103104
mockk<EnvironmentStateColorPalette>(),
104105
mockk<ClientHelper>(),
106+
mockk<LocalDesktopManager>(),
105107
mockk<CoroutineScope>(),
106108
mockk<Logger>(relaxed = true),
107109
mockk<LocalizableStringFactory>(),

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy