Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit0050fc7

Browse files
committed
chore: add handler and router forcoder scheme URIs
1 parent6417d16 commit0050fc7

File tree

6 files changed

+237
-3
lines changed

6 files changed

+237
-3
lines changed

‎Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ struct DesktopApp: App {
1717
Window("Sign In", id:Windows.login.rawValue){
1818
LoginForm()
1919
.environmentObject(appDelegate.state)
20-
}
21-
.windowResizability(.contentSize)
20+
}.handlesExternalEvents(matching:Set()) // Don't handle deep links
21+
.windowResizability(.contentSize)
2222
SwiftUI.Settings{
2323
SettingsView<CoderVPNService>()
2424
.environmentObject(appDelegate.vpn)
@@ -30,7 +30,7 @@ struct DesktopApp: App {
3030
.environmentObject(appDelegate.state)
3131
.environmentObject(appDelegate.fileSyncDaemon)
3232
.environmentObject(appDelegate.vpn)
33-
}
33+
}.handlesExternalEvents(matching:Set()) // Don't handle deep links
3434
}
3535
}
3636

@@ -40,6 +40,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4040
letvpn:CoderVPNService
4141
letstate:AppState
4242
letfileSyncDaemon:MutagenDaemon
43+
leturlHandler:URLHandler
4344

4445
overrideinit(){
4546
vpn=CoderVPNService()
@@ -65,6 +66,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
6566
await fileSyncDaemon.tryStart()
6667
}
6768
self.fileSyncDaemon= fileSyncDaemon
69+
urlHandler=URLHandler(state: state, vpn: vpn)
6870
}
6971

7072
func applicationDidFinishLaunching(_:Notification){
@@ -126,6 +128,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
126128
func applicationShouldTerminateAfterLastWindowClosed(_:NSApplication)->Bool{
127129
false
128130
}
131+
132+
// If a deep link wasn't handled by any scenes in the `App`, it'll be sent here
133+
func application(_:NSApplication, open urls:[URL]){
134+
guardlet url= urls.firstelse{
135+
// We only accept one at time, for now
136+
return
137+
}
138+
do{try urlHandler.handle(url)}catch{
139+
// TODO: Push notification
140+
print(error.description)
141+
}
142+
}
129143
}
130144

131145
extensionAppDelegate{

‎Coder-Desktop/Coder-Desktop/Info.plist

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22
<!DOCTYPEplist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plistversion="1.0">
44
<dict>
5+
<key>CFBundleURLTypes</key>
6+
<array>
7+
<dict>
8+
<key>CFBundleTypeRole</key>
9+
<string>Editor</string>
10+
<key>CFBundleURLIconFile</key>
11+
<string>1024</string>
12+
<key>CFBundleURLName</key>
13+
<string>com.coder.Coder-Desktop</string>
14+
<key>CFBundleURLSchemes</key>
15+
<array>
16+
<string>coder</string>
17+
</array>
18+
</dict>
19+
</array>
520
<key>NSAppTransportSecurity</key>
621
<dict>
722
<!--
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import Foundation
2+
import URLRouting
3+
4+
@MainActor
5+
classURLHandler{
6+
letstate:AppState
7+
letvpn:anyVPNService
8+
letrouter:CoderRouter
9+
10+
init(state:AppState, vpn:anyVPNService){
11+
self.state= state
12+
self.vpn= vpn
13+
router=CoderRouter()
14+
}
15+
16+
func handle(_ url:URL)throws(URLError){
17+
guard state.hasSession,let deployment= state.baseAccessURLelse{
18+
throw.noSession
19+
}
20+
guard deployment.host()== url.hostelse{
21+
throw.invalidAuthority(url.host()??"<none>")
22+
}
23+
do{
24+
switchtry router.match(url: url){
25+
caselet.open(workspace, agent, type):
26+
switch type{
27+
caselet.rdp(creds):
28+
handleRDP(workspace: workspace, agent: agent, creds: creds)
29+
}
30+
}
31+
}catch{
32+
throw.routerError(url: url)
33+
}
34+
35+
func handleRDP(workspace _:String, agent _:String, creds _:RDPCredentials){
36+
// TODO: Handle RDP
37+
}
38+
}
39+
}
40+
41+
structCoderRouter:ParserPrinter{
42+
publicvarbody:someParserPrinter<URLRequestData,CoderRoute>{
43+
Route(.case(CoderRoute.open(workspace:agent:route:))){
44+
// v0/open/ws/<workspace>/agent/<agent>/<openType>
45+
Path{"v0";"open";"ws";Parse(.string);"agent";Parse(.string)}
46+
openRouter
47+
}
48+
}
49+
50+
varopenRouter:someParserPrinter<URLRequestData,OpenRoute>{
51+
OneOf{
52+
Route(.memberwise(OpenRoute.rdp)){
53+
Path{"rdp"}
54+
Query{
55+
Parse(.memberwise(RDPCredentials.init)){
56+
Optionally{Field("username")}
57+
Optionally{Field("password")}
58+
}
59+
}
60+
}
61+
}
62+
}
63+
}
64+
65+
enumURLError:Error{
66+
case invalidAuthority(String)
67+
case routerError(url:URL)
68+
case noSession
69+
70+
vardescription:String{
71+
switchself{
72+
caselet.invalidAuthority(authority):
73+
"Authority '\(authority)' does not match the host of the current Coder deployment."
74+
caselet.routerError(url):
75+
"Failed to handle\(url.absoluteString) because the format is unsupported."
76+
case.noSession:
77+
"Not logged in."
78+
}
79+
}
80+
81+
varlocalizedDescription:String{ description}
82+
}
83+
84+
publicenumCoderRoute:Equatable,Sendable{
85+
case open(workspace:String, agent:String, route:OpenRoute)
86+
}
87+
88+
publicenumOpenRoute:Equatable,Sendable{
89+
case rdp(RDPCredentials)
90+
}
91+
92+
// Due to a Swift Result builder limitation, we can't flatten this out to `case rdp(String?, String?)`
93+
// https://github.com/pointfreeco/swift-url-routing/issues/50
94+
publicstructRDPCredentials:Equatable,Sendable{
95+
letusername:String?
96+
letpassword:String?
97+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
@testableimport Coder_Desktop
2+
import Foundation
3+
import Testing
4+
import URLRouting
5+
6+
@MainActor
7+
@Suite(.timeLimit(.minutes(1)))
8+
structCoderRouterTests{
9+
letrouter:CoderRouter
10+
11+
init(){
12+
router=CoderRouter()
13+
}
14+
15+
structRouteTestCase:CustomStringConvertible,Sendable{
16+
leturlString:String
17+
letexpectedRoute:CoderRoute?
18+
letdescription:String
19+
}
20+
21+
@Test("RDP routes", arguments:[
22+
// Valid routes
23+
RouteTestCase(
24+
urlString:"https://coder.example.com/v0/open/ws/myworkspace/agent/dev/rdp?username=user&password=pass",
25+
expectedRoute:.open(
26+
workspace:"myworkspace",
27+
agent:"dev",
28+
route:.rdp(RDPCredentials(username:"user", password:"pass"))
29+
),
30+
description:"RDP with username and password"
31+
),
32+
RouteTestCase(
33+
urlString:"https://coder.example.com/v0/open/ws/workspace-123/agent/agent-456/rdp",
34+
expectedRoute:.open(
35+
workspace:"workspace-123",
36+
agent:"agent-456",
37+
route:.rdp(RDPCredentials(username:nil, password:nil))
38+
),
39+
description:"RDP without credentials"
40+
),
41+
RouteTestCase(
42+
urlString:"https://coder.example.com/v0/open/ws/workspace-123/agent/agent-456/rdp?username=user",
43+
expectedRoute:.open(
44+
workspace:"workspace-123",
45+
agent:"agent-456",
46+
route:.rdp(RDPCredentials(username:"user", password:nil))
47+
),
48+
description:"RDP with username only"
49+
),
50+
RouteTestCase(
51+
urlString:"https://coder.example.com/v0/open/ws/workspace-123/agent/agent-456/rdp?password=pass",
52+
expectedRoute:.open(
53+
workspace:"workspace-123",
54+
agent:"agent-456",
55+
route:.rdp(RDPCredentials(username:nil, password:"pass"))
56+
),
57+
description:"RDP with password only"
58+
),
59+
RouteTestCase(
60+
urlString:"https://coder.example.com/v0/open/ws/ws-special-chars/agent/agent-with-dashes/rdp",
61+
expectedRoute:.open(
62+
workspace:"ws-special-chars",
63+
agent:"agent-with-dashes",
64+
route:.rdp(RDPCredentials(username:nil, password:nil))
65+
),
66+
description:"RDP with special characters in workspace and agent IDs"
67+
),
68+
69+
// Invalid routes
70+
RouteTestCase(
71+
urlString:"https://coder.example.com/invalid/path",
72+
expectedRoute:nil,
73+
description:"Completely invalid path"
74+
),
75+
RouteTestCase(
76+
urlString:"https://coder.example.com/v1/open/ws/workspace-123/agent/agent-456/rdp",
77+
expectedRoute:nil,
78+
description:"Invalid version prefix (v1 instead of v0)"
79+
),
80+
RouteTestCase(
81+
urlString:"https://coder.example.com/v0/open/workspace-123/agent/agent-456/rdp",
82+
expectedRoute:nil,
83+
description:"Missing 'ws' segment"
84+
),
85+
RouteTestCase(
86+
urlString:"https://coder.example.com/v0/open/ws/workspace-123/rdp",
87+
expectedRoute:nil,
88+
description:"Missing agent segment"
89+
),
90+
])
91+
func testRdpRoutes(testCase:RouteTestCase)throws{
92+
leturl=URL(string: testCase.urlString)!
93+
94+
iflet expectedRoute= testCase.expectedRoute{
95+
letroute=try router.match(url: url)
96+
#expect(route== expectedRoute)
97+
}else{
98+
#expect(throws:(anyError).self){
99+
_=try router.match(url: url)
100+
}
101+
}
102+
}
103+
}

‎Coder-Desktop/Resources/1024.png

17.7 KB
Loading

‎Coder-Desktop/project.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ packages:
126126
SDWebImageSVGCoder:
127127
url:https://github.com/SDWebImage/SDWebImageSVGCoder
128128
exactVersion:1.7.0
129+
URLRouting:
130+
url:https://github.com/pointfreeco/swift-url-routing
131+
revision:09b155d
132+
129133

130134
targets:
131135
Coder Desktop:
@@ -185,6 +189,7 @@ targets:
185189
-package:LaunchAtLogin
186190
-package:SDWebImageSwiftUI
187191
-package:SDWebImageSVGCoder
192+
-package:URLRouting
188193
scheme:
189194
testPlans:
190195
-path:Coder-Desktop.xctestplan

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp