Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions platforms/macos/Sources/Managers/PortForwardManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ extension Defaults.Keys {
static let portForwardShowNotifications = Key<Bool>("portForwardShowNotifications", default: true)
static let customKubectlPath = Key<String?>("customKubectlPath", default: nil)
static let customSocatPath = Key<String?>("customSocatPath", default: nil)
static let customCloudflaredPath = Key<String?>("customCloudflaredPath", default: nil)
}

// MARK: - Port Forward Manager
Expand Down
23 changes: 23 additions & 0 deletions platforms/macos/Sources/Services/CloudflaredService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ actor CloudflaredService {
// MARK: - Dependency Check

nonisolated var cloudflaredPath: String? {
// Check custom path first
if let custom = Defaults[.customCloudflaredPath],
!custom.isEmpty,
FileManager.default.fileExists(atPath: custom) {
return custom
}
let paths = [
"/opt/homebrew/bin/cloudflared",
"/usr/local/bin/cloudflared"
Expand All @@ -25,6 +31,23 @@ actor CloudflaredService {
cloudflaredPath != nil
}

nonisolated var isUsingCustomPath: Bool {
if let custom = Defaults[.customCloudflaredPath],
!custom.isEmpty,
FileManager.default.fileExists(atPath: custom) {
return true
}
return false
}

nonisolated var autoDetectedPath: String? {
let paths = [
"/opt/homebrew/bin/cloudflared",
"/usr/local/bin/cloudflared"
]
return paths.first { FileManager.default.fileExists(atPath: $0) }
}

// MARK: - Handler Management

func setURLHandler(for id: UUID, handler: @escaping @Sendable (String) -> Void) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,112 @@ import Defaults

struct CloudflaredSettingsSection: View {
@Default(.cloudflaredProtocol) private var protocolSelection
@Default(.customCloudflaredPath) private var customPath
@State private var pathInput = ""

private let service = CloudflaredService()

private var effectivePath: String? {
if let custom = customPath, !custom.isEmpty, FileManager.default.fileExists(atPath: custom) {
return custom
}
return service.autoDetectedPath
}

var body: some View {
SettingsGroup("Cloudflare Tunnels", icon: "cloud.fill") {
SettingsRowContainer {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("Tunnel protocol")
.fontWeight(.medium)
Text("Choose how cloudflared connects to Cloudflare (applies to new tunnels)")
.font(.caption)
.foregroundStyle(.secondary)
VStack(spacing: 0) {
SettingsRowContainer {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("Tunnel protocol")
.fontWeight(.medium)
Text("Choose how cloudflared connects to Cloudflare (applies to new tunnels)")
.font(.caption)
.foregroundStyle(.secondary)
}

Spacer()

Picker("", selection: $protocolSelection) {
ForEach(CloudflaredProtocol.allCases, id: \.self) { option in
Text(option.displayName).tag(option)
}
}
.labelsHidden()
.pickerStyle(.segmented)
.frame(width: 160)
}
}

SettingsDivider()

SettingsRowContainer {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("cloudflared path")
.fontWeight(.medium)

Spacer()

Spacer()
if effectivePath != nil {
HStack(spacing: 4) {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
Text("Installed")
.font(.caption)
.foregroundStyle(.secondary)
}
} else {
HStack(spacing: 4) {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.red)
Text("Not found")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}

HStack(spacing: 8) {
TextField("Custom path (leave empty for auto)", text: $pathInput)
.textFieldStyle(.roundedBorder)
.font(.system(.caption, design: .monospaced))
.onAppear {
pathInput = customPath ?? ""
}
.onChange(of: pathInput) { _, newValue in
if newValue.isEmpty {
Defaults[.customCloudflaredPath] = nil
} else {
Defaults[.customCloudflaredPath] = newValue
}
}

if !pathInput.isEmpty {
Button("Clear") {
pathInput = ""
Defaults[.customCloudflaredPath] = nil
}
.buttonStyle(.bordered)
.controlSize(.small)
}
}

Picker("", selection: $protocolSelection) {
ForEach(CloudflaredProtocol.allCases, id: \.self) { option in
Text(option.displayName).tag(option)
if let path = effectivePath {
HStack(spacing: 4) {
Text("Using:")
.font(.caption)
.foregroundStyle(.tertiary)
Text(path)
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.secondary)
Text(service.isUsingCustomPath ? "(custom)" : "(auto)")
.font(.caption2)
.foregroundStyle(service.isUsingCustomPath ? Color.orange : Color.gray)
}
}
}
.labelsHidden()
.pickerStyle(.segmented)
.frame(width: 160)
}
}
}
Expand Down
Loading