Skip to content
Closed
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
62 changes: 17 additions & 45 deletions Lucid/Lucid/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,25 @@ struct ContentView: View {

struct DetailView: View {
@Environment(ProcessMonitor.self) var monitor
@State private var searchText = ""
@State private var sortOrder: [KeyPathComparator<LucidProcess>] = [
.init(\.cpuUsage, order: .reverse)
]
@Environment(ProcessStore.self) var processStore
@Environment(FilterState.self) var filterState

@State private var killTarget: LucidProcess?
@State private var multiKillTargets: [LucidProcess] = []
@State private var selection = Set<LucidProcess.ID>()
@State private var killError: String?

private var filterBinding: Binding<FilterCategory> {
// Computed filtered processes - now delegated to FilterState
@MainActor
var filteredProcesses: [LucidProcess] {
filterState.filter(processStore.processes)
}

@MainActor
private var sortOrderBinding: Binding<[KeyPathComparator<LucidProcess>]> {
Binding(
get: { monitor.selectedFilter },
set: { monitor.selectedFilter = $0 }
get: { filterState.sortOrder },
set: { filterState.applySortOrder($0) }
)
}

Expand All @@ -57,46 +63,11 @@ struct DetailView: View {
)
}

var filteredProcesses: [LucidProcess] {
var result = monitor.processes

// Apply filter
switch monitor.selectedFilter {
case .all:
break
case .system:
result = result.filter { $0.safety == .system }
case .user:
result = result.filter { $0.safety == .user }
case .unknown:
result = result.filter { $0.safety == .unknown }
case .port(let port):
result = result.filter { $0.ports.contains(port) }
}

// Apply search
if !searchText.isEmpty {
result = result.filter { process in
process.name.localizedCaseInsensitiveContains(searchText) ||
process.description.localizedCaseInsensitiveContains(searchText)
}
}

// Apply sort
result.sort(using: sortOrder)

return result
}

var body: some View {
VStack(spacing: 0) {
HeaderBar(
processCount: filteredProcesses.count,
searchText: $searchText,
selectedFilter: filterBinding
)
HeaderBar(processCount: filteredProcesses.count)

Table(filteredProcesses, selection: $selection, sortOrder: $sortOrder) {
Table(filteredProcesses, selection: $selection, sortOrder: sortOrderBinding) {
TableColumn("Name", value: \.name) { process in
Text(process.name)
.font(.system(.body, design: .monospaced))
Expand Down Expand Up @@ -224,6 +195,7 @@ struct DetailView: View {
}
}

@MainActor
private func performKill() {
let processesToKill: [LucidProcess]
if let single = killTarget {
Expand Down Expand Up @@ -251,4 +223,4 @@ struct DetailView: View {
Text("Are you sure you want to kill \(multiKillTargets.count) processes?")
}
}
}
}
9 changes: 6 additions & 3 deletions Lucid/Lucid/LucidApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ struct LucidApp: App {
WindowGroup {
ContentView()
.environment(monitor)
.environment(monitor.processStore)
.environment(monitor.statsStore)
.environment(monitor.filterState)
.frame(minWidth: 1100, minHeight: 750)
.onAppear {
monitor.start()
Expand All @@ -25,14 +28,14 @@ struct LucidApp: App {
forName: NSApplication.didResignActiveNotification,
object: nil, queue: .main
) { _ in
monitor.stop()
Task { @MainActor in monitor.stop() }
},
NotificationCenter.default.addObserver(
forName: NSApplication.didBecomeActiveNotification,
object: nil, queue: .main
) { _ in
monitor.start()
Task { @MainActor in monitor.start() }
}
]
}
}
}
90 changes: 90 additions & 0 deletions Lucid/Lucid/Services/FilterState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import SwiftUI
import Observation

@Observable
@MainActor
final class FilterState {
// MARK: - State
var selectedFilter: FilterCategory = .all
var searchText: String = "" {
didSet {
handleSearchTextChange()
}
}
var debouncedSearchText: String = ""
var sortOrder: [KeyPathComparator<LucidProcess>] = [
.init(\.cpuUsage, order: .reverse)
]

// MARK: - Private State
private var debounceTask: Task<Void, Never>?
private let debounceInterval: Duration = .milliseconds(300)

// MARK: - Public Methods

func applyFilter(_ filter: FilterCategory) {
selectedFilter = filter
}

func clearSearch() {
searchText = ""
debouncedSearchText = ""
debounceTask?.cancel()
}

func applySortOrder(_ order: [KeyPathComparator<LucidProcess>]) {
sortOrder = order
}

// MARK: - Filtering

func filter(_ processes: [LucidProcess]) -> [LucidProcess] {
var result = processes

// Apply category filter
switch selectedFilter {
case .all:
break
case .system:
result = result.filter { $0.safety == .system }
case .user:
result = result.filter { $0.safety == .user }
case .unknown:
result = result.filter { $0.safety == .unknown }
case .port(let port):
result = result.filter { $0.ports.contains(port) }
}

// Apply search filter (using debounced value)
if !debouncedSearchText.isEmpty {
result = result.filter { process in
process.name.localizedCaseInsensitiveContains(debouncedSearchText) ||
process.description.localizedCaseInsensitiveContains(debouncedSearchText)
}
}

// Apply sort
result.sort(using: sortOrder)

return result
}

// MARK: - Private Methods

private func handleSearchTextChange() {
debounceTask?.cancel()

debounceTask = Task { [weak self] in
guard let self else { return }

do {
try await Task.sleep(for: self.debounceInterval)
self.debouncedSearchText = self.searchText
} catch is CancellationError {
// Task was cancelled, do nothing
} catch {
// Other errors shouldn't happen with Task.sleep, but handle just in case
}
}
}
}
Loading
Loading