@@ -149,7 +149,7 @@ struct LiveTailView: View {
149 .font(.caption)
149 .font(.caption)
150 .foregroundStyle(.secondary)
150 .foregroundStyle(.secondary)
152− if !viewModel.filterText.isEmpty && viewModel.displayedCount != viewModel.entryCount {
152+ if (!viewModel.filterText.isEmpty || !viewModel.columnFilters.isEmpty) && viewModel.displayedCount != viewModel.entryCount {
153 Text("(of \(viewModel.entryCount) total)")
153 Text("(of \(viewModel.entryCount) total)")
154 .font(.caption)
154 .font(.caption)
155 .foregroundStyle(.secondary)
155 .foregroundStyle(.secondary)
@@ -161,6 +161,55 @@ struct LiveTailView: View {
162 Divider()
162 Divider()
164+ // Active column filters
165+ if !viewModel.columnFilters.isEmpty {
166+ HStack(spacing: 4) {
167+ Image(systemName: "line.3.horizontal.decrease.circle.fill")
168+ .foregroundStyle(.secondary)
171+ ScrollView(.horizontal, showsIndicators: false) {
172+ HStack(spacing: 4) {
173+ ForEach(viewModel.columnFilters) { filter in
174+ HStack(spacing: 2) {
175+ Text(filter.displayLabel)
179+ viewModel.removeColumnFilter(filter)
181+ Image(systemName: "xmark")
182+ .font(.system(size: 8, weight: .bold))
184+ .buttonStyle(.plain)
186+ .padding(.horizontal, 6)
187+ .padding(.vertical, 2)
189+ RoundedRectangle(cornerRadius: 4)
190+ .fill(filter.exclude
191+ ? Color.red.opacity(0.15)
192+ : Color.accentColor.opacity(0.15))
200+ Button("Clear All") {
201+ viewModel.clearColumnFilters()
204+ .buttonStyle(.plain)
205+ .foregroundStyle(.secondary)
207+ .padding(.horizontal, 8)
208+ .padding(.vertical, 4)
164 // Column table or empty state
213 // Column table or empty state
165 if viewModel.visibleColumns.isEmpty {
214 if viewModel.visibleColumns.isEmpty {
166 VStack(spacing: 8) {
215 VStack(spacing: 8) {
@@ -233,7 +282,10 @@ struct LiveTailView: View {
233 columns: viewModel.visibleColumns,
282 columns: viewModel.visibleColumns,
234 columnWidths: columnWidths,
283 columnWidths: columnWidths,
235 isSelected: selectedRecord == sorted[index].record,
284 isSelected: selectedRecord == sorted[index].record,
236− isAlternate: index % 2 == 1
285+ isAlternate: index % 2 == 1,
286+ onCellFilter: { column, value, exclude in
287+ viewModel.addColumnFilter(column: column, value: value, exclude: exclude)
238 .id(sorted[index].id)
290 .id(sorted[index].id)
239 .onTapGesture {
291 .onTapGesture {
@@ -258,9 +310,10 @@ struct LiveTailView: View {
258 guard autoScroll, sortColumn == nil else { return }
310 guard autoScroll, sortColumn == nil else { return }
259 // When a filter is active, scroll to the last filtered entry;
311 // When a filter is active, scroll to the last filtered entry;
260 // when no filter, scroll to the absolute last entry.
312 // when no filter, scroll to the absolute last entry.
261− let target = viewModel.filterText.isEmpty
313+ let hasFilters = !viewModel.filterText.isEmpty || !viewModel.columnFilters.isEmpty
314+ let target = hasFilters
262− ? viewModel.entries.last
315+ ? viewModel.filteredEntries.last
263− : viewModel.filteredEntries.last
316+ : viewModel.entries.last
264 if let target {
317 if let target {
265 proxy.scrollTo(target.id, anchor: .bottom)
318 proxy.scrollTo(target.id, anchor: .bottom)