dataframe-textual 1.1.4__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataframe-textual
3
- Version: 1.1.4
3
+ Version: 1.2.0
4
4
  Summary: Interactive terminal viewer/editor for tabular data
5
5
  Project-URL: Homepage, https://github.com/need47/dataframe-textual
6
6
  Project-URL: Repository, https://github.com/need47/dataframe-textual.git
@@ -45,11 +45,11 @@ A powerful, interactive terminal-based viewer/editor for CSV/TSV/Excel/Parquet/J
45
45
 
46
46
  ## Features
47
47
 
48
- ### Core Data Viewing
48
+ ### Data Viewing
49
49
  - 🚀 **Fast Loading** - Powered by Polars for efficient data handling
50
- - 🎨 **Rich Terminal UI** - Beautiful, color-coded columns with automatic type detection
50
+ - 🎨 **Rich Terminal UI** - Beautiful, color-coded columns with various data types (e.g., integer, float, string)
51
51
  - ⌨️ **Comprehensive Keyboard Navigation** - Intuitive controls for browsing, editing, and manipulating data
52
- - 📊 **Flexible Input** - Read from files or stdin (pipes/redirects)
52
+ - 📊 **Flexible Input** - Read from files and/or stdin (pipes/redirects)
53
53
  - 🔄 **Smart Pagination** - Lazy load rows on demand for handling large datasets
54
54
 
55
55
  ### Data Manipulation
@@ -60,7 +60,7 @@ A powerful, interactive terminal-based viewer/editor for CSV/TSV/Excel/Parquet/J
60
60
  - 💾 **Save & Undo** - Save edits back to file with full undo/redo support
61
61
 
62
62
  ### Advanced Features
63
- - 📂 **Multi-File Support** - Open multiple files in tabs for side-by-side comparison
63
+ - 📂 **Multi-File Support** - Open multiple files in separate tabs
64
64
  - 🔄 **Tab Management** - Seamlessly switch between open files with keyboard shortcuts
65
65
  - 📌 **Freeze Rows/Columns** - Keep important rows and columns visible while scrolling
66
66
  - 🎯 **Cursor Type Cycling** - Switch between cell, row, and column selection modes
@@ -206,17 +206,21 @@ When multiple files are opened:
206
206
  | Key | Action |
207
207
  |-----|--------|
208
208
  | `Double-click` | Edit cell or rename column header |
209
- | `X` | Clear current cell (set to NULL) |
209
+ | `delete` | Clear current cell (set to NULL) |
210
210
  | `e` | Edit current cell (respects data type) |
211
211
  | `E` | Edit entire column with expression |
212
212
  | `a` | Add empty column after current |
213
213
  | `A` | Add column with name and value/expression |
214
214
  | `-` (minus) | Delete current column |
215
+ | `_` (underscore) | Delete current column and all columns after |
216
+ | `Ctrl+-` | Delete current column and all columns before |
215
217
  | `x` | Delete current row |
218
+ | `X` | Delete current row and all rows below |
219
+ | `Ctrl+X` | Delete current row and all rows above |
216
220
  | `d` | Duplicate current column (appends '_copy' suffix) |
217
221
  | `D` | Duplicate current row |
218
222
  | `h` | Hide current column |
219
- | `H` | Show all hidden columns |
223
+ | `H` | Show all hidden rows/columns |
220
224
 
221
225
  #### Searching & Filtering
222
226
 
@@ -283,7 +287,8 @@ When multiple files are opened:
283
287
  | `Ctrl+R` | Copy row to clipboard (tab-separated) |
284
288
  | `Ctrl+S` | Save current tab to file |
285
289
  | `u` | Undo last action |
286
- | `U` | Reset to original data |
290
+ | `U` | Redo last undone action |
291
+ | `Ctrl+U` | Reset to initial state |
287
292
 
288
293
  ## Features in Detail
289
294
 
@@ -512,7 +517,7 @@ Press `F` to see how many times each value appears in the current column. The mo
512
517
  **In the Frequency Table**:
513
518
  - Press `[` and `]` to sort by any column (value, count, or percentage)
514
519
  - Press `v` to **filter** the main table to show only rows with the selected value
515
- - Press `"` to **highlight** all rows containing the selected value
520
+ - Press `"` to **exclude** all rows except those containing the selected value
516
521
  - Press `q` or `Escape` to close the frequency table
517
522
 
518
523
  This is useful for:
@@ -560,9 +565,25 @@ This is useful for:
560
565
  - Delete all selected rows (if any) at once
561
566
  - Or delete single row at cursor
562
567
 
568
+ **Delete Row and Below** (`X`):
569
+ - Deletes the current row and all rows below it
570
+ - Useful for removing trailing data or the end of a dataset
571
+
572
+ **Delete Row and Above** (`Ctrl+X`):
573
+ - Deletes the current row and all rows above it
574
+ - Useful for removing leading rows or the beginning of a dataset
575
+
563
576
  **Delete Column** (`-`):
564
577
  - Removes the entire column from view and dataframe
565
578
 
579
+ **Delete Column and After** (`_`):
580
+ - Deletes the current column and all columns to the right
581
+ - Useful for removing trailing columns or the end of a dataset
582
+
583
+ **Delete Column and Before** (`Ctrl+-`):
584
+ - Deletes the current column and all columns to the left
585
+ - Useful for removing leading columns or the beginning of a dataset
586
+
566
587
  ### 9. Hide & Show Columns
567
588
 
568
589
  **Hide Column** (`h`):
@@ -570,9 +591,8 @@ This is useful for:
570
591
  - Column data is preserved in the dataframe
571
592
  - Hidden columns are included in saves
572
593
 
573
- **Show Hidden Columns** (`H`):
574
- - Restores all previously hidden columns to the display
575
- - Returns table to full column view
594
+ **Show Hidden Rows/Columns** (`H`):
595
+ - Restores all previously hidden rows/columns to the display
576
596
 
577
597
  This is useful for:
578
598
  - Focusing on specific columns without deleting data
@@ -634,14 +654,23 @@ Press `Ctrl+S` to save:
634
654
  - Choose filename in modal dialog
635
655
  - Confirm if file already exists
636
656
 
637
- ### 15. Undo/Redo
657
+ ### 15. Undo/Redo/Reset
638
658
 
639
- Press `u` to undo:
659
+ **Undo** (`u`):
640
660
  - Reverts last action with full state restoration
641
661
  - Works for edits, deletions, sorts, searches, etc.
642
662
  - Shows description of reverted action
643
663
 
644
- Press `U` to revert to when data was initially loaded
664
+ **Redo** (`U`):
665
+ - Reapplies the last undone action
666
+ - Restores the state before the undo was performed
667
+ - Useful for redoing actions you've undone by mistake
668
+ - Useful for alternating between two different states
669
+
670
+ **Reset** (`Ctrl+U`):
671
+ - Reverts all changes and returns to original data state when file was first loaded
672
+ - Clears all edits, deletions, selections, filters, and sorts
673
+ - Useful for starting fresh without reloading the file
645
674
 
646
675
  ### 16. Column Type Conversion
647
676
 
@@ -6,11 +6,11 @@ A powerful, interactive terminal-based viewer/editor for CSV/TSV/Excel/Parquet/J
6
6
 
7
7
  ## Features
8
8
 
9
- ### Core Data Viewing
9
+ ### Data Viewing
10
10
  - 🚀 **Fast Loading** - Powered by Polars for efficient data handling
11
- - 🎨 **Rich Terminal UI** - Beautiful, color-coded columns with automatic type detection
11
+ - 🎨 **Rich Terminal UI** - Beautiful, color-coded columns with various data types (e.g., integer, float, string)
12
12
  - ⌨️ **Comprehensive Keyboard Navigation** - Intuitive controls for browsing, editing, and manipulating data
13
- - 📊 **Flexible Input** - Read from files or stdin (pipes/redirects)
13
+ - 📊 **Flexible Input** - Read from files and/or stdin (pipes/redirects)
14
14
  - 🔄 **Smart Pagination** - Lazy load rows on demand for handling large datasets
15
15
 
16
16
  ### Data Manipulation
@@ -21,7 +21,7 @@ A powerful, interactive terminal-based viewer/editor for CSV/TSV/Excel/Parquet/J
21
21
  - 💾 **Save & Undo** - Save edits back to file with full undo/redo support
22
22
 
23
23
  ### Advanced Features
24
- - 📂 **Multi-File Support** - Open multiple files in tabs for side-by-side comparison
24
+ - 📂 **Multi-File Support** - Open multiple files in separate tabs
25
25
  - 🔄 **Tab Management** - Seamlessly switch between open files with keyboard shortcuts
26
26
  - 📌 **Freeze Rows/Columns** - Keep important rows and columns visible while scrolling
27
27
  - 🎯 **Cursor Type Cycling** - Switch between cell, row, and column selection modes
@@ -167,17 +167,21 @@ When multiple files are opened:
167
167
  | Key | Action |
168
168
  |-----|--------|
169
169
  | `Double-click` | Edit cell or rename column header |
170
- | `X` | Clear current cell (set to NULL) |
170
+ | `delete` | Clear current cell (set to NULL) |
171
171
  | `e` | Edit current cell (respects data type) |
172
172
  | `E` | Edit entire column with expression |
173
173
  | `a` | Add empty column after current |
174
174
  | `A` | Add column with name and value/expression |
175
175
  | `-` (minus) | Delete current column |
176
+ | `_` (underscore) | Delete current column and all columns after |
177
+ | `Ctrl+-` | Delete current column and all columns before |
176
178
  | `x` | Delete current row |
179
+ | `X` | Delete current row and all rows below |
180
+ | `Ctrl+X` | Delete current row and all rows above |
177
181
  | `d` | Duplicate current column (appends '_copy' suffix) |
178
182
  | `D` | Duplicate current row |
179
183
  | `h` | Hide current column |
180
- | `H` | Show all hidden columns |
184
+ | `H` | Show all hidden rows/columns |
181
185
 
182
186
  #### Searching & Filtering
183
187
 
@@ -244,7 +248,8 @@ When multiple files are opened:
244
248
  | `Ctrl+R` | Copy row to clipboard (tab-separated) |
245
249
  | `Ctrl+S` | Save current tab to file |
246
250
  | `u` | Undo last action |
247
- | `U` | Reset to original data |
251
+ | `U` | Redo last undone action |
252
+ | `Ctrl+U` | Reset to initial state |
248
253
 
249
254
  ## Features in Detail
250
255
 
@@ -473,7 +478,7 @@ Press `F` to see how many times each value appears in the current column. The mo
473
478
  **In the Frequency Table**:
474
479
  - Press `[` and `]` to sort by any column (value, count, or percentage)
475
480
  - Press `v` to **filter** the main table to show only rows with the selected value
476
- - Press `"` to **highlight** all rows containing the selected value
481
+ - Press `"` to **exclude** all rows except those containing the selected value
477
482
  - Press `q` or `Escape` to close the frequency table
478
483
 
479
484
  This is useful for:
@@ -521,9 +526,25 @@ This is useful for:
521
526
  - Delete all selected rows (if any) at once
522
527
  - Or delete single row at cursor
523
528
 
529
+ **Delete Row and Below** (`X`):
530
+ - Deletes the current row and all rows below it
531
+ - Useful for removing trailing data or the end of a dataset
532
+
533
+ **Delete Row and Above** (`Ctrl+X`):
534
+ - Deletes the current row and all rows above it
535
+ - Useful for removing leading rows or the beginning of a dataset
536
+
524
537
  **Delete Column** (`-`):
525
538
  - Removes the entire column from view and dataframe
526
539
 
540
+ **Delete Column and After** (`_`):
541
+ - Deletes the current column and all columns to the right
542
+ - Useful for removing trailing columns or the end of a dataset
543
+
544
+ **Delete Column and Before** (`Ctrl+-`):
545
+ - Deletes the current column and all columns to the left
546
+ - Useful for removing leading columns or the beginning of a dataset
547
+
527
548
  ### 9. Hide & Show Columns
528
549
 
529
550
  **Hide Column** (`h`):
@@ -531,9 +552,8 @@ This is useful for:
531
552
  - Column data is preserved in the dataframe
532
553
  - Hidden columns are included in saves
533
554
 
534
- **Show Hidden Columns** (`H`):
535
- - Restores all previously hidden columns to the display
536
- - Returns table to full column view
555
+ **Show Hidden Rows/Columns** (`H`):
556
+ - Restores all previously hidden rows/columns to the display
537
557
 
538
558
  This is useful for:
539
559
  - Focusing on specific columns without deleting data
@@ -595,14 +615,23 @@ Press `Ctrl+S` to save:
595
615
  - Choose filename in modal dialog
596
616
  - Confirm if file already exists
597
617
 
598
- ### 15. Undo/Redo
618
+ ### 15. Undo/Redo/Reset
599
619
 
600
- Press `u` to undo:
620
+ **Undo** (`u`):
601
621
  - Reverts last action with full state restoration
602
622
  - Works for edits, deletions, sorts, searches, etc.
603
623
  - Shows description of reverted action
604
624
 
605
- Press `U` to revert to when data was initially loaded
625
+ **Redo** (`U`):
626
+ - Reapplies the last undone action
627
+ - Restores the state before the undo was performed
628
+ - Useful for redoing actions you've undone by mistake
629
+ - Useful for alternating between two different states
630
+
631
+ **Reset** (`Ctrl+U`):
632
+ - Reverts all changes and returns to original data state when file was first loaded
633
+ - Clears all edits, deletions, selections, filters, and sorts
634
+ - Useful for starting fresh without reloading the file
606
635
 
607
636
  ### 16. Column Type Conversion
608
637
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dataframe-textual"
7
- version = "1.1.4"
7
+ version = "1.2.0"
8
8
  description = "Interactive terminal viewer/editor for tabular data"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -54,6 +54,14 @@ dev = [
54
54
 
55
55
  [tool.hatch.build.targets.wheel]
56
56
  packages = ["src/dataframe_textual"]
57
+ exclude = [
58
+ "*.png",
59
+ ]
60
+
61
+ [tool.hatch.build.targets.sdist]
62
+ exclude = [
63
+ "*.png",
64
+ ]
57
65
 
58
66
  [dependency-groups]
59
67
  dev = [
@@ -104,8 +104,12 @@ class DataFrameTable(DataTable):
104
104
  - **F** - 📊 Show frequency distribution
105
105
  - **s** - 📈 Show statistics for current column
106
106
  - **S** - 📊 Show statistics for entire dataframe
107
- - **K** - 🔄 Cycle cursor (cell → row → column → cell)
107
+ - **h** - 👁️ Hide current column
108
+ - **H** - 👀 Show all hidden rows/columns
109
+ - **z** - 📌 Freeze rows and columns
108
110
  - **~** - 🏷️ Toggle row labels
111
+ - **,** - 🔢 Toggle thousand separator for numeric display
112
+ - **K** - 🔄 Cycle cursor (cell → row → column → cell)
109
113
 
110
114
  ## ↕️ Sorting
111
115
  - **[** - 🔼 Sort column ascending
@@ -136,7 +140,7 @@ class DataFrameTable(DataTable):
136
140
  - **{** - ⬆️ Go to previous selected row
137
141
  - **}** - ⬇️ Go to next selected row
138
142
  - **"** - 📍 Filter to show only selected rows
139
- - **T** - 🧹 Clear all selections
143
+ - **T** - 🧹 Clear all selections and matches
140
144
 
141
145
  ## ✏️ Edit & Modify
142
146
  - **Double-click** - ✍️ Edit cell or rename column header
@@ -144,13 +148,16 @@ class DataFrameTable(DataTable):
144
148
  - **E** - 📊 Edit entire column with expression
145
149
  - **a** - ➕ Add empty column after current
146
150
  - **A** - ➕ Add column with name and optional expression
147
- - **x** - 🗑️ Delete current row
148
- - **X** - Clear current cell (set to None)
149
- - **D** - 📋 Duplicate current row
151
+ - **x** - Delete current row
152
+ - **X** - Delete row and those below
153
+ - **Ctrl+X** - Delete row and those above
154
+ - **delete** - ❌ Clear current cell (set to NULL)
150
155
  - **-** - ❌ Delete current column
156
+ - **_** - ❌ Delete column and those after
157
+ - **Ctrl+-** - ❌ Delete column and those before
151
158
  - **d** - 📋 Duplicate current column
152
- - **h** - 👁️ Hide current column
153
- - **H** - 👀 Show all hidden columns
159
+ - **D** - 📋 Duplicate current row
160
+
154
161
 
155
162
  ## 🎯 Reorder
156
163
  - **Shift+↑↓** - ⬆️⬇️ Move row up/down
@@ -166,32 +173,39 @@ class DataFrameTable(DataTable):
166
173
  - **@** - 🔗 Make URLs in current column clickable with Ctrl/Cmd
167
174
 
168
175
  ## 💾 Data Management
169
- - **z** - 📌 Freeze rows and columns
170
- - **,** - 🔢 Toggle thousand separator for numeric display
171
176
  - **c** - 📋 Copy cell to clipboard
172
177
  - **Ctrl+c** - 📊 Copy column to clipboard
173
178
  - **Ctrl+r** - 📝 Copy row to clipboard (tab-separated)
174
179
  - **Ctrl+s** - 💾 Save current tab to file
175
180
  - **u** - ↩️ Undo last action
176
- - **U** - 🔄 Reset to original data
181
+ - **U** - 🔄 Redo last undone action
182
+ - **Ctrl+U** - 🔁 Reset to initial state
177
183
  """).strip()
178
184
 
179
185
  # fmt: off
180
186
  BINDINGS = [
187
+ # Navigation
181
188
  ("g", "jump_top", "Jump to top"),
182
189
  ("G", "jump_bottom", "Jump to bottom"),
190
+ # Display
183
191
  ("h", "hide_column", "Hide column"),
184
- ("H", "show_column", "Show columns"),
192
+ ("H", "show_hidden_rows_columns", "Show hidden rows/columns"),
193
+ ("tilde", "toggle_row_labels", "Toggle row labels"), # `~`
194
+ ("K", "cycle_cursor_type", "Cycle cursor mode"), # `K`
195
+ ("z", "freeze_row_column", "Freeze rows/columns"),
196
+ ("comma", "show_thousand_separator", "Toggle thousand separator"), # `,`
197
+ # Copy
185
198
  ("c", "copy_cell", "Copy cell to clipboard"),
186
199
  ("ctrl+c", "copy_column", "Copy column to clipboard"),
187
200
  ("ctrl+r", "copy_row", "Copy row to clipboard"),
201
+ # Save
188
202
  ("ctrl+s", "save_to_file", "Save to file"),
203
+ # Detail, Frequency, and Statistics
189
204
  ("enter", "view_row_detail", "View row details"),
190
- # Frequency & Statistics
191
205
  ("F", "show_frequency", "Show frequency"),
192
206
  ("s", "show_statistics", "Show statistics for column"),
193
207
  ("S", "show_statistics('dataframe')", "Show statistics for dataframe"),
194
- # Sorting
208
+ # Sort
195
209
  ("left_square_bracket", "sort_ascending", "Sort ascending"), # `[`
196
210
  ("right_square_bracket", "sort_descending", "Sort descending"), # `]`
197
211
  # View
@@ -215,16 +229,23 @@ class DataFrameTable(DataTable):
215
229
  # Selection
216
230
  ("apostrophe", "make_selections", "Toggle row selection"), # `'`
217
231
  ("t", "toggle_selections", "Toggle all row selections"),
218
- ("T", "clear_selections", "Clear selections"),
232
+ ("T", "clear_selections_and_matches", "Clear selections"),
219
233
  ("quotation_mark", "filter_selected_rows", "Filter selected"), # `"`
220
- # Edit
234
+ # Delete
235
+ ("delete", "clear_cell", "Clear cell"),
221
236
  ("minus", "delete_column", "Delete column"), # `-`
237
+ ("underscore", "delete_column_and_after", "Delete column and those after"), # `_`
238
+ ("ctrl+minus", "delete_column_and_before", "Delete column and those before"), # `Ctrl+-`
222
239
  ("x", "delete_row", "Delete row"),
223
- ("X", "clear_cell", "Clear cell"),
240
+ ("X", "delete_row_and_below", "Delete row and those below"),
241
+ ("ctrl+x", "delete_row_and_up", "Delete row and those up"),
242
+ # Duplicate
224
243
  ("d", "duplicate_column", "Duplicate column"),
225
244
  ("D", "duplicate_row", "Duplicate row"),
245
+ # Edit
226
246
  ("e", "edit_cell", "Edit cell"),
227
247
  ("E", "edit_column", "Edit column"),
248
+ # Add
228
249
  ("a", "add_column", "Add column"),
229
250
  ("A", "add_column_expr", "Add column with expression"),
230
251
  # Reorder
@@ -238,14 +259,10 @@ class DataFrameTable(DataTable):
238
259
  ("exclamation_mark", "cast_column_dtype('bool')", "Cast column dtype to bool"), # `!`
239
260
  ("dollar_sign", "cast_column_dtype('string')", "Cast column dtype to string"), # `$`
240
261
  ("at", "make_cell_clickable", "Make cell clickable"), # `@`
241
- # Misc
242
- ("tilde", "toggle_row_labels", "Toggle row labels"), # `~`
243
- ("K", "cycle_cursor_type", "Cycle cursor mode"), # `K`
244
- ("z", "freeze_row_column", "Freeze rows/columns"),
245
- ("comma", "show_thousand_separator", "Toggle thousand separator"), # `,`
246
262
  # Undo/Redo
247
263
  ("u", "undo", "Undo"),
248
- ("U", "reset", "Reset to original"),
264
+ ("U", "redo", "Redo"),
265
+ ("ctrl+u", "reset", "Reset to initial state"),
249
266
  ]
250
267
  # fmt: on
251
268
 
@@ -287,8 +304,10 @@ class DataFrameTable(DataTable):
287
304
  self.fixed_rows = 0 # Number of fixed rows
288
305
  self.fixed_columns = 0 # Number of fixed columns
289
306
 
290
- # History stack for undo/redo
307
+ # History stack for undo
291
308
  self.histories: deque[History] = deque()
309
+ # Current history state for redo
310
+ self.history: History = None
292
311
 
293
312
  # Pending filename for save operations
294
313
  self._pending_filename = ""
@@ -391,17 +410,27 @@ class DataFrameTable(DataTable):
391
410
  matches.append((ridx, cidx))
392
411
  return matches
393
412
 
394
- def on_mount(self) -> None:
395
- """Initialize table display when the widget is mounted.
413
+ def get_row_key(self, row_idx: int) -> RowKey:
414
+ """Get the row key for a given table row index.
396
415
 
397
- Called by Textual when the widget is first added to the display tree.
398
- Currently a placeholder as table setup is deferred until first use.
416
+ Args:
417
+ row_idx: Row index in the table display.
399
418
 
400
419
  Returns:
401
- None
420
+ Corresponding row key as string.
402
421
  """
403
- # self._setup_table()
404
- pass
422
+ return self._row_locations.get_key(row_idx)
423
+
424
+ def get_column_key(self, col_idx: int) -> ColumnKey:
425
+ """Get the column key for a given table column index.
426
+
427
+ Args:
428
+ col_idx: Column index in the table display.
429
+
430
+ Returns:
431
+ Corresponding column key as string.
432
+ """
433
+ return self._column_locations.get_key(col_idx)
405
434
 
406
435
  def _should_highlight(self, cursor: Coordinate, target_cell: Coordinate, type_of_cursor: CursorType) -> bool:
407
436
  """Determine if the given cell should be highlighted because of the cursor.
@@ -486,6 +515,30 @@ class DataFrameTable(DataTable):
486
515
  else:
487
516
  self._scroll_cursor_into_view()
488
517
 
518
+ def move_cursor_to(self, ridx: int, cidx: int) -> None:
519
+ """Move cursor based on the dataframe indices.
520
+
521
+ Args:
522
+ ridx: Row index (0-based) in the dataframe.
523
+ cidx: Column index (0-based) in the dataframe.
524
+ """
525
+ row_key = str(ridx)
526
+ col_key = self.df.columns[cidx]
527
+ row_idx, col_idx = self.get_cell_coordinate(row_key, col_key)
528
+ self.move_cursor(row=row_idx, column=col_idx)
529
+
530
+ def on_mount(self) -> None:
531
+ """Initialize table display when the widget is mounted.
532
+
533
+ Called by Textual when the widget is first added to the display tree.
534
+ Currently a placeholder as table setup is deferred until first use.
535
+
536
+ Returns:
537
+ None
538
+ """
539
+ # self._setup_table()
540
+ pass
541
+
489
542
  def on_key(self, event) -> None:
490
543
  """Handle key press events for pagination.
491
544
 
@@ -544,13 +597,21 @@ class DataFrameTable(DataTable):
544
597
  """Delete the current column."""
545
598
  self._delete_column()
546
599
 
600
+ def action_delete_column_and_after(self) -> None:
601
+ """Delete the current column and those after."""
602
+ self._delete_column(more="after")
603
+
604
+ def action_delete_column_and_before(self) -> None:
605
+ """Delete the current column and those before."""
606
+ self._delete_column(more="before")
607
+
547
608
  def action_hide_column(self) -> None:
548
609
  """Hide the current column."""
549
610
  self._hide_column()
550
611
 
551
- def action_show_column(self) -> None:
552
- """Show all hidden columns."""
553
- self._show_column()
612
+ def action_show_hidden_rows_columns(self) -> None:
613
+ """Show all hidden rows/columns."""
614
+ self._show_hidden_rows_columns()
554
615
 
555
616
  def action_sort_ascending(self) -> None:
556
617
  """Sort by current column in ascending order."""
@@ -656,6 +717,14 @@ class DataFrameTable(DataTable):
656
717
  """Delete the current row."""
657
718
  self._delete_row()
658
719
 
720
+ def action_delete_row_and_below(self) -> None:
721
+ """Delete the current row and those below."""
722
+ self._delete_row(more="below")
723
+
724
+ def action_delete_row_and_up(self) -> None:
725
+ """Delete the current row and those above."""
726
+ self._delete_row(more="above")
727
+
659
728
  def action_duplicate_column(self) -> None:
660
729
  """Duplicate the current column."""
661
730
  self._duplicate_column()
@@ -668,10 +737,14 @@ class DataFrameTable(DataTable):
668
737
  """Undo the last action."""
669
738
  self._undo()
670
739
 
740
+ def action_redo(self) -> None:
741
+ """Redo the last undone action."""
742
+ self._redo()
743
+
671
744
  def action_reset(self) -> None:
672
- """Reset to the original data."""
745
+ """Reset to the initial state."""
673
746
  self._setup_table(reset=True)
674
- self.notify("Restored original display", title="Reset")
747
+ self.notify("Restored initial state", title="Reset")
675
748
 
676
749
  def action_move_column_left(self) -> None:
677
750
  """Move the current column to the left."""
@@ -689,9 +762,9 @@ class DataFrameTable(DataTable):
689
762
  """Move the current row down."""
690
763
  self._move_row("down")
691
764
 
692
- def action_clear_selections(self) -> None:
693
- """Clear all row selections."""
694
- self._clear_selections()
765
+ def action_clear_selections_and_matches(self) -> None:
766
+ """Clear all row selections and matches."""
767
+ self._clear_selections_and_matches()
695
768
 
696
769
  def action_cycle_cursor_type(self) -> None:
697
770
  """Cycle through cursor types."""
@@ -781,41 +854,6 @@ class DataFrameTable(DataTable):
781
854
  """Go to the previous selected row."""
782
855
  self._previous_selected_row()
783
856
 
784
- def _make_cell_clickable(self) -> None:
785
- """Make cells with URLs in the current column clickable.
786
-
787
- Scans all loaded rows in the current column for cells containing URLs
788
- (starting with 'http://' or 'https://') and applies Textual link styling
789
- to make them clickable. Does not modify the dataframe.
790
-
791
- Returns:
792
- None
793
- """
794
- cidx = self.cursor_col_idx
795
- col_key = self.cursor_col_key
796
- dtype = self.df.dtypes[cidx]
797
-
798
- # Only process string columns
799
- if dtype != pl.String:
800
- return
801
-
802
- # Count how many URLs were made clickable
803
- url_count = 0
804
-
805
- # Iterate through all loaded rows and make URLs clickable
806
- for row in self.ordered_rows:
807
- cell_text: Text = self.get_cell(row.key, col_key)
808
- if cell_text.plain.startswith(("http://", "https://")):
809
- cell_text.style = f"#00afff link {cell_text.plain}" # sky blue
810
- self.update_cell(row.key, col_key, cell_text)
811
- url_count += 1
812
-
813
- if url_count:
814
- self.notify(
815
- f"Made [$accent]{url_count}[/] cell(s) clickable in column [$success]{col_key.value}[/]",
816
- title="Make Clickable",
817
- )
818
-
819
857
  def on_mouse_scroll_down(self, event) -> None:
820
858
  """Load more rows when scrolling down with mouse."""
821
859
  self._check_and_load_more()
@@ -909,6 +947,7 @@ class DataFrameTable(DataTable):
909
947
  for row_idx, row in enumerate(df_slice.rows(), start):
910
948
  if not self.visible_rows[row_idx]:
911
949
  continue # Skip hidden rows
950
+
912
951
  vals, dtypes = [], []
913
952
  for val, col, dtype in zip(row, self.df.columns, self.df.dtypes):
914
953
  if col in self.hidden_columns:
@@ -916,6 +955,7 @@ class DataFrameTable(DataTable):
916
955
  vals.append(val)
917
956
  dtypes.append(dtype)
918
957
  formatted_row = format_row(vals, dtypes, thousand_separator=self.thousand_separator)
958
+
919
959
  # Always add labels so they can be shown/hidden via CSS
920
960
  self.add_row(*formatted_row, key=str(row_idx), label=str(row_idx + 1))
921
961
 
@@ -937,51 +977,59 @@ class DataFrameTable(DataTable):
937
977
  if bottom_visible_row >= self.loaded_rows - 10:
938
978
  self._load_rows(self.loaded_rows + self.BATCH_SIZE)
939
979
 
940
- def _do_highlight(self, clear: bool = False) -> None:
980
+ def _do_highlight(self, force: bool = False) -> None:
941
981
  """Update all rows, highlighting selected ones and restoring others to default.
942
982
 
943
983
  Args:
944
- clear: If True, clear all highlights.
984
+ force: If True, clear all highlights and restore default styles.
945
985
  """
946
- if clear:
947
- self.selected_rows = [False] * len(self.df)
948
- self.matches = defaultdict(set)
949
-
950
986
  # Ensure all selected rows or matches are loaded
951
987
  stop = rindex(self.selected_rows, True) + 1
952
988
  stop = max(stop, max(self.matches.keys(), default=0) + 1)
953
989
 
954
990
  self._load_rows(stop)
955
- self._highlight_table()
991
+ self._highlight_table(force)
956
992
 
957
- def _highlight_table(self) -> None:
993
+ def _highlight_table(self, force: bool = False) -> None:
958
994
  """Highlight selected rows/cells in red."""
995
+ if not force and not any(self.selected_rows) and not self.matches:
996
+ return # Nothing to highlight
997
+
959
998
  # Update all rows based on selected state
960
999
  for row in self.ordered_rows:
961
- row_idx = int(row.key.value) # 0-based index
962
- is_selected = self.selected_rows[row_idx]
963
- match_cols = self.matches.get(row_idx, set())
1000
+ ridx = int(row.key.value) # 0-based index
1001
+ is_selected = self.selected_rows[ridx]
1002
+ match_cols = self.matches.get(ridx, set())
1003
+
1004
+ if not force and not is_selected and not match_cols:
1005
+ continue # No highlight needed for this row
964
1006
 
965
1007
  # Update all cells in this row
966
1008
  for col_idx, col in enumerate(self.ordered_columns):
967
- cell_text: Text = self.get_cell(row.key, col.key)
1009
+ if not force and not is_selected and col_idx not in match_cols:
1010
+ continue # No highlight needed for this cell
968
1011
 
969
- # Get style config based on dtype
970
- dtype = self.df.dtypes[col_idx]
971
- dc = DtypeConfig(dtype)
972
- cell_text.style = "red" if is_selected or col_idx in match_cols else dc.style
1012
+ cell_text: Text = self.get_cell(row.key, col.key)
1013
+ need_update = False
1014
+
1015
+ if is_selected or col_idx in match_cols:
1016
+ cell_text.style = "red"
1017
+ need_update = True
1018
+ elif force:
1019
+ # Restore original style based on dtype
1020
+ dtype = self.df.schema[col.key.value]
1021
+ dc = DtypeConfig(dtype)
1022
+ cell_text.style = dc.style
1023
+ need_update = True
973
1024
 
974
1025
  # Update the cell in the table
975
- self.update_cell(row.key, col.key, cell_text)
1026
+ if need_update:
1027
+ self.update_cell(row.key, col.key, cell_text)
976
1028
 
977
1029
  # History & Undo
978
- def _add_history(self, description: str) -> None:
979
- """Add the current state to the history stack.
980
-
981
- Args:
982
- description: Description of the action for this history entry.
983
- """
984
- history = History(
1030
+ def _create_history(self, description: str) -> None:
1031
+ """Create the initial history state."""
1032
+ return History(
985
1033
  description=description,
986
1034
  df=self.df,
987
1035
  filename=self.filename,
@@ -995,16 +1043,12 @@ class DataFrameTable(DataTable):
995
1043
  cursor_coordinate=self.cursor_coordinate,
996
1044
  matches={k: v.copy() for k, v in self.matches.items()},
997
1045
  )
998
- self.histories.append(history)
999
1046
 
1000
- def _undo(self) -> None:
1001
- """Undo the last action."""
1002
- if not self.histories:
1003
- self.notify("No actions to undo", title="Undo", severity="warning")
1047
+ def _apply_history(self, history: History) -> None:
1048
+ """Apply the current history state to the table."""
1049
+ if history is None:
1004
1050
  return
1005
1051
 
1006
- history = self.histories.pop()
1007
-
1008
1052
  # Restore state
1009
1053
  self.df = history.df
1010
1054
  self.filename = history.filename
@@ -1016,13 +1060,54 @@ class DataFrameTable(DataTable):
1016
1060
  self.fixed_rows = history.fixed_rows
1017
1061
  self.fixed_columns = history.fixed_columns
1018
1062
  self.cursor_coordinate = history.cursor_coordinate
1019
- self.matches = {k: v.copy() for k, v in history.matches.items()}
1063
+ self.matches = {k: v.copy() for k, v in history.matches.items()} if history.matches else defaultdict(set)
1020
1064
 
1021
1065
  # Recreate the table for display
1022
1066
  self._setup_table()
1023
1067
 
1068
+ def _add_history(self, description: str) -> None:
1069
+ """Add the current state to the history stack.
1070
+
1071
+ Args:
1072
+ description: Description of the action for this history entry.
1073
+ """
1074
+ history = self._create_history(description)
1075
+ self.histories.append(history)
1076
+
1077
+ def _undo(self) -> None:
1078
+ """Undo the last action."""
1079
+ if not self.histories:
1080
+ self.notify("No actions to undo", title="Undo", severity="warning")
1081
+ return
1082
+
1083
+ # Save current state for redo
1084
+ self.history = self._create_history("Redo state")
1085
+
1086
+ # Pop the last history state for undo
1087
+ history = self.histories.pop()
1088
+
1089
+ # Restore state
1090
+ self._apply_history(history)
1091
+
1024
1092
  # self.notify(f"Reverted: {history.description}", title="Undo")
1025
1093
 
1094
+ def _redo(self) -> None:
1095
+ """Redo the last undone action."""
1096
+ if self.history is None:
1097
+ self.notify("No actions to redo", title="Redo", severity="warning")
1098
+ return
1099
+
1100
+ # Save current state for undo
1101
+ self._add_history("Undo state")
1102
+
1103
+ # Restore state
1104
+ self._apply_history(self.history)
1105
+
1106
+ # Clear redo state
1107
+ self.history = None
1108
+
1109
+ # self.notify(f"Reapplied: {history.description}", title="Redo")
1110
+
1026
1111
  # View
1027
1112
  def _view_row_detail(self) -> None:
1028
1113
  """Open a modal screen to view the selected row's details."""
@@ -1071,9 +1156,9 @@ class DataFrameTable(DataTable):
1071
1156
  self._add_history(f"Pinned [$accent]{fixed_rows}[/] rows and [$success]{fixed_columns}[/] columns")
1072
1157
 
1073
1158
  # Apply the pin settings to the table
1074
- if fixed_rows > 0:
1159
+ if fixed_rows >= 0:
1075
1160
  self.fixed_rows = fixed_rows
1076
- if fixed_columns > 0:
1161
+ if fixed_columns >= 0:
1077
1162
  self.fixed_columns = fixed_columns
1078
1163
 
1079
1164
  self.notify(
@@ -1082,38 +1167,69 @@ class DataFrameTable(DataTable):
1082
1167
  )
1083
1168
 
1084
1169
  # Delete & Move
1085
- def _delete_column(self) -> None:
1170
+ def _delete_column(self, more: str = None) -> None:
1086
1171
  """Remove the currently selected column from the table."""
1087
1172
  # Get the column to remove
1088
1173
  col_idx = self.cursor_column
1089
1174
  col_name = self.cursor_col_name
1090
1175
  col_key = self.cursor_col_key
1091
1176
 
1177
+ col_names_to_remove = []
1178
+ col_keys_to_remove = []
1179
+
1180
+ # Remove all columns before the current column
1181
+ if more == "before":
1182
+ for i in range(col_idx + 1):
1183
+ col_key = self.get_column_key(i)
1184
+ col_names_to_remove.append(col_key.value)
1185
+ col_keys_to_remove.append(col_key)
1186
+
1187
+ descr = f"Removed column [$success]{col_name}[/] and all columns before"
1188
+
1189
+ # Remove all columns after the current column
1190
+ elif more == "after":
1191
+ for i in range(col_idx, len(self.columns)):
1192
+ col_key = self.get_column_key(i)
1193
+ col_names_to_remove.append(col_key.value)
1194
+ col_keys_to_remove.append(col_key)
1195
+
1196
+ descr = f"Removed column [$success]{col_name}[/] and all columns after"
1197
+
1198
+ # Remove only the current column
1199
+ else:
1200
+ col_names_to_remove.append(col_name)
1201
+ col_keys_to_remove.append(col_key)
1202
+ descr = f"Removed column [$success]{col_name}[/]"
1203
+
1092
1204
  # Add to history
1093
- self._add_history(f"Removed column [$success]{col_name}[/]")
1205
+ self._add_history(descr)
1094
1206
 
1095
- # Remove the column from the table display using the column name as key
1096
- self.remove_column(col_key)
1207
+ # Remove the columns from the table display using the column names as keys
1208
+ for ck in col_keys_to_remove:
1209
+ self.remove_column(ck)
1097
1210
 
1098
- # Move cursor left if we deleted the last column
1099
- if col_idx >= len(self.columns):
1100
- self.move_cursor(column=len(self.columns) - 1)
1211
+ # Move cursor left if we deleted the last column(s)
1212
+ last_col_idx = len(self.columns) - 1
1213
+ if col_idx > last_col_idx:
1214
+ self.move_cursor(column=last_col_idx)
1101
1215
 
1102
1216
  # Remove from sorted columns if present
1103
- if col_name in self.sorted_columns:
1104
- del self.sorted_columns[col_name]
1217
+ for col_name in col_names_to_remove:
1218
+ if col_name in self.sorted_columns:
1219
+ del self.sorted_columns[col_name]
1105
1220
 
1106
1221
  # Remove from matches
1222
+ col_indices_to_remove = set(self.df.columns.index(name) for name in col_names_to_remove)
1107
1223
  for row_idx in list(self.matches.keys()):
1108
- self.matches[row_idx].discard(col_idx)
1224
+ self.matches[row_idx].difference_update(col_indices_to_remove)
1109
1225
  # Remove empty entries
1110
1226
  if not self.matches[row_idx]:
1111
1227
  del self.matches[row_idx]
1112
1228
 
1113
1229
  # Remove from dataframe
1114
- self.df = self.df.drop(col_name)
1230
+ self.df = self.df.drop(col_names_to_remove)
1115
1231
 
1116
- self.notify(f"Removed column [$success]{col_name}[/]", title="Delete")
1232
+ # self.notify(descr, title="Delete")
1117
1233
 
1118
1234
  def _hide_column(self) -> None:
1119
1235
  """Hide the currently selected column from the table display."""
@@ -1136,28 +1252,32 @@ class DataFrameTable(DataTable):
1136
1252
 
1137
1253
  # self.notify(f"Hid column [$accent]{col_name}[/]. Press [$success]H[/] to show hidden columns", title="Hide")
1138
1254
 
1139
- def _show_column(self) -> None:
1140
- """Show all hidden columns by recreating the table with all dataframe columns."""
1255
+ def _show_hidden_rows_columns(self) -> None:
1256
+ """Show all hidden rows/columns by recreating the table."""
1141
1257
  # Get currently visible columns
1142
1258
  visible_cols = set(col.key for col in self.ordered_columns)
1143
1259
 
1144
- # Find hidden columns (in dataframe but not in table)
1145
- hidden_cols = [col for col in self.df.columns if col not in visible_cols]
1260
+ hidden_row_count = sum(0 if visible else 1 for visible in self.visible_rows)
1261
+ hidden_col_count = sum(0 if col in visible_cols else 1 for col in self.df.columns)
1146
1262
 
1147
- if not hidden_cols:
1148
- self.notify("No hidden columns to show", title="Column", severity="warning")
1263
+ if not hidden_row_count and not hidden_col_count:
1264
+ self.notify("No hidden columns or rows to show", title="Show", severity="warning")
1149
1265
  return
1150
1266
 
1151
1267
  # Add to history
1152
- self._add_history(f"Showed {len(hidden_cols)} hidden column(s)")
1268
+ self._add_history("Showed hidden rows/columns")
1153
1269
 
1154
- # Clear hidden columns tracking
1270
+ # Clear hidden rows/columns tracking
1271
+ self.visible_rows = [True] * len(self.df)
1155
1272
  self.hidden_columns.clear()
1156
1273
 
1157
- # Recreate table with all columns
1274
+ # Recreate table for display
1158
1275
  self._setup_table()
1159
1276
 
1160
- self.notify(f"Showed [$accent]{len(hidden_cols)}[/] hidden column(s)", title="Column")
1277
+ self.notify(
1278
+ f"Showed [$accent]{hidden_row_count}[/] hidden row(s) and/or [$accent]{hidden_col_count}[/] hidden column(s)",
1279
+ title="Show",
1280
+ )
1161
1281
 
1162
1282
  def _duplicate_column(self) -> None:
1163
1283
  """Duplicate the currently selected column, inserting it right after the current column."""
@@ -1179,6 +1299,18 @@ class DataFrameTable(DataTable):
1179
1299
  list(cols_before) + [new_col_name] + list(cols_after)
1180
1300
  )
1181
1301
 
1302
+ # Update matches to account for new column
1303
+ new_matches = defaultdict(set)
1304
+ for row_idx, cols in self.matches.items():
1305
+ new_cols = set()
1306
+ for col_idx_in_set in cols:
1307
+ if col_idx_in_set <= cidx:
1308
+ new_cols.add(col_idx_in_set)
1309
+ else:
1310
+ new_cols.add(col_idx_in_set + 1)
1311
+ new_matches[row_idx] = new_cols
1312
+ self.matches = new_matches
1313
+
1182
1314
  # Recreate the table for display
1183
1315
  self._setup_table()
1184
1316
 
@@ -1190,7 +1322,7 @@ class DataFrameTable(DataTable):
1190
1322
  title="Duplicate",
1191
1323
  )
1192
1324
 
1193
- def _delete_row(self) -> None:
1325
+ def _delete_row(self, more: str = None) -> None:
1194
1326
  """Delete rows from the table and dataframe.
1195
1327
 
1196
1328
  Supports deleting multiple selected rows. If no rows are selected, deletes the row at the cursor.
@@ -1206,11 +1338,27 @@ class DataFrameTable(DataTable):
1206
1338
  if selected:
1207
1339
  predicates[ridx] = False
1208
1340
 
1341
+ # Delete current row and those above
1342
+ elif more == "above":
1343
+ ridx = self.cursor_row_idx
1344
+ history_desc = f"Deleted current row [$success]{ridx + 1}[/] and those above"
1345
+ for i in range(ridx + 1):
1346
+ predicates[i] = False
1347
+
1348
+ # Delete current row and those below
1349
+ elif more == "below":
1350
+ ridx = self.cursor_row_idx
1351
+ history_desc = f"Deleted current row [$success]{ridx + 1}[/] and those below"
1352
+ for i in range(ridx, len(self.df)):
1353
+ if self.visible_rows[i]:
1354
+ predicates[i] = False
1355
+
1209
1356
  # Delete the row at the cursor
1210
1357
  else:
1211
1358
  ridx = self.cursor_row_idx
1212
1359
  history_desc = f"Deleted row [$success]{ridx + 1}[/]"
1213
- predicates[ridx] = False
1360
+ if self.visible_rows[ridx]:
1361
+ predicates[ridx] = False
1214
1362
 
1215
1363
  # Add to history
1216
1364
  self._add_history(history_desc)
@@ -1238,7 +1386,7 @@ class DataFrameTable(DataTable):
1238
1386
 
1239
1387
  deleted_count = old_count - len(self.df)
1240
1388
  if deleted_count > 1:
1241
- self.notify(f"Deleted {deleted_count} row(s)", title="Delete")
1389
+ self.notify(f"Deleted [$accent]{deleted_count}[/] row(s)", title="Delete")
1242
1390
 
1243
1391
  def _duplicate_row(self) -> None:
1244
1392
  """Duplicate the currently selected row, inserting it right after the current row."""
@@ -1263,8 +1411,14 @@ class DataFrameTable(DataTable):
1263
1411
  self.selected_rows = new_selected_rows
1264
1412
  self.visible_rows = new_visible_rows
1265
1413
 
1266
- # Clear all matches since row indices have changed
1267
- self.matches = defaultdict(set)
1414
+ # Update matches to account for new row
1415
+ new_matches = defaultdict(set)
1416
+ for row_idx, cols in self.matches.items():
1417
+ if row_idx <= ridx:
1418
+ new_matches[row_idx] = cols
1419
+ else:
1420
+ new_matches[row_idx + 1] = cols
1421
+ self.matches = new_matches
1268
1422
 
1269
1423
  # Recreate the table display
1270
1424
  self._setup_table()
@@ -2072,18 +2226,6 @@ class DataFrameTable(DataTable):
2072
2226
  title="Global Find",
2073
2227
  )
2074
2228
 
2075
- def _move_cursor(self, ridx: int, cidx: int) -> None:
2076
- """Move cursor based on the dataframe indices.
2077
-
2078
- Args:
2079
- ridx: Row index (0-based) in the dataframe.
2080
- cidx: Column index (0-based) in the dataframe.
2081
- """
2082
- row_key = str(ridx)
2083
- col_key = self.df.columns[cidx]
2084
- row_idx, col_idx = self.get_cell_coordinate(row_key, col_key)
2085
- self.move_cursor(row=row_idx, column=col_idx)
2086
-
2087
2229
  def _next_match(self) -> None:
2088
2230
  """Move cursor to the next match."""
2089
2231
  if not self.matches:
@@ -2099,12 +2241,12 @@ class DataFrameTable(DataTable):
2099
2241
  # Find the next match after current position
2100
2242
  for ridx, cidx in ordered_matches:
2101
2243
  if (ridx, cidx) > current_pos:
2102
- self._move_cursor(ridx, cidx)
2244
+ self.move_cursor_to(ridx, cidx)
2103
2245
  return
2104
2246
 
2105
2247
  # If no next match, wrap around to the first match
2106
2248
  first_ridx, first_cidx = ordered_matches[0]
2107
- self._move_cursor(first_ridx, first_cidx)
2249
+ self.move_cursor_to(first_ridx, first_cidx)
2108
2250
 
2109
2251
  def _previous_match(self) -> None:
2110
2252
  """Move cursor to the previous match."""
@@ -2149,12 +2291,12 @@ class DataFrameTable(DataTable):
2149
2291
  # Find the next selected row after current position
2150
2292
  for ridx in selected_row_indices:
2151
2293
  if ridx > current_ridx:
2152
- self._move_cursor(ridx, self.cursor_col_idx)
2294
+ self.move_cursor_to(ridx, self.cursor_col_idx)
2153
2295
  return
2154
2296
 
2155
2297
  # If no next selected row, wrap around to the first selected row
2156
2298
  first_ridx = selected_row_indices[0]
2157
- self._move_cursor(first_ridx, self.cursor_col_idx)
2299
+ self.move_cursor_to(first_ridx, self.cursor_col_idx)
2158
2300
 
2159
2301
  def _previous_selected_row(self) -> None:
2160
2302
  """Move cursor to the previous selected row."""
@@ -2171,12 +2313,12 @@ class DataFrameTable(DataTable):
2171
2313
  # Find the previous selected row before current position
2172
2314
  for ridx in reversed(selected_row_indices):
2173
2315
  if ridx < current_ridx:
2174
- self._move_cursor(ridx, self.cursor_col_idx)
2316
+ self.move_cursor_to(ridx, self.cursor_col_idx)
2175
2317
  return
2176
2318
 
2177
2319
  # If no previous selected row, wrap around to the last selected row
2178
2320
  last_ridx = selected_row_indices[-1]
2179
- self._move_cursor(last_ridx, self.cursor_col_idx)
2321
+ self.move_cursor_to(last_ridx, self.cursor_col_idx)
2180
2322
 
2181
2323
  def _replace(self) -> None:
2182
2324
  """Open replace screen for current column."""
@@ -2459,8 +2601,8 @@ class DataFrameTable(DataTable):
2459
2601
  title="Toggle",
2460
2602
  )
2461
2603
 
2462
- # Refresh the highlighting (also restores default styles for unselected rows)
2463
- self._do_highlight()
2604
+ # Refresh the highlighting
2605
+ self._do_highlight(force=True)
2464
2606
 
2465
2607
  def _make_selections(self) -> None:
2466
2608
  """Make selections based on current matches or toggle current row selection."""
@@ -2481,10 +2623,10 @@ class DataFrameTable(DataTable):
2481
2623
  self.notify(f"Selected [$accent]{new_selected_count}[/] rows", title="Toggle")
2482
2624
 
2483
2625
  # Refresh the highlighting (also restores default styles for unselected rows)
2484
- self._do_highlight()
2626
+ self._do_highlight(force=True)
2485
2627
 
2486
- def _clear_selections(self) -> None:
2487
- """Clear all selected rows without removing them from the dataframe."""
2628
+ def _clear_selections_and_matches(self) -> None:
2629
+ """Clear all selected rows and matches without removing them from the dataframe."""
2488
2630
  # Check if any selected rows or matches
2489
2631
  if not any(self.selected_rows) and not self.matches:
2490
2632
  self.notify("No selections to clear", title="Clear", severity="warning")
@@ -2497,8 +2639,12 @@ class DataFrameTable(DataTable):
2497
2639
  # Save current state to history
2498
2640
  self._add_history("Cleared all selected rows")
2499
2641
 
2500
- # Clear all selections and refresh highlighting
2501
- self._do_highlight(clear=True)
2642
+ # Clear all selections
2643
+ self.selected_rows = [False] * len(self.df)
2644
+ self.matches = defaultdict(set)
2645
+
2646
+ # Refresh the highlighting to remove all highlights
2647
+ self._do_highlight(force=True)
2502
2648
 
2503
2649
  self.notify(f"Cleared selections for [$accent]{row_count}[/] rows", title="Clear")
2504
2650
 
@@ -2766,3 +2912,38 @@ class DataFrameTable(DataTable):
2766
2912
  f"Saved current tab with [$accent]{len(self.df)}[/] rows to [$success]{filename}[/]",
2767
2913
  title="Save",
2768
2914
  )
2915
+
2916
+ def _make_cell_clickable(self) -> None:
2917
+ """Make cells with URLs in the current column clickable.
2918
+
2919
+ Scans all loaded rows in the current column for cells containing URLs
2920
+ (starting with 'http://' or 'https://') and applies Textual link styling
2921
+ to make them clickable. Does not modify the dataframe.
2922
+
2923
+ Returns:
2924
+ None
2925
+ """
2926
+ cidx = self.cursor_col_idx
2927
+ col_key = self.cursor_col_key
2928
+ dtype = self.df.dtypes[cidx]
2929
+
2930
+ # Only process string columns
2931
+ if dtype != pl.String:
2932
+ return
2933
+
2934
+ # Count how many URLs were made clickable
2935
+ url_count = 0
2936
+
2937
+ # Iterate through all loaded rows and make URLs clickable
2938
+ for row in self.ordered_rows:
2939
+ cell_text: Text = self.get_cell(row.key, col_key)
2940
+ if cell_text.plain.startswith(("http://", "https://")):
2941
+ cell_text.style = f"#00afff link {cell_text.plain}" # sky blue
2942
+ self.update_cell(row.key, col_key, cell_text)
2943
+ url_count += 1
2944
+
2945
+ if url_count:
2946
+ self.notify(
2947
+ f"Made [$accent]{url_count}[/] cell(s) clickable in column [$success]{col_key.value}[/]",
2948
+ title="Make Clickable",
2949
+ )
@@ -30,8 +30,10 @@ class TableScreen(ModalScreen):
30
30
 
31
31
  TableScreen > DataTable {
32
32
  width: auto;
33
- min-width: 20;
33
+ height: auto;
34
34
  border: solid $primary;
35
+ max-width: 100%;
36
+ overflow: auto;
35
37
  }
36
38
  """
37
39
 
@@ -291,6 +293,10 @@ class StatisticsScreen(TableScreen):
291
293
  if False in self.dftable.visible_rows:
292
294
  lf = lf.filter(self.dftable.visible_rows)
293
295
 
296
+ # Apply only to non-hidden columns
297
+ if self.dftable.hidden_columns:
298
+ lf = lf.select(pl.exclude(self.dftable.hidden_columns))
299
+
294
300
  # Get dataframe statistics
295
301
  stats_df = lf.collect().describe()
296
302
 
@@ -171,7 +171,7 @@ wheels = [
171
171
 
172
172
  [[package]]
173
173
  name = "dataframe-textual"
174
- version = "1.1.4"
174
+ version = "1.2.0"
175
175
  source = { editable = "." }
176
176
  dependencies = [
177
177
  { name = "polars" },
Binary file