Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added
- **Crontab** — browse, create, edit, enable/disable, and delete user and system (`/etc/crontab`) cron jobs from the sidebar (similar to Launch Agents). System writes use the same admin password prompt as system LaunchAgents.
- **Homebrew Services** — list, start, stop, and restart `brew services` for user and system scopes; system operations use the same `PrivilegeService` admin prompt as system LaunchAgents.

<!-- Add changes here as you develop. Run ./scripts/release.sh patch to publish. -->

## [1.7.0] - 2026-07-06
Expand Down
65 changes: 61 additions & 4 deletions LaunchManager/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ import SwiftUI

struct ContentView: View {
@StateObject private var store = AgentStore()
@StateObject private var crontabStore = CrontabStore()
@StateObject private var homebrewStore = HomebrewServiceStore()
@StateObject private var serviceStore = ServiceStore()
@StateObject private var updateChecker = UpdateChecker()
@Environment(\.scenePhase) private var scenePhase
@State private var selection: SidebarSelection? = .agents
@State private var showingNewAgent = false
@State private var showingNewFromXml = false
@State private var newAgentScope: LaunchItem.Scope = .userAgent
@State private var showingNewCron = false
@State private var newCronScope: CrontabScope = .user
@State private var serviceLaunchDraft: LaunchAgentDraft?
@State private var searchText = ""
@State private var errorMessage: String?
Expand All @@ -25,6 +29,14 @@ struct ContentView: View {
@State private var showAbout = false
@State private var pendingUpdateCheck = false

private var isHomebrewView: Bool {
selection == .homebrew
}

private var isCrontabView: Bool {
selection == .crontab
}

private var isLoginItemsGuide: Bool {
selection == .loginItems
}
Expand All @@ -39,18 +51,25 @@ struct ContentView: View {

var body: some View {
NavigationSplitView {
SidebarView(selection: $selection, store: store)
SidebarView(
selection: $selection,
store: store,
crontabStore: crontabStore,
homebrewStore: homebrewStore
)
} detail: {
detailView
}
.modifier(ConditionalSearchable(
isEnabled: isAgentsView && !isLoginItemsGuide && !isServicesView,
isEnabled: (isAgentsView || isCrontabView || isHomebrewView) && !isLoginItemsGuide && !isServicesView,
text: $searchText,
prompt: "搜索 Label 或路径"
prompt: searchPrompt
))
.onAppear {
store.refresh()
store.startWatching()
crontabStore.refresh()
homebrewStore.refresh()
serviceStore.startPolling(isActive: scenePhase == .active)
if !hasSeenOnboarding {
showOnboarding = true
Expand All @@ -70,11 +89,17 @@ struct ContentView: View {
serviceStore.startPolling(isActive: phase == .active)
if phase == .active {
store.refresh()
crontabStore.refresh()
homebrewStore.refresh()
}
}
.onChange(of: selection) { _, newSelection in
if newSelection == .services {
serviceStore.refreshNow()
} else if newSelection == .crontab {
crontabStore.refresh()
} else if newSelection == .homebrew {
homebrewStore.refresh()
}
}
.sheet(isPresented: $showOnboarding) {
Expand Down Expand Up @@ -117,6 +142,14 @@ struct ContentView: View {
draft: draft
)
}
.sheet(isPresented: $showingNewCron) {
EditCronSheet(
existingJob: nil,
scope: newCronScope,
store: crontabStore,
errorMessage: $errorMessage
)
}
.alert("错误", isPresented: Binding(
get: { errorMessage != nil },
set: { if !$0 { errorMessage = nil } }
Expand Down Expand Up @@ -153,6 +186,20 @@ struct ContentView: View {
errorMessage: $errorMessage,
onCreateLaunchAgent: { draft in serviceLaunchDraft = draft }
)
case .crontab:
CronListView(
store: crontabStore,
searchText: searchText,
newCronScope: $newCronScope,
showingNewCron: $showingNewCron,
errorMessage: $errorMessage
)
case .homebrew:
HomebrewServiceListView(
store: homebrewStore,
searchText: searchText,
errorMessage: $errorMessage
)
case .loginItems:
LoginItemsGuideView()
case .agents, .none:
Expand All @@ -166,9 +213,19 @@ struct ContentView: View {
)
}
}

private var searchPrompt: LocalizedStringKey {
if isCrontabView {
return "搜索命令或计划"
}
if isHomebrewView {
return "搜索服务名或 Label"
}
return "搜索 Label 或路径"
}
}

/// Applies `.searchable` only when listing launchd agents (hidden on Login Items guide).
/// Applies `.searchable` when listing launchd agents, crontab jobs, or Homebrew services.
private struct ConditionalSearchable: ViewModifier {
let isEnabled: Bool
@Binding var text: String
Expand Down
Loading