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.
- 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 +1336 -764
- dataframe_textual/data_frame_viewer.py +48 -10
- 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.2.dist-info}/METADATA +213 -215
- dataframe_textual-2.2.2.dist-info/RECORD +14 -0
- {dataframe_textual-1.9.0.dist-info → dataframe_textual-2.2.2.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.2.dist-info}/entry_points.txt +0 -0
- {dataframe_textual-1.9.0.dist-info → dataframe_textual-2.2.2.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:
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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",
|
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
|