dataframe-textual 2.0.1__tar.gz → 2.1.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: 2.0.1
3
+ Version: 2.1.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
@@ -247,8 +247,8 @@ zcat compressed_data.csv.gz | dv -f csv
247
247
  | `Q` | Close all tabs and app (prompts to save unsaved changes) |
248
248
  | `Ctrl+Q` | Force to quit app (regardless of unsaved changes) |
249
249
  | `Ctrl+T` | Save current tab to file |
250
+ | `Ctrl+A` | Save all tabs to a Excel file |
250
251
  | `w` | Save current tab to file (overwrite without prompt) |
251
- | `Ctrl+A` | Save all tabs to file |
252
252
  | `W` | Save all tabs to file (overwrite without prompt) |
253
253
  | `Ctrl+D` | Duplicate current tab |
254
254
  | `Ctrl+O` | Open file in a new tab |
@@ -271,7 +271,7 @@ zcat compressed_data.csv.gz | dv -f csv
271
271
  | Key | Action |
272
272
  |-----|--------|
273
273
  | `g` | Jump to first row |
274
- | `G` | Jump to last row (loads all remaining rows) |
274
+ | `G` | Jump to last row |
275
275
  | `↑` / `↓` | Move up/down one row |
276
276
  | `←` / `→` | Move left/right one column |
277
277
  | `Home` / `End` | Jump to first/last column |
@@ -295,9 +295,11 @@ zcat compressed_data.csv.gz | dv -f csv
295
295
  | `F` | Show frequency distribution for current column |
296
296
  | `s` | Show statistics for current column |
297
297
  | `S` | Show statistics for entire dataframe |
298
+ | `m` | Show metadata for row count and column count |
299
+ | `M` | Show metadata for current column |
298
300
  | `K` | Cycle cursor types: cell → row → column → cell |
299
301
  | `~` | Toggle row labels |
300
- | `_` (underscore) | Expand column to full width |
302
+ | `_` (underscore) | Toggle column full width |
301
303
  | `z` | Freeze rows and columns |
302
304
  | `,` | Toggle thousand separator for numeric display |
303
305
  | `h` | Hide current column |
@@ -318,7 +320,7 @@ zcat compressed_data.csv.gz | dv -f csv
318
320
  | `x` | Delete current row |
319
321
  | `X` | Delete current row and all those below |
320
322
  | `Ctrl+X` | Delete current row and all those above |
321
- | `d` | Duplicate current column (appends '_copy' suffix) |
323
+ | `d` | Duplicate current column |
322
324
  | `D` | Duplicate current row |
323
325
 
324
326
  #### Row Selection
@@ -392,7 +394,7 @@ zcat compressed_data.csv.gz | dv -f csv
392
394
  | `c` | Copy current cell to clipboard |
393
395
  | `Ctrl+C` | Copy column to clipboard |
394
396
  | `Ctrl+R` | Copy row to clipboard (tab-separated) |
395
- | `Ctrl+S` | Save current tab to file |
397
+ | `Ctrl+S` | Save to file |
396
398
 
397
399
  ## Features in Detail
398
400
 
@@ -413,8 +415,8 @@ Useful for examining wide datasets where columns don't fit well on screen.
413
415
  **In the Row Detail Modal**:
414
416
  - Press `v` to **view** all rows containing the selected column value (others hidden but preserved)
415
417
  - Press `"` to **filter** all rows containing the selected column value (others removed)
416
- - Press `{` to move to the **previous row** (respects hidden rows)
417
- - Press `}` to move to the **next row** (respects hidden rows)
418
+ - Press `{` to move to the previous row
419
+ - Press `}` to move to the next row
418
420
  - Press `q` or `Escape` to close the modal
419
421
 
420
422
  ### 3. Row Selection
@@ -429,7 +431,7 @@ The application provides multiple modes for selecting rows (marks it for filteri
429
431
  - `{` - Go to previous selected row
430
432
  - `}` - Go to next selected row
431
433
 
432
- **Advanced Matching Options**:
434
+ **Advanced Options**:
433
435
 
434
436
  When searching or finding, you can use checkboxes in the dialog to enable:
435
437
  - **Match Nocase**: Ignore case differences
@@ -445,7 +447,7 @@ These options work with plain text searches. Use Polars regex patterns in expres
445
447
  - Use `u` to undo any search or filter
446
448
 
447
449
  ### 4. Find & Replace
448
- **Find Operations** - Find by value/expression and highlight matching cells:
450
+ Find by value/expression and highlight matching cells:
449
451
  - `/` - Find cursor value within current column (respects data type)
450
452
  - `?` - Open dialog to search current column with expression
451
453
  - `;` - Find cursor value across all columns
@@ -469,10 +471,10 @@ When you press `r` or `R`, enter:
469
471
 
470
472
  **Replace Interactive**:
471
473
  - Review each match one at a time (confirm, skip, or cancel)
472
- - Shows progress: `X of Y`
474
+ - Shows progress
473
475
 
474
476
  **Tips:**
475
- - Search are done by string value (i.e. ignoring data type)
477
+ - Search are done by string value (i.e., ignoring data type)
476
478
  - Type `NULL` to replace null/missing values
477
479
  - Use `Match Nocase` for case-insensitive matching
478
480
  - Use `Match Whole` to avoid partial replacements
@@ -480,7 +482,7 @@ When you press `r` or `R`, enter:
480
482
 
481
483
  ### 5. Filter vs. View
482
484
 
483
- Both operations show rows that are selected or contain matching cells, but with fundamentally different effects:
485
+ Both operations show selected rows but with fundamentally different effects:
484
486
 
485
487
  | Operation | Keyboard | Effect | Data Preserved |
486
488
  |-----------|----------|--------|-----------------|
@@ -571,8 +573,8 @@ View quick metadata about your dataframe and columns to understand their structu
571
573
 
572
574
  **Dataframe Metadata** (`m`):
573
575
  - Press `m` to open a modal displaying:
574
- - **Rows** - Total number of rows in the dataframe
575
- - **Columns** - Total number of columns in the dataframe
576
+ - **Row** - Total number of rows in the dataframe
577
+ - **Column** - Total number of columns in the dataframe
576
578
 
577
579
  **Column Metadata** (`M`):
578
580
  - Press `M` to open a modal displaying details for all columns:
@@ -751,7 +753,6 @@ SELECT specific columns and apply WHERE conditions without writing full SQL:
751
753
  #### Advanced SQL Interface (`L`)
752
754
  Execute complete SQL queries for advanced data manipulation:
753
755
  - Write full SQL queries with standard [SQL syntax](https://docs.pola.rs/api/python/stable/reference/sql/index.html)
754
- - Support for JOINs, GROUP BY, aggregations, and more
755
756
  - Access to all SQL capabilities for complex transformations
756
757
  - Always use `self` as the table name
757
758
  - Syntax highlighted
@@ -773,7 +774,7 @@ WHERE `product id` = 7
773
774
 
774
775
  Copies value to system clipboard with `pbcopy` on macOS and `xclip` on Linux.
775
776
 
776
- **Note** May require a X server to work.
777
+ **Note**: may require a X server to work.
777
778
 
778
779
  - Press `c` to copy cursor value
779
780
  - Press `Ctrl+C` to copy column values
@@ -815,8 +816,8 @@ Manage multiple files and dataframes simultaneously with tabs.
815
816
  - **`Double-click`** - Rename the tab
816
817
  - **`Ctrl+D`** - Duplicate current tab (creates a copy with same data and state)
817
818
  - **`Ctrl+T`** - Save current tab to file
818
- - **`w`** - Save current tab to file (overwrite without prompt)
819
819
  - **`Ctrl+A`** - Save all tabs in a single Excel file
820
+ - **`w`** - Save current tab to file (overwrite without prompt)
820
821
  - **`W`** - Save all tabs to file (overwrite without prompt)
821
822
  - **`q`** - Close current tab (closes tab, prompts to save if unsaved changes)
822
823
  - **`Q`** - Close all tabs and exit app (prompts to save tabs with unsaved changes)
@@ -208,8 +208,8 @@ zcat compressed_data.csv.gz | dv -f csv
208
208
  | `Q` | Close all tabs and app (prompts to save unsaved changes) |
209
209
  | `Ctrl+Q` | Force to quit app (regardless of unsaved changes) |
210
210
  | `Ctrl+T` | Save current tab to file |
211
+ | `Ctrl+A` | Save all tabs to a Excel file |
211
212
  | `w` | Save current tab to file (overwrite without prompt) |
212
- | `Ctrl+A` | Save all tabs to file |
213
213
  | `W` | Save all tabs to file (overwrite without prompt) |
214
214
  | `Ctrl+D` | Duplicate current tab |
215
215
  | `Ctrl+O` | Open file in a new tab |
@@ -232,7 +232,7 @@ zcat compressed_data.csv.gz | dv -f csv
232
232
  | Key | Action |
233
233
  |-----|--------|
234
234
  | `g` | Jump to first row |
235
- | `G` | Jump to last row (loads all remaining rows) |
235
+ | `G` | Jump to last row |
236
236
  | `↑` / `↓` | Move up/down one row |
237
237
  | `←` / `→` | Move left/right one column |
238
238
  | `Home` / `End` | Jump to first/last column |
@@ -256,9 +256,11 @@ zcat compressed_data.csv.gz | dv -f csv
256
256
  | `F` | Show frequency distribution for current column |
257
257
  | `s` | Show statistics for current column |
258
258
  | `S` | Show statistics for entire dataframe |
259
+ | `m` | Show metadata for row count and column count |
260
+ | `M` | Show metadata for current column |
259
261
  | `K` | Cycle cursor types: cell → row → column → cell |
260
262
  | `~` | Toggle row labels |
261
- | `_` (underscore) | Expand column to full width |
263
+ | `_` (underscore) | Toggle column full width |
262
264
  | `z` | Freeze rows and columns |
263
265
  | `,` | Toggle thousand separator for numeric display |
264
266
  | `h` | Hide current column |
@@ -279,7 +281,7 @@ zcat compressed_data.csv.gz | dv -f csv
279
281
  | `x` | Delete current row |
280
282
  | `X` | Delete current row and all those below |
281
283
  | `Ctrl+X` | Delete current row and all those above |
282
- | `d` | Duplicate current column (appends '_copy' suffix) |
284
+ | `d` | Duplicate current column |
283
285
  | `D` | Duplicate current row |
284
286
 
285
287
  #### Row Selection
@@ -353,7 +355,7 @@ zcat compressed_data.csv.gz | dv -f csv
353
355
  | `c` | Copy current cell to clipboard |
354
356
  | `Ctrl+C` | Copy column to clipboard |
355
357
  | `Ctrl+R` | Copy row to clipboard (tab-separated) |
356
- | `Ctrl+S` | Save current tab to file |
358
+ | `Ctrl+S` | Save to file |
357
359
 
358
360
  ## Features in Detail
359
361
 
@@ -374,8 +376,8 @@ Useful for examining wide datasets where columns don't fit well on screen.
374
376
  **In the Row Detail Modal**:
375
377
  - Press `v` to **view** all rows containing the selected column value (others hidden but preserved)
376
378
  - Press `"` to **filter** all rows containing the selected column value (others removed)
377
- - Press `{` to move to the **previous row** (respects hidden rows)
378
- - Press `}` to move to the **next row** (respects hidden rows)
379
+ - Press `{` to move to the previous row
380
+ - Press `}` to move to the next row
379
381
  - Press `q` or `Escape` to close the modal
380
382
 
381
383
  ### 3. Row Selection
@@ -390,7 +392,7 @@ The application provides multiple modes for selecting rows (marks it for filteri
390
392
  - `{` - Go to previous selected row
391
393
  - `}` - Go to next selected row
392
394
 
393
- **Advanced Matching Options**:
395
+ **Advanced Options**:
394
396
 
395
397
  When searching or finding, you can use checkboxes in the dialog to enable:
396
398
  - **Match Nocase**: Ignore case differences
@@ -406,7 +408,7 @@ These options work with plain text searches. Use Polars regex patterns in expres
406
408
  - Use `u` to undo any search or filter
407
409
 
408
410
  ### 4. Find & Replace
409
- **Find Operations** - Find by value/expression and highlight matching cells:
411
+ Find by value/expression and highlight matching cells:
410
412
  - `/` - Find cursor value within current column (respects data type)
411
413
  - `?` - Open dialog to search current column with expression
412
414
  - `;` - Find cursor value across all columns
@@ -430,10 +432,10 @@ When you press `r` or `R`, enter:
430
432
 
431
433
  **Replace Interactive**:
432
434
  - Review each match one at a time (confirm, skip, or cancel)
433
- - Shows progress: `X of Y`
435
+ - Shows progress
434
436
 
435
437
  **Tips:**
436
- - Search are done by string value (i.e. ignoring data type)
438
+ - Search are done by string value (i.e., ignoring data type)
437
439
  - Type `NULL` to replace null/missing values
438
440
  - Use `Match Nocase` for case-insensitive matching
439
441
  - Use `Match Whole` to avoid partial replacements
@@ -441,7 +443,7 @@ When you press `r` or `R`, enter:
441
443
 
442
444
  ### 5. Filter vs. View
443
445
 
444
- Both operations show rows that are selected or contain matching cells, but with fundamentally different effects:
446
+ Both operations show selected rows but with fundamentally different effects:
445
447
 
446
448
  | Operation | Keyboard | Effect | Data Preserved |
447
449
  |-----------|----------|--------|-----------------|
@@ -532,8 +534,8 @@ View quick metadata about your dataframe and columns to understand their structu
532
534
 
533
535
  **Dataframe Metadata** (`m`):
534
536
  - Press `m` to open a modal displaying:
535
- - **Rows** - Total number of rows in the dataframe
536
- - **Columns** - Total number of columns in the dataframe
537
+ - **Row** - Total number of rows in the dataframe
538
+ - **Column** - Total number of columns in the dataframe
537
539
 
538
540
  **Column Metadata** (`M`):
539
541
  - Press `M` to open a modal displaying details for all columns:
@@ -712,7 +714,6 @@ SELECT specific columns and apply WHERE conditions without writing full SQL:
712
714
  #### Advanced SQL Interface (`L`)
713
715
  Execute complete SQL queries for advanced data manipulation:
714
716
  - Write full SQL queries with standard [SQL syntax](https://docs.pola.rs/api/python/stable/reference/sql/index.html)
715
- - Support for JOINs, GROUP BY, aggregations, and more
716
717
  - Access to all SQL capabilities for complex transformations
717
718
  - Always use `self` as the table name
718
719
  - Syntax highlighted
@@ -734,7 +735,7 @@ WHERE `product id` = 7
734
735
 
735
736
  Copies value to system clipboard with `pbcopy` on macOS and `xclip` on Linux.
736
737
 
737
- **Note** May require a X server to work.
738
+ **Note**: may require a X server to work.
738
739
 
739
740
  - Press `c` to copy cursor value
740
741
  - Press `Ctrl+C` to copy column values
@@ -776,8 +777,8 @@ Manage multiple files and dataframes simultaneously with tabs.
776
777
  - **`Double-click`** - Rename the tab
777
778
  - **`Ctrl+D`** - Duplicate current tab (creates a copy with same data and state)
778
779
  - **`Ctrl+T`** - Save current tab to file
779
- - **`w`** - Save current tab to file (overwrite without prompt)
780
780
  - **`Ctrl+A`** - Save all tabs in a single Excel file
781
+ - **`w`** - Save current tab to file (overwrite without prompt)
781
782
  - **`W`** - Save all tabs to file (overwrite without prompt)
782
783
  - **`q`** - Close current tab (closes tab, prompts to save if unsaved changes)
783
784
  - **`Q`** - Close all tabs and exit app (prompts to save tabs with unsaved changes)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dataframe-textual"
7
- version = "2.0.1"
7
+ version = "2.1.0"
8
8
  description = "Interactive terminal viewer/editor for tabular data"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -20,6 +20,7 @@ from textual.widgets._data_table import (
20
20
  CellDoesNotExist,
21
21
  CellKey,
22
22
  CellType,
23
+ Column,
23
24
  ColumnKey,
24
25
  CursorType,
25
26
  DuplicateKey,
@@ -79,14 +80,13 @@ class History:
79
80
  df: pl.DataFrame
80
81
  df_view: pl.DataFrame | None
81
82
  filename: str
82
- loaded_rows: int
83
83
  hidden_columns: set[str]
84
84
  selected_rows: set[int]
85
85
  sorted_columns: dict[str, bool] # col_name -> descending
86
+ matches: dict[int, set[str]] # RID -> set of col names
86
87
  fixed_rows: int
87
88
  fixed_columns: int
88
89
  cursor_coordinate: Coordinate
89
- matches: dict[int, set[str]] # RID -> set of col names
90
90
  dirty: bool = False # Whether this history state has unsaved changes
91
91
 
92
92
 
@@ -141,7 +141,7 @@ class DataFrameTable(DataTable):
141
141
  - **M** - 📋 Show column metadata (ID, name, type)
142
142
  - **h** - 👁️ Hide current column
143
143
  - **H** - 👀 Show all hidden rows/columns
144
- - **_** - 📏 Expand column to full width
144
+ - **_** - 📏 Toggle column full width
145
145
  - **z** - 📌 Freeze rows and columns
146
146
  - **~** - 🏷️ Toggle row labels
147
147
  - **,** - 🔢 Toggle thousand separator for numeric display
@@ -352,6 +352,9 @@ class DataFrameTable(DataTable):
352
352
  # Whether to use thousand separator for numeric display
353
353
  self.thousand_separator = False
354
354
 
355
+ # Set of columns expanded to full width
356
+ self.expanded_columns: set[str] = set()
357
+
355
358
  # Whether to show internal row index column
356
359
  self.show_rid = False
357
360
 
@@ -1036,7 +1039,7 @@ class DataFrameTable(DataTable):
1036
1039
  Returns:
1037
1040
  dict[str, int]: Mapping of column name to width (None for auto-sizing columns).
1038
1041
  """
1039
- column_widths = {}
1042
+ col_widths, col_label_widths = {}, {}
1040
1043
 
1041
1044
  # Get available width for the table (with some padding for borders/scrollbar)
1042
1045
  available_width = self.scrollable_content_region.width
@@ -1046,7 +1049,7 @@ class DataFrameTable(DataTable):
1046
1049
 
1047
1050
  # No string columns, let TextualDataTable auto-size all columns
1048
1051
  if not string_cols:
1049
- return column_widths
1052
+ return col_widths
1050
1053
 
1051
1054
  # Sample a reasonable number of rows to calculate widths (don't scan entire dataframe)
1052
1055
  sample_size = min(self.BATCH_SIZE, len(self.df))
@@ -1060,7 +1063,10 @@ class DataFrameTable(DataTable):
1060
1063
  # Get column label width
1061
1064
  # Add padding for sort indicators if any
1062
1065
  label_width = measure(self.app.console, col, 1) + 2
1063
- if dtype != pl.String:
1066
+ col_label_widths[col] = label_width
1067
+
1068
+ # Let Textual auto-size for non-string columns and already expanded columns
1069
+ if dtype != pl.String or col in self.expanded_columns:
1064
1070
  available_width -= label_width
1065
1071
  continue
1066
1072
 
@@ -1083,16 +1089,16 @@ class DataFrameTable(DataTable):
1083
1089
  max_width = label_width
1084
1090
  self.log(f"Error determining width for column '{col}': {e}")
1085
1091
 
1086
- column_widths[col] = max_width
1092
+ col_widths[col] = max_width
1087
1093
  available_width -= max_width
1088
1094
 
1089
1095
  # If there's no more available width, auto-size remaining columns
1090
1096
  if available_width < 0:
1091
- for col in column_widths:
1092
- if column_widths[col] > STRING_WIDTH_CAP:
1093
- column_widths[col] = STRING_WIDTH_CAP # Cap string columns
1097
+ for col in col_widths:
1098
+ if col_widths[col] > STRING_WIDTH_CAP and col_label_widths[col] < STRING_WIDTH_CAP:
1099
+ col_widths[col] = STRING_WIDTH_CAP # Cap string columns
1094
1100
 
1095
- return column_widths
1101
+ return col_widths
1096
1102
 
1097
1103
  def setup_columns(self) -> None:
1098
1104
  """Clear table and setup columns.
@@ -1522,14 +1528,13 @@ class DataFrameTable(DataTable):
1522
1528
  df=self.df,
1523
1529
  df_view=self.df_view,
1524
1530
  filename=self.filename,
1525
- loaded_rows=self.loaded_rows,
1526
1531
  hidden_columns=self.hidden_columns.copy(),
1527
1532
  selected_rows=self.selected_rows.copy(),
1528
1533
  sorted_columns=self.sorted_columns.copy(),
1534
+ matches={k: v.copy() for k, v in self.matches.items()},
1529
1535
  fixed_rows=self.fixed_rows,
1530
1536
  fixed_columns=self.fixed_columns,
1531
1537
  cursor_coordinate=self.cursor_coordinate,
1532
- matches={k: v.copy() for k, v in self.matches.items()},
1533
1538
  dirty=self.dirty,
1534
1539
  )
1535
1540
 
@@ -1542,14 +1547,13 @@ class DataFrameTable(DataTable):
1542
1547
  self.df = history.df
1543
1548
  self.df_view = history.df_view
1544
1549
  self.filename = history.filename
1545
- self.loaded_rows = history.loaded_rows
1546
1550
  self.hidden_columns = history.hidden_columns.copy()
1547
1551
  self.selected_rows = history.selected_rows.copy()
1548
1552
  self.sorted_columns = history.sorted_columns.copy()
1553
+ self.matches = {k: v.copy() for k, v in history.matches.items()} if history.matches else defaultdict(set)
1549
1554
  self.fixed_rows = history.fixed_rows
1550
1555
  self.fixed_columns = history.fixed_columns
1551
1556
  self.cursor_coordinate = history.cursor_coordinate
1552
- self.matches = {k: v.copy() for k, v in history.matches.items()} if history.matches else defaultdict(set)
1553
1557
  self.dirty = history.dirty
1554
1558
 
1555
1559
  # Recreate table for display
@@ -1713,11 +1717,15 @@ class DataFrameTable(DataTable):
1713
1717
  if dtype != pl.String:
1714
1718
  return
1715
1719
 
1720
+ # The column to expand/shrink
1721
+ col: Column = self.columns[col_key]
1722
+
1716
1723
  # Calculate the maximum width across all loaded rows
1717
- max_width = len(col_name) + 2 # Start with column name width + padding
1724
+ label_width = len(col_name) + 2 # Start with column name width + padding
1718
1725
 
1719
1726
  try:
1720
1727
  need_expand = False
1728
+ max_width = label_width
1721
1729
 
1722
1730
  # Scan through all loaded rows that are visible to find max width
1723
1731
  for row_idx in range(self.loaded_rows):
@@ -1731,22 +1739,28 @@ class DataFrameTable(DataTable):
1731
1739
  if not need_expand:
1732
1740
  return
1733
1741
 
1734
- # Update the column width
1735
- col = self.columns[col_key]
1736
- col.width = max_width
1742
+ if col_name in self.expanded_columns:
1743
+ col.width = max(label_width, STRING_WIDTH_CAP)
1744
+ self.expanded_columns.remove(col_name)
1745
+ else:
1746
+ self.expanded_columns.add(col_name)
1737
1747
 
1738
- # Force a refresh
1739
- self._update_count += 1
1740
- self._require_update_dimensions = True
1741
- self.refresh(layout=True)
1748
+ # Update the column width
1749
+ col.width = max_width
1742
1750
 
1743
- # self.notify(f"Expanded column [$success]{col_name}[/] to width [$accent]{max_width}[/]", title="Expand")
1744
1751
  except Exception as e:
1745
1752
  self.notify(
1746
1753
  f"Error expanding column [$error]{col_name}[/]", title="Expand Column", severity="error", timeout=10
1747
1754
  )
1748
1755
  self.log(f"Error expanding column `{col_name}`: {str(e)}")
1749
1756
 
1757
+ # Force a refresh
1758
+ self._update_count += 1
1759
+ self._require_update_dimensions = True
1760
+ self.refresh(layout=True)
1761
+
1762
+ # self.notify(f"Expanded column [$success]{col_name}[/] to width [$accent]{max_width}[/]", title="Expand")
1763
+
1750
1764
  def do_toggle_rid(self) -> None:
1751
1765
  """Toggle display of the internal RID column."""
1752
1766
  self.show_rid = not self.show_rid
@@ -1968,13 +1982,21 @@ class DataFrameTable(DataTable):
1968
1982
  if self.df_view is not None:
1969
1983
  # Get updated column from df for rows that exist in df_view
1970
1984
  col_updated = f"^_{col_name}_^"
1971
- lf_updated = self.df.lazy().select(RID, pl.col(col_name).alias(col_updated))
1972
- # Join and use coalesce to prefer updated value or keep original
1985
+ col_exists = "^_exists_^"
1986
+ lf_updated = self.df.lazy().select(
1987
+ RID, pl.col(col_name).alias(col_updated), pl.lit(True).alias(col_exists)
1988
+ )
1989
+ # Join and use when/then/otherwise to handle all updates including NULLs
1973
1990
  self.df_view = (
1974
1991
  self.df_view.lazy()
1975
1992
  .join(lf_updated, on=RID, how="left")
1976
- .with_columns(pl.coalesce(pl.col(col_updated), pl.col(col_name)).alias(col_name))
1977
- .drop(col_updated)
1993
+ .with_columns(
1994
+ pl.when(pl.col(col_exists))
1995
+ .then(pl.col(col_updated))
1996
+ .otherwise(pl.col(col_name))
1997
+ .alias(col_name)
1998
+ )
1999
+ .drop(col_updated, col_exists)
1978
2000
  .collect()
1979
2001
  )
1980
2002
  except Exception as e:
@@ -2272,7 +2294,12 @@ class DataFrameTable(DataTable):
2272
2294
  """Remove the currently selected column from the table."""
2273
2295
  # Get the column to remove
2274
2296
  col_idx = self.cursor_column
2275
- col_name = self.cursor_col_name
2297
+ try:
2298
+ col_name = self.cursor_col_name
2299
+ except CellDoesNotExist:
2300
+ self.notify("No column to delete at the current cursor position", title="Delete Column", severity="warning")
2301
+ return
2302
+
2276
2303
  col_key = self.cursor_col_key
2277
2304
 
2278
2305
  col_names_to_remove = []
@@ -2337,7 +2364,7 @@ class DataFrameTable(DataTable):
2337
2364
  if self.df_view is not None:
2338
2365
  self.df_view = self.df_view.drop(col_names_to_remove)
2339
2366
 
2340
- self.notify(message, title="Delete")
2367
+ self.notify(message, title="Delete Column")
2341
2368
 
2342
2369
  def do_duplicate_column(self) -> None:
2343
2370
  """Duplicate the currently selected column, inserting it right after the current column."""
@@ -2389,7 +2416,7 @@ class DataFrameTable(DataTable):
2389
2416
  # Delete all selected rows
2390
2417
  if selected_count := len(self.selected_rows):
2391
2418
  history_desc = f"Deleted {selected_count} selected row(s)"
2392
- rids_to_delete = self.selected_rows
2419
+ rids_to_delete.update(self.selected_rows)
2393
2420
 
2394
2421
  # Delete current row and those above
2395
2422
  elif more == "above":
@@ -549,7 +549,7 @@ class DataFrameViewer(App):
549
549
  )
550
550
  self.push_screen(
551
551
  ConfirmScreen(
552
- "Close All Tabs",
552
+ "Close All Tabs" if len(self.tabs) > 1 else "Close Tab",
553
553
  label=label,
554
554
  yes="Save",
555
555
  maybe="Discard",
@@ -163,7 +163,7 @@ class RowDetailScreen(TableScreen):
163
163
 
164
164
  # Get all columns and values from the dataframe row
165
165
  for col, val, dtype in zip(self.df.columns, self.df.row(self.ridx), self.df.dtypes):
166
- if col == RID:
166
+ if col in self.dftable.hidden_columns or col == RID:
167
167
  continue # Skip RID column
168
168
  formatted_row = []
169
169
  formatted_row.append(col)
@@ -171,7 +171,7 @@ wheels = [
171
171
 
172
172
  [[package]]
173
173
  name = "dataframe-textual"
174
- version = "2.0.1"
174
+ version = "2.1.0"
175
175
  source = { editable = "." }
176
176
  dependencies = [
177
177
  { name = "polars" },