Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4b77803

Browse files
committedJan 17, 2025·
feat: support user-supplied literal headers
1 parent 5d97953 commit 4b77803

File tree

16 files changed

+456
-47
lines changed

16 files changed

+456
-47
lines changed
 

‎Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj

Lines changed: 123 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/* Begin PBXBuildFile section */
1010
961679332CFF117300B2B6DF /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 961679322CFF117300B2B6DF /* NetworkExtension.framework */; };
1111
9616793D2CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 961679302CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
12+
AA2C690F2D34F6920059AFAF /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = AA2C690E2D34F6920059AFAF /* LaunchAtLogin */; };
1213
AA3B3DA92D2D23860099996A /* VPNLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; };
1314
AA3B3DBF2D2D23AB0099996A /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B3DBE2D2D23AB0099996A /* SwiftProtobuf */; };
1415
AA3B3DC12D2D23AB0099996A /* SwiftProtobufPluginLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B3DC02D2D23AB0099996A /* SwiftProtobufPluginLibrary */; };
@@ -24,6 +25,7 @@
2425
AA8BC3392D0060A900E1ABAA /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC3382D0060A900E1ABAA /* ViewInspector */; };
2526
AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC33E2D0061F200E1ABAA /* FluidMenuBarExtra */; };
2627
AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC4CE2D00A4B700E1ABAA /* KeychainAccess */; };
28+
AA8EECF72D3A22320049DD09 /* SettingsAccess in Frameworks */ = {isa = PBXBuildFile; productRef = AA8EECF62D3A22320049DD09 /* SettingsAccess */; };
2729
/* End PBXBuildFile section */
2830

2931
/* Begin PBXContainerItemProxy section */
@@ -229,7 +231,9 @@
229231
files = (
230232
AA3B40A42D2FC8560099996A /* CoderSDK.framework in Frameworks */,
231233
AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */,
234+
AA2C690F2D34F6920059AFAF /* LaunchAtLogin in Frameworks */,
232235
AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */,
236+
AA8EECF72D3A22320049DD09 /* SettingsAccess in Frameworks */,
233237
);
234238
runOnlyForDeploymentPostprocessing = 0;
235239
};
@@ -368,7 +372,7 @@
368372
buildRules = (
369373
);
370374
dependencies = (
371-
AA8BC33C2D0060E700E1ABAA /* PBXTargetDependency */,
375+
AA2C698C2D354A800059AFAF /* PBXTargetDependency */,
372376
9616793C2CFF117300B2B6DF /* PBXTargetDependency */,
373377
AA3B40A32D2FC8560099996A /* PBXTargetDependency */,
374378
);
@@ -379,6 +383,8 @@
379383
packageProductDependencies = (
380384
AA8BC33E2D0061F200E1ABAA /* FluidMenuBarExtra */,
381385
AA8BC4CE2D00A4B700E1ABAA /* KeychainAccess */,
386+
AA2C690E2D34F6920059AFAF /* LaunchAtLogin */,
387+
AA8EECF62D3A22320049DD09 /* SettingsAccess */,
382388
);
383389
productName = "Coder Desktop";
384390
productReference = 961678FC2CFF100D00B2B6DF /* Coder Desktop.app */;
@@ -395,6 +401,7 @@
395401
buildRules = (
396402
);
397403
dependencies = (
404+
AA2C698E2D354A840059AFAF /* PBXTargetDependency */,
398405
961679112CFF100E00B2B6DF /* PBXTargetDependency */,
399406
AA3B40BA2D2FDA5C0099996A /* PBXTargetDependency */,
400407
);
@@ -421,6 +428,7 @@
421428
buildRules = (
422429
);
423430
dependencies = (
431+
AA2C69902D354A880059AFAF /* PBXTargetDependency */,
424432
9616791B2CFF100E00B2B6DF /* PBXTargetDependency */,
425433
);
426434
fileSystemSynchronizedGroups = (
@@ -445,6 +453,7 @@
445453
buildRules = (
446454
);
447455
dependencies = (
456+
AA2C69922D354A8B0059AFAF /* PBXTargetDependency */,
448457
AA3B3DD02D2D249F0099996A /* PBXTargetDependency */,
449458
);
450459
fileSystemSynchronizedGroups = (
@@ -469,6 +478,7 @@
469478
buildRules = (
470479
);
471480
dependencies = (
481+
AA2C69942D354A8E0059AFAF /* PBXTargetDependency */,
472482
AA3B40C32D2FE7760099996A /* PBXTargetDependency */,
473483
);
474484
fileSystemSynchronizedGroups = (
@@ -494,6 +504,7 @@
494504
buildRules = (
495505
);
496506
dependencies = (
507+
AA2C69962D354A910059AFAF /* PBXTargetDependency */,
497508
AA3B3DAB2D2D23860099996A /* PBXTargetDependency */,
498509
AA3B3DAD2D2D23860099996A /* PBXTargetDependency */,
499510
);
@@ -520,6 +531,7 @@
520531
buildRules = (
521532
);
522533
dependencies = (
534+
AA2C69982D354A940059AFAF /* PBXTargetDependency */,
523535
);
524536
fileSystemSynchronizedGroups = (
525537
AA3B40922D2FC8560099996A /* CoderSDK */,
@@ -542,6 +554,7 @@
542554
buildRules = (
543555
);
544556
dependencies = (
557+
AA2C699A2D354A970059AFAF /* PBXTargetDependency */,
545558
AA3B409B2D2FC8560099996A /* PBXTargetDependency */,
546559
AA3B409D2D2FC8560099996A /* PBXTargetDependency */,
547560
);
@@ -607,11 +620,13 @@
607620
minimizedProjectReferenceProxies = 1;
608621
packageReferences = (
609622
AA8BC3372D00609700E1ABAA /* XCRemoteSwiftPackageReference "ViewInspector" */,
610-
AA8BC33A2D0060C500E1ABAA /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */,
611623
AA8BC33D2D0061F200E1ABAA /* XCRemoteSwiftPackageReference "fluid-menu-bar-extra" */,
612624
AA8BC4CD2D00A4B700E1ABAA /* XCRemoteSwiftPackageReference "KeychainAccess" */,
613625
961679512CFF207900B2B6DF /* XCRemoteSwiftPackageReference "swift-protobuf" */,
614626
AA3B3E8A2D2E0FE10099996A /* XCRemoteSwiftPackageReference "Mocker" */,
627+
AA2C690D2D34F6920059AFAF /* XCRemoteSwiftPackageReference "LaunchAtLogin-modern" */,
628+
AA2C698A2D354A600059AFAF /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */,
629+
AA8EECF52D3A22320049DD09 /* XCRemoteSwiftPackageReference "SettingsAccess" */,
615630
);
616631
preferredProjectObjectVersion = 77;
617632
productRefGroup = 961678FD2CFF100D00B2B6DF /* Products */;
@@ -764,6 +779,38 @@
764779
target = 9616792F2CFF117300B2B6DF /* VPN */;
765780
targetProxy = 9616793B2CFF117300B2B6DF /* PBXContainerItemProxy */;
766781
};
782+
AA2C698C2D354A800059AFAF /* PBXTargetDependency */ = {
783+
isa = PBXTargetDependency;
784+
productRef = AA2C698B2D354A800059AFAF /* SwiftLintBuildToolPlugin */;
785+
};
786+
AA2C698E2D354A840059AFAF /* PBXTargetDependency */ = {
787+
isa = PBXTargetDependency;
788+
productRef = AA2C698D2D354A840059AFAF /* SwiftLintBuildToolPlugin */;
789+
};
790+
AA2C69902D354A880059AFAF /* PBXTargetDependency */ = {
791+
isa = PBXTargetDependency;
792+
productRef = AA2C698F2D354A880059AFAF /* SwiftLintBuildToolPlugin */;
793+
};
794+
AA2C69922D354A8B0059AFAF /* PBXTargetDependency */ = {
795+
isa = PBXTargetDependency;
796+
productRef = AA2C69912D354A8B0059AFAF /* SwiftLintBuildToolPlugin */;
797+
};
798+
AA2C69942D354A8E0059AFAF /* PBXTargetDependency */ = {
799+
isa = PBXTargetDependency;
800+
productRef = AA2C69932D354A8E0059AFAF /* SwiftLintBuildToolPlugin */;
801+
};
802+
AA2C69962D354A910059AFAF /* PBXTargetDependency */ = {
803+
isa = PBXTargetDependency;
804+
productRef = AA2C69952D354A910059AFAF /* SwiftLintBuildToolPlugin */;
805+
};
806+
AA2C69982D354A940059AFAF /* PBXTargetDependency */ = {
807+
isa = PBXTargetDependency;
808+
productRef = AA2C69972D354A940059AFAF /* SwiftLintBuildToolPlugin */;
809+
};
810+
AA2C699A2D354A970059AFAF /* PBXTargetDependency */ = {
811+
isa = PBXTargetDependency;
812+
productRef = AA2C69992D354A970059AFAF /* SwiftLintBuildToolPlugin */;
813+
};
767814
AA3B3DAB2D2D23860099996A /* PBXTargetDependency */ = {
768815
isa = PBXTargetDependency;
769816
target = AA3B3DA02D2D23860099996A /* VPNLib */;
@@ -804,10 +851,6 @@
804851
target = AA3B40902D2FC8560099996A /* CoderSDK */;
805852
targetProxy = AA3B40C22D2FE7760099996A /* PBXContainerItemProxy */;
806853
};
807-
AA8BC33C2D0060E700E1ABAA /* PBXTargetDependency */ = {
808-
isa = PBXTargetDependency;
809-
productRef = AA8BC33B2D0060E700E1ABAA /* SwiftLintBuildToolPlugin */;
810-
};
811854
/* End PBXTargetDependency section */
812855

813856
/* Begin XCBuildConfiguration section */
@@ -1446,6 +1489,22 @@
14461489
version = 1.28.2;
14471490
};
14481491
};
1492+
AA2C690D2D34F6920059AFAF /* XCRemoteSwiftPackageReference "LaunchAtLogin-modern" */ = {
1493+
isa = XCRemoteSwiftPackageReference;
1494+
repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-modern";
1495+
requirement = {
1496+
kind = exactVersion;
1497+
version = 1.1.0;
1498+
};
1499+
};
1500+
AA2C698A2D354A600059AFAF /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */ = {
1501+
isa = XCRemoteSwiftPackageReference;
1502+
repositoryURL = "https://github.com/SimplyDanny/SwiftLintPlugins";
1503+
requirement = {
1504+
kind = upToNextMajorVersion;
1505+
minimumVersion = 0.58.0;
1506+
};
1507+
};
14491508
AA3B3E8A2D2E0FE10099996A /* XCRemoteSwiftPackageReference "Mocker" */ = {
14501509
isa = XCRemoteSwiftPackageReference;
14511510
repositoryURL = "https://github.com/WeTransfer/Mocker";
@@ -1462,14 +1521,6 @@
14621521
minimumVersion = 0.10.0;
14631522
};
14641523
};
1465-
AA8BC33A2D0060C500E1ABAA /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */ = {
1466-
isa = XCRemoteSwiftPackageReference;
1467-
repositoryURL = "https://github.com/SimplyDanny/SwiftLintPlugins";
1468-
requirement = {
1469-
kind = upToNextMajorVersion;
1470-
minimumVersion = 0.57.1;
1471-
};
1472-
};
14731524
AA8BC33D2D0061F200E1ABAA /* XCRemoteSwiftPackageReference "fluid-menu-bar-extra" */ = {
14741525
isa = XCRemoteSwiftPackageReference;
14751526
repositoryURL = "https://github.com/lfroms/fluid-menu-bar-extra";
@@ -1486,9 +1537,62 @@
14861537
kind = branch;
14871538
};
14881539
};
1540+
AA8EECF52D3A22320049DD09 /* XCRemoteSwiftPackageReference "SettingsAccess" */ = {
1541+
isa = XCRemoteSwiftPackageReference;
1542+
repositoryURL = "https://github.com/orchetect/SettingsAccess";
1543+
requirement = {
1544+
kind = upToNextMajorVersion;
1545+
minimumVersion = 2.1.0;
1546+
};
1547+
};
14891548
/* End XCRemoteSwiftPackageReference section */
14901549

14911550
/* Begin XCSwiftPackageProductDependency section */
1551+
AA2C690E2D34F6920059AFAF /* LaunchAtLogin */ = {
1552+
isa = XCSwiftPackageProductDependency;
1553+
package = AA2C690D2D34F6920059AFAF /* XCRemoteSwiftPackageReference "LaunchAtLogin-modern" */;
1554+
productName = LaunchAtLogin;
1555+
};
1556+
AA2C698B2D354A800059AFAF /* SwiftLintBuildToolPlugin */ = {
1557+
isa = XCSwiftPackageProductDependency;
1558+
package = AA2C698A2D354A600059AFAF /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
1559+
productName = "plugin:SwiftLintBuildToolPlugin";
1560+
};
1561+
AA2C698D2D354A840059AFAF /* SwiftLintBuildToolPlugin */ = {
1562+
isa = XCSwiftPackageProductDependency;
1563+
package = AA2C698A2D354A600059AFAF /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
1564+
productName = "plugin:SwiftLintBuildToolPlugin";
1565+
};
1566+
AA2C698F2D354A880059AFAF /* SwiftLintBuildToolPlugin */ = {
1567+
isa = XCSwiftPackageProductDependency;
1568+
package = AA2C698A2D354A600059AFAF /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
1569+
productName = "plugin:SwiftLintBuildToolPlugin";
1570+
};
1571+
AA2C69912D354A8B0059AFAF /* SwiftLintBuildToolPlugin */ = {
1572+
isa = XCSwiftPackageProductDependency;
1573+
package = AA2C698A2D354A600059AFAF /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
1574+
productName = "plugin:SwiftLintBuildToolPlugin";
1575+
};
1576+
AA2C69932D354A8E0059AFAF /* SwiftLintBuildToolPlugin */ = {
1577+
isa = XCSwiftPackageProductDependency;
1578+
package = AA2C698A2D354A600059AFAF /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
1579+
productName = "plugin:SwiftLintBuildToolPlugin";
1580+
};
1581+
AA2C69952D354A910059AFAF /* SwiftLintBuildToolPlugin */ = {
1582+
isa = XCSwiftPackageProductDependency;
1583+
package = AA2C698A2D354A600059AFAF /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
1584+
productName = "plugin:SwiftLintBuildToolPlugin";
1585+
};
1586+
AA2C69972D354A940059AFAF /* SwiftLintBuildToolPlugin */ = {
1587+
isa = XCSwiftPackageProductDependency;
1588+
package = AA2C698A2D354A600059AFAF /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
1589+
productName = "plugin:SwiftLintBuildToolPlugin";
1590+
};
1591+
AA2C69992D354A970059AFAF /* SwiftLintBuildToolPlugin */ = {
1592+
isa = XCSwiftPackageProductDependency;
1593+
package = AA2C698A2D354A600059AFAF /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
1594+
productName = "plugin:SwiftLintBuildToolPlugin";
1595+
};
14921596
AA3B3DBE2D2D23AB0099996A /* SwiftProtobuf */ = {
14931597
isa = XCSwiftPackageProductDependency;
14941598
package = 961679512CFF207900B2B6DF /* XCRemoteSwiftPackageReference "swift-protobuf" */;
@@ -1519,11 +1623,6 @@
15191623
package = AA8BC3372D00609700E1ABAA /* XCRemoteSwiftPackageReference "ViewInspector" */;
15201624
productName = ViewInspector;
15211625
};
1522-
AA8BC33B2D0060E700E1ABAA /* SwiftLintBuildToolPlugin */ = {
1523-
isa = XCSwiftPackageProductDependency;
1524-
package = AA8BC33A2D0060C500E1ABAA /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
1525-
productName = "plugin:SwiftLintBuildToolPlugin";
1526-
};
15271626
AA8BC33E2D0061F200E1ABAA /* FluidMenuBarExtra */ = {
15281627
isa = XCSwiftPackageProductDependency;
15291628
package = AA8BC33D2D0061F200E1ABAA /* XCRemoteSwiftPackageReference "fluid-menu-bar-extra" */;
@@ -1534,6 +1633,11 @@
15341633
package = AA8BC4CD2D00A4B700E1ABAA /* XCRemoteSwiftPackageReference "KeychainAccess" */;
15351634
productName = KeychainAccess;
15361635
};
1636+
AA8EECF62D3A22320049DD09 /* SettingsAccess */ = {
1637+
isa = XCSwiftPackageProductDependency;
1638+
package = AA8EECF52D3A22320049DD09 /* XCRemoteSwiftPackageReference "SettingsAccess" */;
1639+
productName = SettingsAccess;
1640+
};
15371641
/* End XCSwiftPackageProductDependency section */
15381642
};
15391643
rootObject = 961678F42CFF100D00B2B6DF /* Project object */;

‎Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 21 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Coder Desktop/Coder Desktop/About.swift

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,7 @@ enum About {
3232

3333
@MainActor
3434
static func open() {
35-
#if compiler(>=5.9) && canImport(AppKit)
36-
if #available(macOS 14, *) {
37-
NSApp.activate()
38-
} else {
39-
NSApp.activate(ignoringOtherApps: true)
40-
}
41-
#else
42-
NSApp.activate(ignoringOtherApps: true)
43-
#endif
35+
appActivate()
4436
NSApp.orderFrontStandardAboutPanel(options: [
4537
.credits: credits,
4638
])

‎Coder Desktop/Coder Desktop/Coder_DesktopApp.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import FluidMenuBarExtra
2+
import SwiftData
23
import SwiftUI
34

45
@main
@@ -11,7 +12,12 @@ struct DesktopApp: App {
1112
EmptyView()
1213
}
1314
Window("Sign In", id: Windows.login.rawValue) {
14-
LoginForm<PreviewSession>().environmentObject(appDelegate.session)
15+
LoginForm<SecureSession>().environmentObject(appDelegate.session)
16+
}
17+
.windowResizability(.contentSize)
18+
SwiftUI.Settings { SettingsView<PreviewVPN>()
19+
.environmentObject(appDelegate.vpn)
20+
.environmentObject(appDelegate.settings)
1521
}
1622
.windowResizability(.contentSize)
1723
}
@@ -22,10 +28,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
2228
private var menuBarExtra: FluidMenuBarExtra?
2329
let vpn: PreviewVPN
2430
let session: PreviewSession
31+
let settings: Settings
2532

2633
override init() {
27-
// TODO: Replace with real implementations
34+
// TODO: Replace with real implementation
2835
vpn = PreviewVPN()
36+
settings = Settings()
2937
session = PreviewSession()
3038
}
3139

@@ -34,10 +42,24 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3442
VPNMenu<PreviewVPN, PreviewSession>().frame(width: 256)
3543
.environmentObject(self.vpn)
3644
.environmentObject(self.session)
45+
.environmentObject(self.settings)
3746
}
3847
}
3948

4049
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
4150
false
4251
}
4352
}
53+
54+
@MainActor
55+
func appActivate() {
56+
#if compiler(>=5.9) && canImport(AppKit)
57+
if #available(macOS 14, *) {
58+
NSApp.activate()
59+
} else {
60+
NSApp.activate(ignoringOtherApps: true)
61+
}
62+
#else
63+
NSApp.activate(ignoringOtherApps: true)
64+
#endif
65+
}

‎Coder Desktop/Coder Desktop/Session.swift renamed to ‎Coder Desktop/Coder Desktop/State.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import CoderSDK
12
import Foundation
23
import KeychainAccess
34
import NetworkExtension
5+
import SwiftUI
46

57
protocol Session: ObservableObject {
68
var hasSession: Bool { get }
@@ -89,3 +91,47 @@ class SecureSession: ObservableObject, Session {
8991
static let sessionToken = "sessionToken"
9092
}
9193
}
94+
95+
class Settings: ObservableObject {
96+
let store: UserDefaults
97+
@AppStorage(Keys.useLiteralHeaders) var useLiteralHeaders = false
98+
99+
@Published var literalHeaders: [LiteralHeader] {
100+
didSet {
101+
try? store.set(JSONEncoder().encode(literalHeaders), forKey: Keys.literalHeaders)
102+
}
103+
}
104+
105+
init(store: UserDefaults = UserDefaults.standard) {
106+
self.store = store
107+
_literalHeaders = Published(
108+
initialValue: UserDefaults.standard.data(
109+
forKey: Keys.literalHeaders
110+
).flatMap { try? JSONDecoder().decode([LiteralHeader].self, from: $0) } ?? []
111+
)
112+
}
113+
114+
enum Keys {
115+
static let useLiteralHeaders = "UseLiteralHeaders"
116+
static let literalHeaders = "LiteralHeaders"
117+
}
118+
}
119+
120+
struct LiteralHeader: Hashable, Identifiable, Equatable, Codable {
121+
var header: String
122+
var value: String
123+
var id: String {
124+
"\(header):\(value)"
125+
}
126+
127+
init(header: String, value: String) {
128+
self.header = header
129+
self.value = value
130+
}
131+
}
132+
133+
extension LiteralHeader {
134+
func toSDKHeader() -> HTTPHeader {
135+
return .init(header: header, value: value)
136+
}
137+
}

‎Coder Desktop/Coder Desktop/Views/LoginForm.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import CoderSDK
2+
import SwiftData
23
import SwiftUI
34

45
struct LoginForm<S: Session>: View {
56
@EnvironmentObject var session: S
7+
@EnvironmentObject var settings: Settings
68
@Environment(\.dismiss) private var dismiss
79

810
@State private var baseAccessURL: String = ""
@@ -68,7 +70,7 @@ struct LoginForm<S: Session>: View {
6870
}
6971
loading = true
7072
defer { loading = false }
71-
let client = Client(url: url, token: sessionToken)
73+
let client = Client(url: url, token: sessionToken, headers: settings.literalHeaders.map { $0.toSDKHeader() })
7274
do {
7375
_ = try await client.user("me")
7476
} catch {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import LaunchAtLogin
2+
import SwiftUI
3+
4+
struct GeneralTab: View {
5+
var body: some View {
6+
Form {
7+
Section {
8+
LaunchAtLogin.Toggle("Launch at Login")
9+
}
10+
}.formStyle(.grouped)
11+
}
12+
}
13+
14+
#Preview {
15+
GeneralTab()
16+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import SwiftData
2+
import SwiftUI
3+
4+
struct LiteralHeaderModal: View {
5+
var existingHeader: LiteralHeader?
6+
7+
@EnvironmentObject var settings: Settings
8+
@Environment(\.dismiss) private var dismiss
9+
10+
@State private var header: String = ""
11+
@State private var value: String = ""
12+
13+
var body: some View {
14+
VStack(spacing: 0) {
15+
Form {
16+
Section {
17+
TextField("Header", text: $header)
18+
TextField("Value", text: $value)
19+
}
20+
}.formStyle(.grouped).scrollDisabled(true).padding(.horizontal)
21+
Divider()
22+
HStack {
23+
Spacer()
24+
Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
25+
Button(existingHeader == nil ? "Add" : "Save", action: submit)
26+
.keyboardShortcut(.defaultAction)
27+
}.padding(20)
28+
}.onAppear {
29+
if let existingHeader {
30+
self.header = existingHeader.header
31+
self.value = existingHeader.value
32+
}
33+
}
34+
}
35+
36+
func submit() {
37+
defer { dismiss() }
38+
if let existingHeader {
39+
settings.literalHeaders.removeAll { $0 == existingHeader }
40+
}
41+
let newHeader = LiteralHeader(header: header, value: value)
42+
if !settings.literalHeaders.contains(newHeader) {
43+
settings.literalHeaders.append(newHeader)
44+
}
45+
}
46+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import SwiftData
2+
import SwiftUI
3+
4+
struct LiteralHeadersSection<VPN: VPNService>: View {
5+
@EnvironmentObject var vpn: VPN
6+
@EnvironmentObject var settings: Settings
7+
8+
@State private var selectedHeader: LiteralHeader.ID?
9+
@State private var editingHeader: LiteralHeader?
10+
@State private var addingNewHeader = false
11+
12+
let inspection = Inspection<Self>()
13+
14+
var body: some View {
15+
Section {
16+
Toggle(isOn: settings.$useLiteralHeaders) {
17+
Text("HTTP Headers")
18+
Text("When enabled, these headers will be included on all outgoing HTTP requests.")
19+
if vpn.state != .disabled { Text("Cannot be modified while Coder VPN is enabled.") }
20+
}
21+
.controlSize(.large)
22+
23+
Table(settings.literalHeaders, selection: $selectedHeader) {
24+
TableColumn("Header", value: \.header)
25+
TableColumn("Value", value: \.value)
26+
}.opacity(settings.useLiteralHeaders ? 1 : 0.5)
27+
.frame(minWidth: 400, minHeight: 200)
28+
.padding(.bottom, 25)
29+
.overlay(alignment: .bottom) {
30+
VStack(alignment: .leading, spacing: 0) {
31+
Divider()
32+
HStack(spacing: 0) {
33+
Button {
34+
addingNewHeader = true
35+
} label: {
36+
Image(systemName: "plus")
37+
.frame(width: 24, height: 24)
38+
}
39+
Divider()
40+
Button {
41+
settings.literalHeaders.removeAll { $0.id == selectedHeader }
42+
selectedHeader = nil
43+
} label: {
44+
Image(systemName: "minus")
45+
.frame(width: 24, height: 24)
46+
}.disabled(selectedHeader == nil)
47+
}
48+
.buttonStyle(.borderless)
49+
}
50+
.background(.primary.opacity(0.04))
51+
.fixedSize(horizontal: false, vertical: true)
52+
}
53+
.background(.primary.opacity(0.04))
54+
.contextMenu(forSelectionType: LiteralHeader.ID.self, menu: { _ in },
55+
primaryAction: { selectedHeaders in
56+
if let firstHeader = selectedHeaders.first {
57+
editingHeader = settings.literalHeaders.first(where: { $0.id == firstHeader })
58+
}
59+
})
60+
.disabled(!settings.useLiteralHeaders)
61+
}
62+
.sheet(isPresented: $addingNewHeader) {
63+
LiteralHeaderModal()
64+
}
65+
.sheet(item: $editingHeader) { header in
66+
LiteralHeaderModal(existingHeader: header)
67+
}.onTapGesture {
68+
selectedHeader = nil
69+
}.disabled(vpn.state != .disabled)
70+
.onReceive(inspection.notice) { self.inspection.visit(self, $0) } // ViewInspector
71+
}
72+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import SwiftData
2+
import SwiftUI
3+
4+
struct NetworkTab<VPN: VPNService>: View {
5+
var body: some View {
6+
Form {
7+
LiteralHeadersSection<VPN>()
8+
}
9+
.formStyle(.grouped)
10+
}
11+
}
12+
13+
#Preview {
14+
NetworkTab<PreviewVPN>()
15+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import SwiftUI
2+
3+
struct SettingsView<VPN: VPNService>: View {
4+
@AppStorage("SettingsSelectedIndex") private var selection: SettingsTab = .general
5+
6+
var body: some View {
7+
TabView(selection: $selection) {
8+
GeneralTab()
9+
.tabItem {
10+
Label("General", systemImage: "gearshape")
11+
}.tag(SettingsTab.general)
12+
NetworkTab<VPN>()
13+
.tabItem {
14+
Label("Network", systemImage: "dot.radiowaves.left.and.right")
15+
}.tag(SettingsTab.network)
16+
}.frame(width: 600)
17+
.frame(maxHeight: 500)
18+
.scrollContentBackground(.hidden)
19+
.fixedSize()
20+
}
21+
}
22+
23+
enum SettingsTab: Int {
24+
case general
25+
case network
26+
}

‎Coder Desktop/Coder Desktop/Views/Util.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Combine
2+
import SwiftUI
23

34
// This is required for inspecting stateful views
45
final class Inspection<V> {

‎Coder Desktop/Coder Desktop/Views/VPNMenu.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import SettingsAccess
12
import SwiftUI
23

34
struct VPNMenu<VPN: VPNService, S: Session>: View {
@@ -21,6 +22,8 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
2122
)) {
2223
Text("CoderVPN")
2324
.frame(maxWidth: .infinity, alignment: .leading)
25+
.font(.body.bold())
26+
.foregroundColor(.primary)
2427
}.toggleStyle(.switch)
2528
.disabled(vpnDisabled)
2629
}
@@ -50,6 +53,11 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
5053
TrayDivider()
5154
}
5255
AuthButton<VPN, S>()
56+
SettingsLink {
57+
ButtonRowView { Text("Settings") }
58+
} preAction: {} postAction: {
59+
appActivate()
60+
}.buttonStyle(.plain)
5361
Button {
5462
About.open()
5563
} label: {

‎Coder Desktop/Coder Desktop/Windows.swift

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,7 @@ enum Windows: String {
88
extension OpenWindowAction {
99
// Type-safe wrapper for opening windows that also focuses the new window
1010
func callAsFunction(id: Windows) {
11-
#if compiler(>=5.9) && canImport(AppKit)
12-
if #available(macOS 14, *) {
13-
NSApp.activate()
14-
} else {
15-
NSApp.activate(ignoringOtherApps: true)
16-
}
17-
#else
18-
NSApp.activate(ignoringOtherApps: true)
19-
#endif
11+
appActivate()
2012
callAsFunction(id: id.rawValue)
2113
// The arranging behaviour is flakey without this
2214
NSApp.arrangeInFront(nil)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
@testable import Coder_Desktop
2+
import SwiftUI
3+
import Testing
4+
import ViewInspector
5+
6+
@MainActor
7+
@Suite(.timeLimit(.minutes(1)))
8+
struct LiteralHeadersSettingTests {
9+
let vpn: MockVPNService
10+
let sut: LiteralHeadersSection<MockVPNService>
11+
let view: any View
12+
13+
init() {
14+
vpn = MockVPNService()
15+
sut = LiteralHeadersSection<MockVPNService>()
16+
let store = UserDefaults(suiteName: #file)!
17+
store.removePersistentDomain(forName: #file)
18+
view = sut.environmentObject(vpn).environmentObject(Settings(store: store))
19+
}
20+
21+
@Test
22+
func testToggleDisabledWhenVPNEnabled() async throws {
23+
vpn.state = .connected
24+
25+
try await ViewHosting.host(view) {
26+
try await sut.inspection.inspect { view in
27+
let toggle = try view.find(ViewType.Toggle.self)
28+
#expect(toggle.isDisabled())
29+
#expect(throws: Never.self) { try toggle.labelView().find(text: "HTTP Headers") }
30+
}
31+
}
32+
}
33+
34+
@Test
35+
func testToggleEnabledWhenVPNDisabled() async throws {
36+
vpn.state = .disabled
37+
38+
try await ViewHosting.host(view) {
39+
try await sut.inspection.inspect { view in
40+
let toggle = try view.find(ViewType.Toggle.self)
41+
#expect(!toggle.isDisabled())
42+
#expect(throws: Never.self) { try toggle.labelView().find(text: "HTTP Headers") }
43+
}
44+
}
45+
}
46+
47+
// TODO: More tests, ViewInspector cannot currently inspect Tables
48+
}

‎Coder Desktop/Coder DesktopTests/LoginFormTests.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ struct LoginTests {
1515
init() {
1616
session = MockSession()
1717
sut = LoginForm<MockSession>()
18-
view = sut.environmentObject(session)
18+
let store = UserDefaults(suiteName: #file)!
19+
store.removePersistentDomain(forName: #file)
20+
view = sut.environmentObject(session).environmentObject(Settings(store: store))
1921
}
2022

2123
@Test
@@ -70,12 +72,11 @@ struct LoginTests {
7072

7173
@Test
7274
func testFailedAuthentication() async throws {
73-
let login = LoginForm<MockSession>()
7475
let url = URL(string: "https://testFailedAuthentication.com")!
7576
Mock(url: url.appendingPathComponent("/api/v2/users/me"), statusCode: 401, data: [.get: Data()]).register()
7677

77-
try await ViewHosting.host(login.environmentObject(session)) {
78-
try await login.inspection.inspect { view in
78+
try await ViewHosting.host(view) {
79+
try await sut.inspection.inspect { view in
7980
try view.find(ViewType.TextField.self).setInput(url.absoluteString)
8081
try view.find(button: "Next").tap()
8182
#expect(throws: Never.self) { try view.find(text: "Session Token") }

0 commit comments

Comments
 (0)
Please sign in to comment.