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.
- dataframe_textual/__init__.py +27 -1
- dataframe_textual/__main__.py +13 -2
- dataframe_textual/common.py +139 -52
- dataframe_textual/data_frame_help_panel.py +0 -3
- dataframe_textual/data_frame_table.py +1377 -792
- dataframe_textual/data_frame_viewer.py +61 -18
- dataframe_textual/sql_screen.py +17 -20
- dataframe_textual/table_screen.py +164 -144
- dataframe_textual/yes_no_screen.py +34 -39
- {dataframe_textual-1.9.0.dist-info → dataframe_textual-2.2.3.dist-info}/METADATA +213 -215
- dataframe_textual-2.2.3.dist-info/RECORD +14 -0
- {dataframe_textual-1.9.0.dist-info → dataframe_textual-2.2.3.dist-info}/WHEEL +1 -1
- dataframe_textual-1.9.0.dist-info/RECORD +0 -14
- {dataframe_textual-1.9.0.dist-info → dataframe_textual-2.2.3.dist-info}/entry_points.txt +0 -0
- {dataframe_textual-1.9.0.dist-info → dataframe_textual-2.2.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
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="
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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")
|
dataframe_textual/sql_screen.py
CHANGED
|
@@ -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.
|
|
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("
|
|
167
|
+
yield Label("SELECT columns (all if none selected):", id="select-label")
|
|
172
168
|
yield SelectionList(
|
|
173
|
-
*[
|
|
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("
|
|
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
|
|
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.
|
|
225
|
-
on_maybe_callback=partial(self.
|
|
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
|
|
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
|
|
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
|