dataframe-textual 1.9.0__py3-none-any.whl → 2.2.3__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:
@@ -136,7 +137,9 @@ class DataFrameViewer(App):
136
137
  except Exception as e:
137
138
  self.notify(
138
139
  f"Error loading [$error]{filename}[/]: Try [$accent]-I[/] to disable schema inference",
140
+ title="Load File",
139
141
  severity="error",
142
+ timeout=10,
140
143
  )
141
144
  self.log(f"Error loading `{filename}`: {str(e)}")
142
145
 
@@ -165,7 +168,7 @@ class DataFrameViewer(App):
165
168
  """
166
169
  if event.key == "k":
167
170
  self.theme = get_next_item(list(BUILTIN_THEMES.keys()), self.theme)
168
- self.notify(f"Switched to theme: [$success]{self.theme}[/]", title="Theme")
171
+ self.notify(f"Switched to theme: [$success]{self.theme}[/]", title="SwitchTheme")
169
172
 
170
173
  def on_click(self, event: Click) -> None:
171
174
  """Handle mouse click events on tabs.
@@ -247,7 +250,7 @@ class DataFrameViewer(App):
247
250
  Opens the save dialog for the active tab's DataFrameTable to save its data.
248
251
  """
249
252
  if table := self.get_active_table():
250
- table.do_save_to_file(title="Save Current Tab", all_tabs=False)
253
+ table.do_save_to_file(all_tabs=False)
251
254
 
252
255
  def action_save_all_tabs(self) -> None:
253
256
  """Save all open tabs to their respective files.
@@ -255,11 +258,37 @@ class DataFrameViewer(App):
255
258
  Iterates through all DataFrameTable widgets and opens the save dialog for each.
256
259
  """
257
260
  if table := self.get_active_table():
258
- table.do_save_to_file(title="Save All Tabs", all_tabs=True)
261
+ table.do_save_to_file(all_tabs=True)
262
+
263
+ def action_save_current_tab_overwrite(self) -> None:
264
+ """Save the currently active tab to file, overwriting if it exists."""
265
+ if table := self.get_active_table():
266
+ filepath = Path(table.filename)
267
+ filename = filepath.with_stem(table.tabname)
268
+ table.save_to_file((filename, False, False))
269
+
270
+ def action_save_all_tabs_overwrite(self) -> None:
271
+ """Save all open tabs to their respective files, overwriting if they exist."""
272
+ if table := self.get_active_table():
273
+ filepath = Path(table.filename)
274
+ if filepath.suffix.lower() in [".xlsx", ".xls"]:
275
+ filename = table.filename
276
+ else:
277
+ filename = "all-tabs.xlsx"
278
+
279
+ table.save_to_file((filename, True, False))
259
280
 
260
281
  def action_duplicate_tab(self) -> None:
261
282
  """Duplicate the currently active tab.
262
283
 
284
+ Creates a copy of the current tab with the same data and filename.
285
+ The new tab is named with '_copy' suffix and inserted after the current tab.
286
+ """
287
+ self.do_duplicate_tab()
288
+
289
+ def do_duplicate_tab(self) -> None:
290
+ """Duplicate the currently active tab.
291
+
263
292
  Creates a copy of the current tab with the same data and filename.
264
293
  The new tab is named with '_copy' suffix and inserted after the current tab.
265
294
  """
@@ -273,7 +302,7 @@ class DataFrameViewer(App):
273
302
 
274
303
  # Create new table with the same dataframe and filename
275
304
  new_table = DataFrameTable(
276
- table.df,
305
+ table.df.clone(),
277
306
  table.filename,
278
307
  tabname=new_tabname,
279
308
  zebra_stripes=True,
@@ -301,6 +330,17 @@ class DataFrameViewer(App):
301
330
  Cycles through tabs by the specified offset. With offset=1, moves to next tab.
302
331
  With offset=-1, moves to previous tab. Wraps around when reaching edges.
303
332
 
333
+ Args:
334
+ offset: Number of tabs to advance (+1 for next, -1 for previous). Defaults to 1.
335
+ """
336
+ self.do_next_tab(offset)
337
+
338
+ def do_next_tab(self, offset: int = 1) -> None:
339
+ """Switch to the next tab or previous tab.
340
+
341
+ Cycles through tabs by the specified offset. With offset=1, moves to next tab.
342
+ With offset=-1, moves to previous tab. Wraps around when reaching edges.
343
+
304
344
  Args:
305
345
  offset: Number of tabs to advance (+1 for next, -1 for previous). Defaults to 1.
306
346
  """
@@ -322,7 +362,7 @@ class DataFrameViewer(App):
322
362
  tabs = self.query_one(ContentTabs)
323
363
  tabs.display = not tabs.display
324
364
  # status = "shown" if tabs.display else "hidden"
325
- # self.notify(f"Tab bar [$success]{status}[/]", title="Toggle")
365
+ # self.notify(f"Tab bar [$success]{status}[/]", title="Toggle Tab Bar")
326
366
 
327
367
  def get_active_table(self) -> DataFrameTable | None:
328
368
  """Get the currently active DataFrameTable widget.
@@ -338,7 +378,8 @@ class DataFrameViewer(App):
338
378
  if active_pane := tabbed.active_pane:
339
379
  return active_pane.query_one(DataFrameTable)
340
380
  except (NoMatches, AttributeError):
341
- self.notify("No active table found", title="Locate", severity="error")
381
+ self.notify("No active table found", title="Locate Table", severity="error", timeout=10)
382
+
342
383
  return None
343
384
 
344
385
  def get_unique_tabname(self, tab_name: str) -> str:
@@ -376,11 +417,13 @@ class DataFrameViewer(App):
376
417
  for source in load_file(filename, prefix_sheet=True):
377
418
  self.add_tab(source.frame, filename, source.tabname, after=self.tabbed.active_pane)
378
419
  n_tab += 1
379
- # self.notify(f"Added [$accent]{n_tab}[/] tab(s) for [$success]{filename}[/]", title="Open")
420
+ # self.notify(f"Added [$accent]{n_tab}[/] tab(s) for [$success]{filename}[/]", title="Open File")
380
421
  except Exception as e:
381
- self.notify(f"Error loading [$error]{filename}[/]: {str(e)}", title="Open", severity="error")
422
+ self.notify(
423
+ f"Error loading [$error]{filename}[/]: {str(e)}", title="Open File", severity="error", timeout=10
424
+ )
382
425
  else:
383
- self.notify(f"File does not exist: [$warning]{filename}[/]", title="Open", severity="warning")
426
+ self.notify(f"File does not exist: [$warning]{filename}[/]", title="Open File", severity="warning")
384
427
 
385
428
  def add_tab(
386
429
  self,
@@ -459,7 +502,7 @@ class DataFrameViewer(App):
459
502
  """Handle the "save before closing?" confirmation."""
460
503
  if result:
461
504
  # 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")
505
+ active_table.do_save_to_file(task_after_save="close_tab")
463
506
  elif result is None:
464
507
  # User cancelled - do nothing
465
508
  return
@@ -530,7 +573,7 @@ class DataFrameViewer(App):
530
573
  )
531
574
  self.push_screen(
532
575
  ConfirmScreen(
533
- "Close All Tabs",
576
+ "Close All Tabs" if len(self.tabs) > 1 else "Close Tab",
534
577
  label=label,
535
578
  yes="Save",
536
579
  maybe="Discard",
@@ -572,7 +615,7 @@ class DataFrameViewer(App):
572
615
  content_tab, new_name = result
573
616
 
574
617
  # Update the tab name
575
- old_name = content_tab.label_text
618
+ # old_name = content_tab.label_text
576
619
  content_tab.label = new_name
577
620
 
578
621
  # Mark tab as dirty to indicate name change
@@ -584,4 +627,4 @@ class DataFrameViewer(App):
584
627
  table.focus()
585
628
  break
586
629
 
587
- self.notify(f"Renamed tab [$accent]{old_name}[/] to [$success]{new_name}[/]", title="Rename")
630
+ # self.notify(f"Renamed tab [$accent]{old_name}[/] to [$success]{new_name}[/]", title="Rename Tab")
@@ -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