[ConsoleLogView]: Feature: Added capability to search the logs

This commit is contained in:
Magesh K
2025-01-14 01:30:31 +05:30
parent 3e74e4ae5d
commit 1eba9b60cb

View File

@@ -10,6 +10,10 @@ import SwiftUI
class ConsoleLogViewModel: ObservableObject { class ConsoleLogViewModel: ObservableObject {
@Published var logLines: [String] = [] @Published var logLines: [String] = []
@Published var searchTerm: String = ""
@Published var currentSearchIndex: Int = 0
@Published var searchResults: [Int] = [] // Stores indices of matching lines
private var fileWatcher: DispatchSourceFileSystemObject? private var fileWatcher: DispatchSourceFileSystemObject?
private let backgroundQueue = DispatchQueue(label: "com.myapp.backgroundQueue", qos: .background) private let backgroundQueue = DispatchQueue(label: "com.myapp.backgroundQueue", qos: .background)
@@ -47,15 +51,40 @@ class ConsoleLogViewModel: ObservableObject {
deinit { deinit {
fileWatcher?.cancel() fileWatcher?.cancel()
} }
func performSearch() {
searchResults = logLines.enumerated()
.filter { $0.element.localizedCaseInsensitiveContains(searchTerm) }
.map { $0.offset }
}
func nextSearchResult() {
guard !searchResults.isEmpty else { return }
currentSearchIndex = (currentSearchIndex + 1) % searchResults.count
}
func previousSearchResult() {
guard !searchResults.isEmpty else { return }
currentSearchIndex = (currentSearchIndex - 1 + searchResults.count) % searchResults.count
}
} }
public struct ConsoleLogView: View { public struct ConsoleLogView: View {
@ObservedObject var viewModel: ConsoleLogViewModel @ObservedObject var viewModel: ConsoleLogViewModel
@State private var isAtBottom: Bool = true
// private let linesToShow: Int = 100 // Number of lines to show at once
@State private var scrollToBottom: Bool = false // State variable to trigger scroll @State private var scrollToBottom: Bool = false // State variable to trigger scroll
@State private var searchBarState: Bool = false
@FocusState private var isSearchFieldFocused: Bool
@State private var searchText: String = ""
@State private var scrollToIndex: Int?
private let resultHighlightColor = Color.orange
private let resultHighlightOpacity = 0.5
private let otherResultsColor = Color.yellow
private let otherResultsOpacity = 0.3
init(logURL: URL) { init(logURL: URL) {
self.viewModel = ConsoleLogViewModel(logURL: logURL) self.viewModel = ConsoleLogViewModel(logURL: logURL)
@@ -63,12 +92,24 @@ public struct ConsoleLogView: View {
public var body: some View { public var body: some View {
VStack { VStack {
// Custom Header Bar (similar to QuickLook's preview screen) // Custom Header Bar (similar to QuickLook's preview screen)
HStack { HStack {
Text("Console Log") Text("Console Log")
.font(.system(size: 22, weight: .semibold)) .font(.system(size: 22, weight: .semibold))
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
if(!searchBarState){
SwiftUI.Button(action: {
searchBarState.toggle()
}) {
Image(systemName: "magnifyingglass")
.foregroundColor(.white)
.imageScale(.large)
}
.padding(.trailing)
}
SwiftUI.Button(action: { SwiftUI.Button(action: {
scrollToBottom.toggle() scrollToBottom.toggle()
}) { }) {
@@ -87,37 +128,99 @@ public struct ConsoleLogView: View {
.foregroundColor(Color.gray.opacity(0.2)), alignment: .bottom .foregroundColor(Color.gray.opacity(0.2)), alignment: .bottom
) )
// Main Log Display (scrollable area) if(searchBarState){
ScrollView(.vertical) { // Search bar
ScrollViewReader { scrollViewProxy in HStack {
LazyVStack(alignment: .leading, spacing: 4) { Image(systemName: "magnifyingglass")
ForEach(viewModel.logLines.indices, id: \.self) { index in .foregroundColor(.gray)
Text(viewModel.logLines[index]) .padding(.trailing, 4)
.font(.system(size: 12, design: .monospaced))
.foregroundColor(.white) TextField("Search", text: $searchText)
} .textFieldStyle(RoundedBorderTextFieldStyle())
} .onChange(of: searchText) { newValue in
.onChange(of: scrollToBottom) { _ in viewModel.searchTerm = newValue
scrollToBottomIfNeeded(scrollViewProxy: scrollViewProxy) viewModel.performSearch()
} }
} .keyboardShortcut("f", modifiers: .command) // Focus search field
if !searchText.isEmpty {
// Search navigation buttons
SwiftUI.Button(action: {
viewModel.previousSearchResult()
scrollToIndex = viewModel.searchResults[viewModel.currentSearchIndex]
}) {
Image(systemName: "chevron.up")
}
.keyboardShortcut(.return, modifiers: [.command, .shift])
.disabled(viewModel.searchResults.isEmpty)
SwiftUI.Button(action: {
viewModel.nextSearchResult()
scrollToIndex = viewModel.searchResults[viewModel.currentSearchIndex]
}) {
Image(systemName: "chevron.down")
}
.keyboardShortcut(.return, modifiers: .command)
.disabled(viewModel.searchResults.isEmpty)
// Results counter
Text("\(viewModel.currentSearchIndex + 1)/\(viewModel.searchResults.count)")
.foregroundColor(.gray)
.font(.caption)
}
SwiftUI.Button(action: {
searchBarState.toggle()
}) {
Image(systemName: "xmark")
}
}
.padding(.horizontal, 15)
} }
}
.background(Color.black) // Set background color to mimic QL's dark theme
.edgesIgnoringSafeArea(.all)
} // Main Log Display (scrollable area)
ScrollView(.vertical) {
// Scroll to the last index (bottom) only if logLines is not empty ScrollViewReader { proxy in
private func scrollToBottomIfNeeded(scrollViewProxy: ScrollViewProxy) { LazyVStack(alignment: .leading, spacing: 4) {
// Ensure we have log data before attempting to scroll ForEach(viewModel.logLines.indices, id: \.self) { index in
guard !viewModel.logLines.isEmpty else { Text(viewModel.logLines[index])
return .font(.system(size: 12, design: .monospaced))
.foregroundColor(.white)
.background(
viewModel.searchResults.contains(index) ?
otherResultsColor.opacity(otherResultsOpacity) : Color.clear
)
.background(
viewModel.searchResults[safe: viewModel.currentSearchIndex] == index ?
resultHighlightColor.opacity(resultHighlightOpacity) : Color.clear
)
}
}
.onChange(of: scrollToIndex) { newIndex in
if let index = newIndex {
withAnimation {
proxy.scrollTo(index, anchor: .center)
}
}
}
.onChange(of: scrollToBottom) { _ in
viewModel.logLines.indices.last.map { last in
proxy.scrollTo(last, anchor: .bottom)
}
}
}
}
} }
.background(Color.black) // Set background color to mimic QL's dark theme
let last = viewModel.logLines.count - 1 .edgesIgnoringSafeArea(.all)
let lastIdx = viewModel.logLines.indices.last }
assert(last == lastIdx) }
// scrollViewProxy.scrollTo(lastIdx, anchor: .bottom)
scrollViewProxy.scrollTo(last, anchor: .bottom) // Helper extension for safe array access
extension Array {
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
} }
} }