dataframe-textual 2.0.1__py3-none-any.whl → 2.2.0__py3-none-any.whl

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.
@@ -46,6 +46,9 @@ def cli() -> argparse.Namespace:
46
46
  parser.add_argument(
47
47
  "-I", "--no-inference", action="store_true", help="Do not infer data types when reading CSV/TSV"
48
48
  )
49
+ parser.add_argument(
50
+ "-t", "--truncate-ragged-lines", action="store_true", help="Truncate ragged lines when reading CSV/TSV"
51
+ )
49
52
  parser.add_argument("-E", "--ignore-errors", action="store_true", help="Ignore errors when reading CSV/TSV")
50
53
  parser.add_argument(
51
54
  "-c", "--comment-prefix", nargs="?", const="#", help="Comment lines are skipped when reading CSV/TSV"
@@ -94,6 +97,7 @@ def main() -> None:
94
97
  skip_rows_after_header=args.skip_rows_after_header,
95
98
  null_values=args.null,
96
99
  ignore_errors=args.ignore_errors,
100
+ truncate_ragged_lines=args.truncate_ragged_lines,
97
101
  )
98
102
  app = DataFrameViewer(*sources)
99
103
  app.run()
@@ -484,6 +484,7 @@ def load_dataframe(
484
484
  skip_rows_after_header: int = 0,
485
485
  null_values: list[str] | None = None,
486
486
  ignore_errors: bool = False,
487
+ truncate_ragged_lines: bool = False,
487
488
  ) -> list[Source]:
488
489
  """Load DataFrames from file specifications.
489
490
 
@@ -548,6 +549,7 @@ def load_dataframe(
548
549
  skip_rows_after_header=skip_rows_after_header,
549
550
  null_values=null_values,
550
551
  ignore_errors=ignore_errors,
552
+ truncate_ragged_lines=truncate_ragged_lines,
551
553
  )
552
554
  )
553
555
 
@@ -624,6 +626,7 @@ def load_file(
624
626
  schema_overrides: dict[str, pl.DataType] | None = None,
625
627
  null_values: list[str] | None = None,
626
628
  ignore_errors: bool = False,
629
+ truncate_ragged_lines: bool = False,
627
630
  ) -> list[Source]:
628
631
  """Load a single file.
629
632
 
@@ -678,6 +681,7 @@ def load_file(
678
681
  schema_overrides=schema_overrides,
679
682
  null_values=null_values,
680
683
  ignore_errors=ignore_errors,
684
+ truncate_ragged_lines=truncate_ragged_lines,
681
685
  )
682
686
  data.append(Source(lf, filename, filepath.stem))
683
687
  elif file_format in ("xlsx", "xls", "excel"):
@@ -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
 
@@ -110,6 +110,20 @@ class ReplaceState:
110
110
  done: bool = False # Whether the replace operation is complete
111
111
 
112
112
 
113
+ def add_rid_column(df: pl.DataFrame) -> pl.DataFrame:
114
+ """Add internal row index as last column to the dataframe if not already present.
115
+
116
+ Args:
117
+ df: The Polars DataFrame to modify.
118
+
119
+ Returns:
120
+ The modified DataFrame with the internal row index column added.
121
+ """
122
+ if RID not in df.columns:
123
+ df = df.lazy().with_row_index(RID).select(pl.exclude(RID), RID).collect()
124
+ return df
125
+
126
+
113
127
  class DataFrameTable(DataTable):
114
128
  """Custom DataTable to highlight row/column labels based on cursor position."""
115
129
 
@@ -141,7 +155,7 @@ class DataFrameTable(DataTable):
141
155
  - **M** - 📋 Show column metadata (ID, name, type)
142
156
  - **h** - 👁️ Hide current column
143
157
  - **H** - 👀 Show all hidden rows/columns
144
- - **_** - 📏 Expand column to full width
158
+ - **_** - 📏 Toggle column full width
145
159
  - **z** - 📌 Freeze rows and columns
146
160
  - **~** - 🏷️ Toggle row labels
147
161
  - **,** - 🔢 Toggle thousand separator for numeric display
@@ -321,7 +335,7 @@ class DataFrameTable(DataTable):
321
335
  super().__init__(**kwargs)
322
336
 
323
337
  # DataFrame state
324
- self.dataframe = df.lazy().with_row_index(RID).select(pl.exclude(RID), RID).collect() # Original dataframe
338
+ self.dataframe = add_rid_column(df) # Original dataframe
325
339
  self.df = self.dataframe # Internal/working dataframe
326
340
  self.filename = filename or "untitled.csv" # Current filename
327
341
  self.tabname = tabname or Path(filename).stem # Tab name
@@ -352,6 +366,9 @@ class DataFrameTable(DataTable):
352
366
  # Whether to use thousand separator for numeric display
353
367
  self.thousand_separator = False
354
368
 
369
+ # Set of columns expanded to full width
370
+ self.expanded_columns: set[str] = set()
371
+
355
372
  # Whether to show internal row index column
356
373
  self.show_rid = False
357
374
 
@@ -1036,7 +1053,7 @@ class DataFrameTable(DataTable):
1036
1053
  Returns:
1037
1054
  dict[str, int]: Mapping of column name to width (None for auto-sizing columns).
1038
1055
  """
1039
- column_widths = {}
1056
+ col_widths, col_label_widths = {}, {}
1040
1057
 
1041
1058
  # Get available width for the table (with some padding for borders/scrollbar)
1042
1059
  available_width = self.scrollable_content_region.width
@@ -1046,7 +1063,7 @@ class DataFrameTable(DataTable):
1046
1063
 
1047
1064
  # No string columns, let TextualDataTable auto-size all columns
1048
1065
  if not string_cols:
1049
- return column_widths
1066
+ return col_widths
1050
1067
 
1051
1068
  # Sample a reasonable number of rows to calculate widths (don't scan entire dataframe)
1052
1069
  sample_size = min(self.BATCH_SIZE, len(self.df))
@@ -1060,7 +1077,10 @@ class DataFrameTable(DataTable):
1060
1077
  # Get column label width
1061
1078
  # Add padding for sort indicators if any
1062
1079
  label_width = measure(self.app.console, col, 1) + 2
1063
- if dtype != pl.String:
1080
+ col_label_widths[col] = label_width
1081
+
1082
+ # Let Textual auto-size for non-string columns and already expanded columns
1083
+ if dtype != pl.String or col in self.expanded_columns:
1064
1084
  available_width -= label_width
1065
1085
  continue
1066
1086
 
@@ -1083,16 +1103,16 @@ class DataFrameTable(DataTable):
1083
1103
  max_width = label_width
1084
1104
  self.log(f"Error determining width for column '{col}': {e}")
1085
1105
 
1086
- column_widths[col] = max_width
1106
+ col_widths[col] = max_width
1087
1107
  available_width -= max_width
1088
1108
 
1089
1109
  # If there's no more available width, auto-size remaining columns
1090
1110
  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
1111
+ for col in col_widths:
1112
+ if col_widths[col] > STRING_WIDTH_CAP and col_label_widths[col] < STRING_WIDTH_CAP:
1113
+ col_widths[col] = STRING_WIDTH_CAP # Cap string columns
1094
1114
 
1095
- return column_widths
1115
+ return col_widths
1096
1116
 
1097
1117
  def setup_columns(self) -> None:
1098
1118
  """Clear table and setup columns.
@@ -1522,14 +1542,13 @@ class DataFrameTable(DataTable):
1522
1542
  df=self.df,
1523
1543
  df_view=self.df_view,
1524
1544
  filename=self.filename,
1525
- loaded_rows=self.loaded_rows,
1526
1545
  hidden_columns=self.hidden_columns.copy(),
1527
1546
  selected_rows=self.selected_rows.copy(),
1528
1547
  sorted_columns=self.sorted_columns.copy(),
1548
+ matches={k: v.copy() for k, v in self.matches.items()},
1529
1549
  fixed_rows=self.fixed_rows,
1530
1550
  fixed_columns=self.fixed_columns,
1531
1551
  cursor_coordinate=self.cursor_coordinate,
1532
- matches={k: v.copy() for k, v in self.matches.items()},
1533
1552
  dirty=self.dirty,
1534
1553
  )
1535
1554
 
@@ -1542,14 +1561,13 @@ class DataFrameTable(DataTable):
1542
1561
  self.df = history.df
1543
1562
  self.df_view = history.df_view
1544
1563
  self.filename = history.filename
1545
- self.loaded_rows = history.loaded_rows
1546
1564
  self.hidden_columns = history.hidden_columns.copy()
1547
1565
  self.selected_rows = history.selected_rows.copy()
1548
1566
  self.sorted_columns = history.sorted_columns.copy()
1567
+ self.matches = {k: v.copy() for k, v in history.matches.items()} if history.matches else defaultdict(set)
1549
1568
  self.fixed_rows = history.fixed_rows
1550
1569
  self.fixed_columns = history.fixed_columns
1551
1570
  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
1571
  self.dirty = history.dirty
1554
1572
 
1555
1573
  # Recreate table for display
@@ -1713,11 +1731,15 @@ class DataFrameTable(DataTable):
1713
1731
  if dtype != pl.String:
1714
1732
  return
1715
1733
 
1734
+ # The column to expand/shrink
1735
+ col: Column = self.columns[col_key]
1736
+
1716
1737
  # Calculate the maximum width across all loaded rows
1717
- max_width = len(col_name) + 2 # Start with column name width + padding
1738
+ label_width = len(col_name) + 2 # Start with column name width + padding
1718
1739
 
1719
1740
  try:
1720
1741
  need_expand = False
1742
+ max_width = label_width
1721
1743
 
1722
1744
  # Scan through all loaded rows that are visible to find max width
1723
1745
  for row_idx in range(self.loaded_rows):
@@ -1731,22 +1753,28 @@ class DataFrameTable(DataTable):
1731
1753
  if not need_expand:
1732
1754
  return
1733
1755
 
1734
- # Update the column width
1735
- col = self.columns[col_key]
1736
- col.width = max_width
1756
+ if col_name in self.expanded_columns:
1757
+ col.width = max(label_width, STRING_WIDTH_CAP)
1758
+ self.expanded_columns.remove(col_name)
1759
+ else:
1760
+ self.expanded_columns.add(col_name)
1737
1761
 
1738
- # Force a refresh
1739
- self._update_count += 1
1740
- self._require_update_dimensions = True
1741
- self.refresh(layout=True)
1762
+ # Update the column width
1763
+ col.width = max_width
1742
1764
 
1743
- # self.notify(f"Expanded column [$success]{col_name}[/] to width [$accent]{max_width}[/]", title="Expand")
1744
1765
  except Exception as e:
1745
1766
  self.notify(
1746
1767
  f"Error expanding column [$error]{col_name}[/]", title="Expand Column", severity="error", timeout=10
1747
1768
  )
1748
1769
  self.log(f"Error expanding column `{col_name}`: {str(e)}")
1749
1770
 
1771
+ # Force a refresh
1772
+ self._update_count += 1
1773
+ self._require_update_dimensions = True
1774
+ self.refresh(layout=True)
1775
+
1776
+ # self.notify(f"Expanded column [$success]{col_name}[/] to width [$accent]{max_width}[/]", title="Expand")
1777
+
1750
1778
  def do_toggle_rid(self) -> None:
1751
1779
  """Toggle display of the internal RID column."""
1752
1780
  self.show_rid = not self.show_rid
@@ -1968,13 +1996,21 @@ class DataFrameTable(DataTable):
1968
1996
  if self.df_view is not None:
1969
1997
  # Get updated column from df for rows that exist in df_view
1970
1998
  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
1999
+ col_exists = "^_exists_^"
2000
+ lf_updated = self.df.lazy().select(
2001
+ RID, pl.col(col_name).alias(col_updated), pl.lit(True).alias(col_exists)
2002
+ )
2003
+ # Join and use when/then/otherwise to handle all updates including NULLs
1973
2004
  self.df_view = (
1974
2005
  self.df_view.lazy()
1975
2006
  .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)
2007
+ .with_columns(
2008
+ pl.when(pl.col(col_exists))
2009
+ .then(pl.col(col_updated))
2010
+ .otherwise(pl.col(col_name))
2011
+ .alias(col_name)
2012
+ )
2013
+ .drop(col_updated, col_exists)
1978
2014
  .collect()
1979
2015
  )
1980
2016
  except Exception as e:
@@ -2272,7 +2308,12 @@ class DataFrameTable(DataTable):
2272
2308
  """Remove the currently selected column from the table."""
2273
2309
  # Get the column to remove
2274
2310
  col_idx = self.cursor_column
2275
- col_name = self.cursor_col_name
2311
+ try:
2312
+ col_name = self.cursor_col_name
2313
+ except CellDoesNotExist:
2314
+ self.notify("No column to delete at the current cursor position", title="Delete Column", severity="warning")
2315
+ return
2316
+
2276
2317
  col_key = self.cursor_col_key
2277
2318
 
2278
2319
  col_names_to_remove = []
@@ -2337,7 +2378,7 @@ class DataFrameTable(DataTable):
2337
2378
  if self.df_view is not None:
2338
2379
  self.df_view = self.df_view.drop(col_names_to_remove)
2339
2380
 
2340
- self.notify(message, title="Delete")
2381
+ self.notify(message, title="Delete Column")
2341
2382
 
2342
2383
  def do_duplicate_column(self) -> None:
2343
2384
  """Duplicate the currently selected column, inserting it right after the current column."""
@@ -2389,7 +2430,7 @@ class DataFrameTable(DataTable):
2389
2430
  # Delete all selected rows
2390
2431
  if selected_count := len(self.selected_rows):
2391
2432
  history_desc = f"Deleted {selected_count} selected row(s)"
2392
- rids_to_delete = self.selected_rows
2433
+ rids_to_delete.update(self.selected_rows)
2393
2434
 
2394
2435
  # Delete current row and those above
2395
2436
  elif more == "above":
@@ -279,6 +279,14 @@ class DataFrameViewer(App):
279
279
  def action_duplicate_tab(self) -> None:
280
280
  """Duplicate the currently active tab.
281
281
 
282
+ Creates a copy of the current tab with the same data and filename.
283
+ The new tab is named with '_copy' suffix and inserted after the current tab.
284
+ """
285
+ self.do_duplicate_tab()
286
+
287
+ def do_duplicate_tab(self) -> None:
288
+ """Duplicate the currently active tab.
289
+
282
290
  Creates a copy of the current tab with the same data and filename.
283
291
  The new tab is named with '_copy' suffix and inserted after the current tab.
284
292
  """
@@ -320,6 +328,17 @@ class DataFrameViewer(App):
320
328
  Cycles through tabs by the specified offset. With offset=1, moves to next tab.
321
329
  With offset=-1, moves to previous tab. Wraps around when reaching edges.
322
330
 
331
+ Args:
332
+ offset: Number of tabs to advance (+1 for next, -1 for previous). Defaults to 1.
333
+ """
334
+ self.do_next_tab(offset)
335
+
336
+ def do_next_tab(self, offset: int = 1) -> None:
337
+ """Switch to the next tab or previous tab.
338
+
339
+ Cycles through tabs by the specified offset. With offset=1, moves to next tab.
340
+ With offset=-1, moves to previous tab. Wraps around when reaching edges.
341
+
323
342
  Args:
324
343
  offset: Number of tabs to advance (+1 for next, -1 for previous). Defaults to 1.
325
344
  """
@@ -478,7 +497,7 @@ class DataFrameViewer(App):
478
497
  """Handle the "save before closing?" confirmation."""
479
498
  if result:
480
499
  # User wants to save - close after save dialog opens
481
- active_table.do_save_to_file(title="Save Current Tab", task_after_save="close_tab")
500
+ active_table.do_save_to_file(task_after_save="close_tab")
482
501
  elif result is None:
483
502
  # User cancelled - do nothing
484
503
  return
@@ -549,7 +568,7 @@ class DataFrameViewer(App):
549
568
  )
550
569
  self.push_screen(
551
570
  ConfirmScreen(
552
- "Close All Tabs",
571
+ "Close All Tabs" if len(self.tabs) > 1 else "Close Tab",
553
572
  label=label,
554
573
  yes="Save",
555
574
  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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataframe-textual
3
- Version: 2.0.1
3
+ Version: 2.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
@@ -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)
@@ -0,0 +1,14 @@
1
+ dataframe_textual/__init__.py,sha256=E53fW1spQRA4jW9grxSqPEmoe9zofzr6twdveMbt_W8,1310
2
+ dataframe_textual/__main__.py,sha256=xXeUA2EqVhufPkTbvv6MOCt3_ESHBH3PsCE--07a0ww,3613
3
+ dataframe_textual/common.py,sha256=ZDhAT13UCgwtQborPbcHboX956A6N-m1-YvCpZHFtwY,28108
4
+ dataframe_textual/data_frame_help_panel.py,sha256=UEtj64XsVRdtLzuwOaITfoEQUkAfwFuvpr5Npip5WHs,3381
5
+ dataframe_textual/data_frame_table.py,sha256=sxs2AZ6UUmiPc6yf503Vvj3FoASgcQuDSec17pN1I80,149069
6
+ dataframe_textual/data_frame_viewer.py,sha256=fyrMSH2dFxc3h3HkJSaT1jBCQyoUUstGVXXPSG6wyjA,23099
7
+ dataframe_textual/sql_screen.py,sha256=P3j1Fv45NIKEYo9adb7NPod54FaU-djFIvCUMMHbvjY,7534
8
+ dataframe_textual/table_screen.py,sha256=XPzJI6FXjwnxtQSMTmluygwkYM-0-Lx3v9o-MuL6bMg,19071
9
+ dataframe_textual/yes_no_screen.py,sha256=NI7Zt3rETDWYiT5CH_FDy7sIWkZ7d7LquaZZbX79b2g,26400
10
+ dataframe_textual-2.2.0.dist-info/METADATA,sha256=yf7TJe5fHtrSlTl6nI3PkpzE80aTW-zuJKsBbZ0q0pQ,29160
11
+ dataframe_textual-2.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ dataframe_textual-2.2.0.dist-info/entry_points.txt,sha256=R_GoooOxcq6ab4RaHiVoZ4zrZJ-phMcGmlL2rwqncW8,107
13
+ dataframe_textual-2.2.0.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
14
+ dataframe_textual-2.2.0.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- dataframe_textual/__init__.py,sha256=E53fW1spQRA4jW9grxSqPEmoe9zofzr6twdveMbt_W8,1310
2
- dataframe_textual/__main__.py,sha256=vgHjpSsHBF34UN46iMgu_EiebZUzxWwJZ_ngOU3nQvI,3412
3
- dataframe_textual/common.py,sha256=gpNNY5ZePJkJPf-YoLSCHKaBpdXQ1yW2iSKYV6zZYUo,27908
4
- dataframe_textual/data_frame_help_panel.py,sha256=UEtj64XsVRdtLzuwOaITfoEQUkAfwFuvpr5Npip5WHs,3381
5
- dataframe_textual/data_frame_table.py,sha256=p7PXV-39IRimrzdbuDfiacK9yNQ7XXkWBFyZ4finEk8,147693
6
- dataframe_textual/data_frame_viewer.py,sha256=fkiQ0OGi2rrE06VAVJuAM_9wwmqLY1AZouwEMNoDmy8,22367
7
- dataframe_textual/sql_screen.py,sha256=P3j1Fv45NIKEYo9adb7NPod54FaU-djFIvCUMMHbvjY,7534
8
- dataframe_textual/table_screen.py,sha256=XlVxU_haCxPoA41ZIDcwixOg341Wf35JrFwPoCTnMzE,19033
9
- dataframe_textual/yes_no_screen.py,sha256=NI7Zt3rETDWYiT5CH_FDy7sIWkZ7d7LquaZZbX79b2g,26400
10
- dataframe_textual-2.0.1.dist-info/METADATA,sha256=XUm9YBbqWaSjAGv8uOrAk8ss1blAjG30BLZwJn2IjwM,29306
11
- dataframe_textual-2.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- dataframe_textual-2.0.1.dist-info/entry_points.txt,sha256=R_GoooOxcq6ab4RaHiVoZ4zrZJ-phMcGmlL2rwqncW8,107
13
- dataframe_textual-2.0.1.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
14
- dataframe_textual-2.0.1.dist-info/RECORD,,