dataframe-textual 1.9.0__py3-none-any.whl → 2.2.2__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.
@@ -33,14 +33,17 @@ class DataFrameViewer(App):
33
33
  - **Q** - ❌ Close all tabs (prompts to save unsaved changes)
34
34
  - **Ctrl+Q** - 🚪 Force to quit app (discards unsaved changes)
35
35
  - **Ctrl+T** - 💾 Save current tab to file
36
+ - **w** - 💾 Save current tab to file (overwrite without prompt)
36
37
  - **Ctrl+A** - 💾 Save all tabs to file
38
+ - **W** - 💾 Save all tabs to file (overwrite without prompt)
37
39
  - **Ctrl+D** - 📋 Duplicate current tab
38
40
  - **Ctrl+O** - 📁 Open a file
39
- - **Double-click tab** - ✏️ Rename current tab
41
+ - **Double-click** - ✏️ Rename tab
40
42
 
41
43
  ## 🎨 View & Settings
42
44
  - **F1** - ❓ Toggle this help panel
43
45
  - **k** - 🌙 Cycle through themes
46
+ - **Ctrl+P -> Screenshot** - 📸 Capture terminal view as a SVG image
44
47
 
45
48
  ## ⭐ Features
46
49
  - **Multi-file support** - 📂 Open multiple CSV/Excel files as tabs
@@ -62,6 +65,8 @@ class DataFrameViewer(App):
62
65
  ("ctrl+o", "open_file", "Open File"),
63
66
  ("ctrl+t", "save_current_tab", "Save Current Tab"),
64
67
  ("ctrl+a", "save_all_tabs", "Save All Tabs"),
68
+ ("w", "save_current_tab_overwrite", "Save Current Tab (overwrite)"),
69
+ ("W", "save_all_tabs_overwrite", "Save All Tabs (overwrite)"),
65
70
  ("ctrl+d", "duplicate_tab", "Duplicate Tab"),
66
71
  ("greater_than_sign,b", "next_tab(1)", "Next Tab"), # '>' and 'b'
67
72
  ("less_than_sign", "next_tab(-1)", "Prev Tab"), # '<'
@@ -81,10 +86,6 @@ class DataFrameViewer(App):
81
86
  ContentTab.dirty {
82
87
  background: $warning-darken-3;
83
88
  }
84
-
85
- .underline--bar {
86
- color: red;
87
- }
88
89
  """
89
90
 
90
91
  def __init__(self, *sources: Source) -> None:
@@ -247,7 +248,7 @@ class DataFrameViewer(App):
247
248
  Opens the save dialog for the active tab's DataFrameTable to save its data.
248
249
  """
249
250
  if table := self.get_active_table():
250
- table.do_save_to_file(title="Save Current Tab", all_tabs=False)
251
+ table.do_save_to_file(all_tabs=False)
251
252
 
252
253
  def action_save_all_tabs(self) -> None:
253
254
  """Save all open tabs to their respective files.
@@ -255,11 +256,37 @@ class DataFrameViewer(App):
255
256
  Iterates through all DataFrameTable widgets and opens the save dialog for each.
256
257
  """
257
258
  if table := self.get_active_table():
258
- table.do_save_to_file(title="Save All Tabs", all_tabs=True)
259
+ table.do_save_to_file(all_tabs=True)
260
+
261
+ def action_save_current_tab_overwrite(self) -> None:
262
+ """Save the currently active tab to file, overwriting if it exists."""
263
+ if table := self.get_active_table():
264
+ filepath = Path(table.filename)
265
+ filename = filepath.with_stem(table.tabname)
266
+ table.save_to_file((filename, False, False))
267
+
268
+ def action_save_all_tabs_overwrite(self) -> None:
269
+ """Save all open tabs to their respective files, overwriting if they exist."""
270
+ if table := self.get_active_table():
271
+ filepath = Path(table.filename)
272
+ if filepath.suffix.lower() in [".xlsx", ".xls"]:
273
+ filename = table.filename
274
+ else:
275
+ filename = "all-tabs.xlsx"
276
+
277
+ table.save_to_file((filename, True, False))
259
278
 
260
279
  def action_duplicate_tab(self) -> None:
261
280
  """Duplicate the currently active tab.
262
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
+
263
290
  Creates a copy of the current tab with the same data and filename.
264
291
  The new tab is named with '_copy' suffix and inserted after the current tab.
265
292
  """
@@ -273,7 +300,7 @@ class DataFrameViewer(App):
273
300
 
274
301
  # Create new table with the same dataframe and filename
275
302
  new_table = DataFrameTable(
276
- table.df,
303
+ table.df.clone(),
277
304
  table.filename,
278
305
  tabname=new_tabname,
279
306
  zebra_stripes=True,
@@ -301,6 +328,17 @@ class DataFrameViewer(App):
301
328
  Cycles through tabs by the specified offset. With offset=1, moves to next tab.
302
329
  With offset=-1, moves to previous tab. Wraps around when reaching edges.
303
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
+
304
342
  Args:
305
343
  offset: Number of tabs to advance (+1 for next, -1 for previous). Defaults to 1.
306
344
  """
@@ -459,7 +497,7 @@ class DataFrameViewer(App):
459
497
  """Handle the "save before closing?" confirmation."""
460
498
  if result:
461
499
  # User wants to save - close after save dialog opens
462
- 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")
463
501
  elif result is None:
464
502
  # User cancelled - do nothing
465
503
  return
@@ -530,7 +568,7 @@ class DataFrameViewer(App):
530
568
  )
531
569
  self.push_screen(
532
570
  ConfirmScreen(
533
- "Close All Tabs",
571
+ "Close All Tabs" if len(self.tabs) > 1 else "Close Tab",
534
572
  label=label,
535
573
  yes="Save",
536
574
  maybe="Discard",
@@ -13,6 +13,8 @@ from textual.screen import ModalScreen
13
13
  from textual.widgets import Button, Input, Label, SelectionList, TextArea
14
14
  from textual.widgets.selection_list import Selection
15
15
 
16
+ from .common import RID
17
+
16
18
 
17
19
  class SqlScreen(ModalScreen):
18
20
  """Base class for modal screens handling SQL query."""
@@ -151,37 +153,35 @@ class SimpleSqlScreen(SqlScreen):
151
153
 
152
154
  Args:
153
155
  dftable: Reference to the parent DataFrameTable widget.
154
-
155
- Returns:
156
- None
157
156
  """
158
157
  super().__init__(
159
158
  dftable,
160
- on_yes_callback=self._handle_simple,
161
- on_maybe_callback=partial(
162
- self._handle_simple,
163
- view=False,
164
- ),
159
+ on_yes_callback=self.handle_simple,
160
+ on_maybe_callback=partial(self.handle_simple, view=False),
165
161
  )
166
162
 
167
163
  def compose(self) -> ComposeResult:
168
164
  """Compose the simple SQL screen widget structure."""
169
165
  with Container(id="sql-container") as container:
170
166
  container.border_title = "SQL Query"
171
- yield Label("Select columns (default to all):", id="select-label")
167
+ yield Label("SELECT columns (all if none selected):", id="select-label")
172
168
  yield SelectionList(
173
- *[Selection(col, col) for col in self.df.columns if col not in self.dftable.hidden_columns],
169
+ *[
170
+ Selection(col, col)
171
+ for col in self.df.columns
172
+ if col not in self.dftable.hidden_columns and col != RID
173
+ ],
174
174
  id="column-selection",
175
175
  )
176
- yield Label("Where condition (optional)", id="where-label")
176
+ yield Label("WHERE condition (optional)", id="where-label")
177
177
  yield Input(placeholder="e.g., age > 30 and height < 180", id="where-input")
178
178
  yield from super().compose()
179
179
 
180
- def _handle_simple(self, view: bool = True) -> None:
180
+ def handle_simple(self, view: bool = True) -> None:
181
181
  """Handle Yes button/Enter key press."""
182
182
  selections = self.query_one(SelectionList).selected
183
183
  if not selections:
184
- selections = [col for col in self.df.columns if col not in self.dftable.hidden_columns]
184
+ selections = [col for col in self.df.columns if col not in self.dftable.hidden_columns and col != RID]
185
185
 
186
186
  columns = ", ".join(f"`{s}`" for s in selections)
187
187
  where = self.query_one(Input).value.strip()
@@ -215,14 +215,11 @@ class AdvancedSqlScreen(SqlScreen):
215
215
 
216
216
  Args:
217
217
  dftable: Reference to the parent DataFrameTable widget.
218
-
219
- Returns:
220
- None
221
218
  """
222
219
  super().__init__(
223
220
  dftable,
224
- on_yes_callback=self._handle_advanced,
225
- on_maybe_callback=partial(self._handle_advanced, view=False),
221
+ on_yes_callback=self.handle_advanced,
222
+ on_maybe_callback=partial(self.handle_advanced, view=False),
226
223
  )
227
224
 
228
225
  def compose(self) -> ComposeResult:
@@ -230,12 +227,12 @@ class AdvancedSqlScreen(SqlScreen):
230
227
  with Container(id="sql-container") as container:
231
228
  container.border_title = "Advanced SQL Query"
232
229
  yield TextArea.code_editor(
233
- placeholder="Enter SQL query (use `self` as the table name), e.g., \n\nSELECT * \nFROM self \nWHERE age > 30",
230
+ placeholder="Enter SQL query, e.g., \n\nSELECT * \nFROM self \nWHERE age > 30\n\n- use 'self' as the table name\n- use backticks (`) for column names with spaces.",
234
231
  id="sql-textarea",
235
232
  language="sql",
236
233
  )
237
234
  yield from super().compose()
238
235
 
239
- def _handle_advanced(self, view: bool = True) -> None:
236
+ def handle_advanced(self, view: bool = True) -> None:
240
237
  """Handle Yes button/Enter key press."""
241
238
  return self.query_one(TextArea).text.strip(), view