Skip to content

Commit 53234aa

Browse files
committed
update
2 parents 4d5c35c + e84c808 commit 53234aa

File tree

17 files changed

+803
-227
lines changed

17 files changed

+803
-227
lines changed

README.md

Lines changed: 147 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010

1111

1212
<p align="center">
13-
<img src="data/icon-192x192.png" alt="tv icon"/>
13+
<img src="data/icon_white.png" style="width:150px;" alt="tv icon"/>
1414
</p>
1515

16+
1617
## Demo
1718

1819
[![asciicast](https://asciinema.org/a/AL2UvtQBxa00Aa44rhsmqj5mn.svg)](https://asciinema.org/a/AL2UvtQBxa00Aa44rhsmqj5mn)
@@ -34,6 +35,7 @@
3435
- [Search](#search)
3536
- [Column Filter](#column-filter)
3637
- [Text Wrapping](#text-wrapping)
38+
- [Filter Operators Guide](FILTER_OPERATORS.md)
3739
- [Advanced Examples](#advanced-examples)
3840
- [Biological Data Formats](#biological-data-formats)
3941

@@ -45,12 +47,13 @@ tv brings spreadsheet-like functionality to your terminal with vim-inspired cont
4547
- **Smart parsing** - Automatically detects delimiters (CSV, TSV, custom separators)
4648
- **Progressive loading** - Start viewing large files immediately while they load
4749
- **Gzip support** - Read compressed files directly
48-
- **Powerful search** - Find text across all cells with highlighting
49-
- **Column filtering** - Show only rows matching specific criteria
50+
- **Powerful search** - Find text across all cells with highlighting and regex pattern matching support
51+
- **Advanced filtering** - Filter rows with complex regex queries
5052
- **Flexible sorting** - Sort by any column with intelligent type detection
5153
- **Text wrapping** - Wrap long cell content for better readability
5254
- **Statistics & plots** - View column statistics with visual distribution charts
5355
- **Vim keybindings** - Navigate naturally with h/j/k/l and more
56+
- **Mouse support** - Click to select cells, scroll with mouse wheel, interact with dialogs
5457
- **Pipe support** - Read from stdin for seamless integration with shell pipelines
5558

5659

@@ -219,9 +222,9 @@ tv uses vim-inspired keybindings for intuitive navigation.
219222
| `/` | Search |
220223
| `n` | Next search result |
221224
| `N` | Previous search result |
222-
| `Ctrl-/` | Clear search |
223-
| `f` | Filter by column value |
224-
| `r` | Reset/clear filter |
225+
| `Esc` | Clear search highlighting / Close dialogs |
226+
| `f` | Filter by column |
227+
| `r` | Remove filter for current column |
225228
| `s` | Sort ascending |
226229
| `S` | Sort descending |
227230
| `t` | Toggle column type (String → Number → Date) |
@@ -231,6 +234,18 @@ tv uses vim-inspired keybindings for intuitive navigation.
231234
| `Esc` | Close dialogs / clear search |
232235
| `q` | Quit |
233236

237+
### Mouse Support
238+
239+
| Action | Behavior |
240+
|--------|----------|
241+
| **Left Click** | Select cell at click position |
242+
| **Scroll Wheel Up** | Scroll up one row |
243+
| **Scroll Wheel Down** | Scroll down one row |
244+
| **Click on Buttons** | Activate buttons in dialogs (Search, Filter, Stats) |
245+
| **Click on Checkboxes** | Toggle checkboxes in forms (e.g., "Use Regex") |
246+
247+
**Note:** Mouse support works in most modern terminals. If your terminal doesn't support mouse events, you can still use keyboard navigation exclusively.
248+
234249
## Features in Detail
235250

236251
### Progressive Loading
@@ -282,53 +297,158 @@ Analyze your data with comprehensive statistics and modern ASCII plots.
282297
- Frequency distribution with percentages
283298
- **Visual distribution:** Bar chart of top 15 most frequent values
284299

300+
**Important:** When column filters are active, statistics are calculated **only on the filtered/visible data**, not the entire dataset. The dialog title will indicate when statistics are based on filtered data and show the number of active filters.
301+
285302
The statistics dialog features a split-pane layout with numerical stats on the left and an ASCII graph visualization on the right, powered by `asciigraph` for modern, clean plots.
286303

287304
### Search
288305

289-
Find text anywhere in your table with full highlighting support.
306+
Find text anywhere in your table with full highlighting support and powerful regex pattern matching.
290307

291308
**How to search:**
292309

293310
1. Press `/` to open the search dialog
294-
2. Type your search term (case-insensitive)
295-
3. Press Enter to execute
296-
4. Navigate results with `n` (next) and `N` (previous)
297-
5. Press `Ctrl-/` to clear highlighting
311+
2. Type your search term.
312+
3. **Optional:** Press Tab to navigate to the checkboxes, then Space to enable:
313+
- **Use Regex**: for pattern matching with regular expressions.
314+
- **Case Sensitive**: for case-sensitive matching.
315+
4. Press Enter to execute the search.
316+
5. Navigate results with `n` (next) and `N` (previous).
317+
6. Press `Esc` to clear highlighting.
318+
319+
**Search Modes:**
320+
321+
- **Plain Text (default):** Case-insensitive substring matching. Enable `Case Sensitive` for exact matching.
322+
- **Regex:** Full regular expression support. By default, regex is case-insensitive. Enable `Case Sensitive` for case-sensitive regex matching.
323+
324+
**Navigation in Search Dialog:**
325+
- Type your search query in the text field.
326+
- Press `Tab` to move between the search field, checkboxes, and buttons.
327+
- Press `Space` to toggle a checkbox when it is focused.
328+
- Press `Enter` from anywhere in the form to execute the search.
329+
- Press `Esc` to cancel and close the dialog.
298330

299331
**Visual feedback:**
300332
- Current match: bright cyan highlight
301333
- Other matches: gray highlight
302-
- Footer shows position: `Match 3/12`
334+
- Footer shows position: `Match 3/12` or `regex matches 3/12`
303335

304336
**Example:**
305337
```
306-
/ → type "error" → Enter → n → n → N → Ctrl-/
338+
# Simple text search (case-insensitive)
339+
/ → type "error" → Enter → n → n → N → Esc
340+
341+
# Case-sensitive text search
342+
/ → check "Case Sensitive" → type "Error" → Enter
343+
344+
# Regex search examples
345+
/ → check "Use Regex" → type "^ERROR" → Enter # Lines starting with ERROR (case-insensitive)
346+
/ → check "Use Regex" and "Case Sensitive" → type "^Error" → Enter # Lines starting with Error (case-sensitive)
347+
/ → check "Use Regex" → type "\\d{4}-\\d{2}-\\d{2}" → Enter # Date patterns (YYYY-MM-DD)
348+
/ → check "Use Regex" → type "user(name)?" → Enter # Match "user" or "username"
349+
/ → check "Use Regex" → type "error|warning|critical" → Enter # Match any of these words
350+
/ → check "Use Regex" → type "@.*\\.(com|org)$" → Enter # Email domains ending in .com or .org
307351
```
308352

353+
**Common Regex Patterns:**
354+
355+
| Pattern | Description | Example Match |
356+
|---------|-------------|---------------|
357+
| `^start` | Match at beginning of cell | `^Error` matches "Error: failed" |
358+
| `end$` | Match at end of cell | `\\.txt$` matches "file.txt" |
359+
| `\\d+` | Match one or more digits | `\\d+` matches "123" |
360+
| `\\w+@\\w+\\.\\w+` | Match email pattern | Matches "user@example.com" |
361+
| `word1\\|word2` | Match either word (OR) | `success\\|complete` matches either |
362+
| `[A-Z]+` | Match uppercase letters | `[A-Z]{3}` matches "USA" |
363+
| `.*` | Match any characters | `start.*end` matches "start...end" |
364+
| `\\s+` | Match whitespace | `\\s{2,}` matches 2+ spaces |
365+
366+
**Note:** For case-insensitive regex search, the `(?i)` flag is automatically added to your pattern. For case-sensitive regex, this flag is omitted.
367+
309368
### Column Filter
310369

311-
Show only rows where a specific column matches your criteria.
370+
Show only rows where specific columns match your criteria. **Supports filtering on multiple columns simultaneously**.
312371

313372
**How to filter:**
314373

315374
1. Navigate to the column you want to filter
316375
2. Press `f` to open the filter dialog
317-
3. Type filter text (case-insensitive, partial match)
318-
4. Press Enter to apply
319-
5. Press `r` to reset and show all rows
376+
3. Use the dropdown to select an operator (e.g., `contains`, `equals`, `regex`, `>`).
377+
4. Enter the value to filter by.
378+
5. Optionally, check the `Case Sensitive` box.
379+
6. Press Enter to apply the filter.
380+
7. **Repeat on other columns to add more filters**
381+
8. Press `r` on a filtered column to remove that column's filter
382+
383+
**Multi-Column Filtering:**
384+
385+
- Apply filters to **multiple columns** by pressing `f` on each column and entering criteria
386+
- All filters are combined with AND logic (rows must match all active filters)
387+
- Each column can have different filter criteria including operators
388+
- Press `f` on a filtered column to edit or remove its filter (empty query removes the filter)
389+
- The footer shows how many filters are active
390+
- Filtered column headers display 🔎 icons with orange background
391+
392+
**Operators:**
393+
394+
| Operator | Description |
395+
|---|---|
396+
| `contains` | Matches cells containing the term |
397+
| `equals` | Matches cells that are exactly the term |
398+
| `starts with` | Matches cells that start with the term |
399+
| `ends with` | Matches cells that end with the term |
400+
| `regex` | Matches cells based on a regular expression |
401+
| `>` | Numeric: greater than (number columns only) |
402+
| `<` | Numeric: less than (number columns only) |
403+
| `>=` | Numeric: greater than or equal (number columns only) |
404+
| `<=` | Numeric: less than or equal (number columns only) |
405+
406+
**Key Features:**
407+
- **Numeric operators** (`>`, `<`, `>=`, `<=`): Only work on numeric and date columns (automatically detected). Perform numeric comparisons instead of text matching.
408+
- **Regex**: Provides the full power of regular expressions for complex pattern matching.
409+
- **Case-Insensitive by default**: All string-based comparisons are case-insensitive unless the `Case Sensitive` box is checked.
410+
- **Visual indicator**: Filtered column headers show 🔎 icons and an orange background
320411

321-
**Features:**
322-
- Partial matching: "act" matches "active", "action", "react"
323-
- Header always visible
324-
- Footer shows filtered row count
412+
**Examples:**
325413

326-
**Example:**
327-
```
328-
Navigate to "Status" column → f → type "pending" → Enter
414+
```bash
415+
# Simple filter - partial match
416+
Navigate to "Status" column → f → select 'contains'type "pending" → Enter
417+
# Result: Matches "Pending", "Pending Review", etc.
418+
419+
# Exact match filter
420+
Navigate to "Status" column → f → select 'equals'type "active" → Enter
421+
# Result: Rows where Status is exactly "active" (case-insensitive)
422+
423+
# Regex filter
424+
Navigate to "Email" column → f → select 'regex'type "^.+@gmail\.com$" → Enter
425+
# Result: Rows where Email ends with "@gmail.com"
426+
427+
# Numeric comparison - greater than
428+
Navigate to "Age" column → f → select '>'type "30" → Enter
429+
# Result: Rows where Age is greater than 30
430+
431+
# Multi-column filtering
432+
Navigate to "City" column → f → select 'equals'type "New York" → Enter
433+
Navigate to "Department" column → f → select 'contains'type "Engineering" → Enter
434+
# Result: Rows where City is "New York" AND Department contains "Engineering"
435+
436+
# Edit existing filter
437+
Navigate to filtered column → f → modify operator/value → Enter
438+
# Or enter empty text to remove that column's filter
439+
440+
# Remove a specific filter
441+
Navigate to filtered column → Press r
442+
# Result: That column's filter removed, other filters remain
329443
```
330444
331-
Now only rows where Status contains "pending" are displayed.
445+
**Visual Feedback:**
446+
- When a filter is active, the filtered column header displays 🔎 icons and an orange background
447+
- A dedicated **filter strip** appears above the main footer showing the active filter on the current column.
448+
- Filter strip format: `🔎 Filter Active: [Column Name] [operator] "query" | Press 'r' to clear`
449+
- The strip automatically hides when you move to a different column
450+
- Press `r` to clear the filter and return to normal view
451+
332452
333453
### Text Wrapping
334454
@@ -434,7 +554,9 @@ tv data.txt -s " "
434554
- **Too many columns?** Use `--columns` to show only what you need
435555
- **Long text?** Press `W` to wrap the current column
436556
- **Wrong sort order?** Press `t` to change the column type, then `s` to re-sort
557+
- **Complex filtering?** Use `OR` for alternatives, `AND` for requirements, `ROR` to combine results
437558
- **Need insights?** Press `i` for comprehensive statistics with visual plots - histograms for numeric data, frequency charts for categorical data
559+
- **Prefer mouse?** Click cells to select them, use scroll wheel to navigate, and click buttons in dialogs
438560
439561
## License
440562

buffer.go

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"errors"
5+
"regexp"
56
"sort"
67
"strconv"
78
"strings"
@@ -274,8 +275,6 @@ func parseDateValueFast(s string) int64 {
274275
return 0
275276
}
276277

277-
278-
279278
// getCol returns the ith column data as a string slice
280279
// Uses pointer receiver to avoid copying mutex
281280
func (b *Buffer) getCol(i int) []string {
@@ -502,9 +501,16 @@ func (b *Buffer) selectBySearch(s string) {
502501
}
503502
}
504503

505-
// filterByColumn filters rows based on column value containing the search string
506-
// Returns a new filtered buffer
507-
func (b *Buffer) filterByColumn(colIndex int, query string, caseSensitive bool) *Buffer {
504+
// FilterOptions defines the parameters for a column filter.
505+
type FilterOptions struct {
506+
Query string
507+
Operator string
508+
CaseSensitive bool
509+
}
510+
511+
// filterByColumn filters rows based on column value using the provided options.
512+
// It returns a new buffer containing the filtered rows.
513+
func (b *Buffer) filterByColumn(colIndex int, options FilterOptions) *Buffer {
508514
b.mu.RLock()
509515
defer b.mu.RUnlock()
510516

@@ -517,11 +523,17 @@ func (b *Buffer) filterByColumn(colIndex int, query string, caseSensitive bool)
517523
copy(filtered.colType, b.colType)
518524

519525
// Add header row if present
520-
if b.rowFreeze > 0 {
526+
if b.rowFreeze > 0 && b.rowLen > 0 {
521527
filtered.cont = append(filtered.cont, b.cont[0])
522528
filtered.rowLen = 1
523529
}
524530

531+
// Get column type for numeric comparisons
532+
colType := colTypeStr
533+
if colIndex < len(b.colType) {
534+
colType = b.colType[colIndex]
535+
}
536+
525537
// Filter data rows
526538
startRow := b.rowFreeze
527539
for i := startRow; i < b.rowLen; i++ {
@@ -530,18 +542,77 @@ func (b *Buffer) filterByColumn(colIndex int, query string, caseSensitive bool)
530542
}
531543

532544
cellValue := b.cont[i][colIndex]
533-
queryStr := query
534-
535-
if !caseSensitive {
536-
cellValue = toLowerSimple(cellValue)
537-
queryStr = toLowerSimple(query)
538-
}
539545

540-
if containsStr(cellValue, queryStr) {
546+
// Evaluate filter condition
547+
if evaluateFilter(cellValue, options, colType) {
541548
filtered.cont = append(filtered.cont, b.cont[i])
542549
filtered.rowLen++
543550
}
544551
}
545552

546553
return filtered
547554
}
555+
556+
// evaluateFilter checks if a cell value matches the filter query based on the operator.
557+
func evaluateFilter(cellValue string, options FilterOptions, colType int) bool {
558+
query := options.Query
559+
operator := options.Operator
560+
561+
// Handle numeric comparisons first
562+
if colType == colTypeFloat || colType == colTypeDate {
563+
isNumericOperator := false
564+
switch operator {
565+
case ">", "<", ">=", "<=":
566+
isNumericOperator = true
567+
}
568+
569+
if isNumericOperator {
570+
cellVal := parseNumericValueFast(cellValue)
571+
thresholdVal, err := strconv.ParseFloat(strings.TrimSpace(query), 64)
572+
if err != nil {
573+
return false // Cannot compare if query is not a number
574+
}
575+
576+
switch operator {
577+
case ">":
578+
return cellVal > thresholdVal
579+
case "<":
580+
return cellVal < thresholdVal
581+
case ">=":
582+
return cellVal >= thresholdVal
583+
case "<=":
584+
return cellVal <= thresholdVal
585+
}
586+
}
587+
}
588+
589+
// Prepare strings for comparison
590+
cell := cellValue
591+
q := query
592+
if !options.CaseSensitive {
593+
cell = strings.ToLower(cell)
594+
q = strings.ToLower(q)
595+
}
596+
597+
// Handle string-based operators
598+
switch operator {
599+
case "contains":
600+
return strings.Contains(cell, q)
601+
case "equals":
602+
return cell == q
603+
case "starts with":
604+
return strings.HasPrefix(cell, q)
605+
case "ends with":
606+
return strings.HasSuffix(cell, q)
607+
case "regex":
608+
// When using regex, the user has full control over case sensitivity in the pattern.
609+
re, err := regexp.Compile(options.Query)
610+
if err != nil {
611+
return false // Invalid regex
612+
}
613+
return re.MatchString(cellValue)
614+
default:
615+
// Default to contains for backward compatibility if operator is empty
616+
return strings.Contains(cell, q)
617+
}
618+
}

data/favicon.ico

-15 KB
Binary file not shown.

data/icon-16x16.png

-460 Bytes
Binary file not shown.

data/icon-192x192.png

-7.24 KB
Binary file not shown.

data/icon-32x32.png

-910 Bytes
Binary file not shown.

data/icon-512x512.png

-21.6 KB
Binary file not shown.

data/icon.png

278 KB
Loading

data/icon_white.png

282 KB
Loading

0 commit comments

Comments
 (0)