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
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
## 1.0.0 - 2026-06-04

- Initial GitHub-ready release.
- Added real `/etc/hosts` apply and revert flow through a bundled privileged helper.
- Added managed-block replacement, backups, and DNS cache flushing.
- Added SwiftUI app shell with sidebar contexts, hosts editor, settings, menu bar access, import/export, and notarization scripts.
- DNS Proxy Network Extension for applying host mappings without editing system files.
- App Group shared configuration between main app and DNS proxy extension.
- SwiftUI app shell with sidebar contexts, hosts editor, settings, menu bar access, import/export, and notarization scripts.
3 changes: 1 addition & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,4 @@ Run the app locally with:

## Safety

Locale edits `/etc/hosts`, so changes to `SystemApplyService` should be reviewed carefully. The app must preserve all content outside its managed block and keep creating backups before writes.

Locale applies DNS mappings through a sandboxed DNS Proxy Network Extension. Changes to `SystemApplyService`, `DNSProxyController`, or `DNSProxyProvider` should be reviewed carefully. The app must not introduce direct `/etc/hosts` edits, privileged helpers, or `osascript` calls.
5 changes: 5 additions & 0 deletions Sources/LocaleApp/LocaleStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ final class LocaleStore: ObservableObject {
copy.isActive = false
copy.lastActivated = nil
copy.createdAt = Date()
copy.hosts = context.hosts.map { host in
var newHost = host
newHost.id = UUID()
return newHost
}
if let idx = contexts.firstIndex(where: { $0.id == context.id }) {
contexts.insert(copy, at: idx + 1)
} else {
Expand Down
4 changes: 3 additions & 1 deletion Sources/LocaleApp/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@ struct NetworkContext: Identifiable, Codable, Equatable, Sendable {
var activeHostCount: Int { hosts.filter(\.isEnabled).count }
var disabledHostCount: Int { hosts.filter { !$0.isEnabled }.count }

static let homeContextID = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!

static let homeContext = NetworkContext(
id: UUID(),
id: homeContextID,
name: "Home",
color: .green,
isActive: true,
Expand Down
9 changes: 6 additions & 3 deletions Sources/LocaleApp/SidebarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ struct SidebarContextRow: View {

@State private var isActivePulse = false

@EnvironmentObject var store: LocaleStore

var body: some View {
Button(action: onSelect) {
HStack(spacing: 9) {
Expand Down Expand Up @@ -159,10 +161,11 @@ struct SidebarContextRow: View {
if context.isActive { isActivePulse = true }
}
.contextMenu {
Button("Activate") {}
Button("Duplicate") {}
Button("Activate") { store.activateContext(context) }
Button("Duplicate") { store.duplicateContext(context) }
Divider()
Button("Delete", role: .destructive) {}
Button("Delete", role: .destructive) { store.deleteContext(context) }
.disabled(context.id == NetworkContext.homeContextID)
}
}
}
Expand Down
Loading