dataframe-textual 1.10.1__py3-none-any.whl → 1.16.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/__main__.py +2 -2
- dataframe_textual/common.py +104 -42
- dataframe_textual/data_frame_table.py +836 -372
- dataframe_textual/data_frame_viewer.py +17 -2
- dataframe_textual/sql_screen.py +3 -9
- dataframe_textual/table_screen.py +102 -54
- dataframe_textual/yes_no_screen.py +26 -22
- {dataframe_textual-1.10.1.dist-info → dataframe_textual-1.16.2.dist-info}/METADATA +202 -205
- dataframe_textual-1.16.2.dist-info/RECORD +14 -0
- {dataframe_textual-1.10.1.dist-info → dataframe_textual-1.16.2.dist-info}/WHEEL +1 -1
- dataframe_textual-1.10.1.dist-info/RECORD +0 -14
- {dataframe_textual-1.10.1.dist-info → dataframe_textual-1.16.2.dist-info}/entry_points.txt +0 -0
- {dataframe_textual-1.10.1.dist-info → dataframe_textual-1.16.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"), # '<'
|
|
@@ -245,6 +250,11 @@ class DataFrameViewer(App):
|
|
|
245
250
|
if table := self.get_active_table():
|
|
246
251
|
table.do_save_to_file(title="Save Current Tab", all_tabs=False)
|
|
247
252
|
|
|
253
|
+
def action_save_current_tab_overwrite(self) -> None:
|
|
254
|
+
"""Save the currently active tab to file, overwriting if it exists."""
|
|
255
|
+
if table := self.get_active_table():
|
|
256
|
+
table.save_to_file((table.filename, False, False))
|
|
257
|
+
|
|
248
258
|
def action_save_all_tabs(self) -> None:
|
|
249
259
|
"""Save all open tabs to their respective files.
|
|
250
260
|
|
|
@@ -253,6 +263,11 @@ class DataFrameViewer(App):
|
|
|
253
263
|
if table := self.get_active_table():
|
|
254
264
|
table.do_save_to_file(title="Save All Tabs", all_tabs=True)
|
|
255
265
|
|
|
266
|
+
def action_save_all_tabs_overwrite(self) -> None:
|
|
267
|
+
"""Save all open tabs to their respective files, overwriting if they exist."""
|
|
268
|
+
if table := self.get_active_table():
|
|
269
|
+
table.save_to_file((table.filename, True, False))
|
|
270
|
+
|
|
256
271
|
def action_duplicate_tab(self) -> None:
|
|
257
272
|
"""Duplicate the currently active tab.
|
|
258
273
|
|
|
@@ -269,7 +284,7 @@ class DataFrameViewer(App):
|
|
|
269
284
|
|
|
270
285
|
# Create new table with the same dataframe and filename
|
|
271
286
|
new_table = DataFrameTable(
|
|
272
|
-
table.df,
|
|
287
|
+
table.df.clone(),
|
|
273
288
|
table.filename,
|
|
274
289
|
tabname=new_tabname,
|
|
275
290
|
zebra_stripes=True,
|
dataframe_textual/sql_screen.py
CHANGED
|
@@ -151,9 +151,6 @@ class SimpleSqlScreen(SqlScreen):
|
|
|
151
151
|
|
|
152
152
|
Args:
|
|
153
153
|
dftable: Reference to the parent DataFrameTable widget.
|
|
154
|
-
|
|
155
|
-
Returns:
|
|
156
|
-
None
|
|
157
154
|
"""
|
|
158
155
|
super().__init__(
|
|
159
156
|
dftable,
|
|
@@ -165,12 +162,12 @@ class SimpleSqlScreen(SqlScreen):
|
|
|
165
162
|
"""Compose the simple SQL screen widget structure."""
|
|
166
163
|
with Container(id="sql-container") as container:
|
|
167
164
|
container.border_title = "SQL Query"
|
|
168
|
-
yield Label("
|
|
165
|
+
yield Label("SELECT columns (all if none selected):", id="select-label")
|
|
169
166
|
yield SelectionList(
|
|
170
167
|
*[Selection(col, col) for col in self.df.columns if col not in self.dftable.hidden_columns],
|
|
171
168
|
id="column-selection",
|
|
172
169
|
)
|
|
173
|
-
yield Label("
|
|
170
|
+
yield Label("WHERE condition (optional)", id="where-label")
|
|
174
171
|
yield Input(placeholder="e.g., age > 30 and height < 180", id="where-input")
|
|
175
172
|
yield from super().compose()
|
|
176
173
|
|
|
@@ -212,9 +209,6 @@ class AdvancedSqlScreen(SqlScreen):
|
|
|
212
209
|
|
|
213
210
|
Args:
|
|
214
211
|
dftable: Reference to the parent DataFrameTable widget.
|
|
215
|
-
|
|
216
|
-
Returns:
|
|
217
|
-
None
|
|
218
212
|
"""
|
|
219
213
|
super().__init__(
|
|
220
214
|
dftable,
|
|
@@ -227,7 +221,7 @@ class AdvancedSqlScreen(SqlScreen):
|
|
|
227
221
|
with Container(id="sql-container") as container:
|
|
228
222
|
container.border_title = "Advanced SQL Query"
|
|
229
223
|
yield TextArea.code_editor(
|
|
230
|
-
placeholder="Enter SQL query
|
|
224
|
+
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.",
|
|
231
225
|
id="sql-textarea",
|
|
232
226
|
language="sql",
|
|
233
227
|
)
|
|
@@ -13,7 +13,7 @@ from textual.renderables.bar import Bar
|
|
|
13
13
|
from textual.screen import ModalScreen
|
|
14
14
|
from textual.widgets import DataTable
|
|
15
15
|
|
|
16
|
-
from .common import NULL, NULL_DISPLAY, RIDX, DtypeConfig, format_float
|
|
16
|
+
from .common import NULL, NULL_DISPLAY, RIDX, DtypeConfig, format_float
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class TableScreen(ModalScreen):
|
|
@@ -181,14 +181,12 @@ class RowDetailScreen(TableScreen):
|
|
|
181
181
|
|
|
182
182
|
# Get all columns and values from the dataframe row
|
|
183
183
|
for col, val, dtype in zip(self.df.columns, self.df.row(self.ridx), self.df.dtypes):
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
)
|
|
191
|
-
)
|
|
184
|
+
formatted_row = []
|
|
185
|
+
formatted_row.append(col)
|
|
186
|
+
|
|
187
|
+
dc = DtypeConfig(dtype)
|
|
188
|
+
formatted_row.append(dc.format(val, justify="", thousand_separator=self.thousand_separator))
|
|
189
|
+
self.table.add_row(*formatted_row)
|
|
192
190
|
|
|
193
191
|
self.table.cursor_type = "row"
|
|
194
192
|
|
|
@@ -209,7 +207,25 @@ class RowDetailScreen(TableScreen):
|
|
|
209
207
|
# Filter the main table by the selected value
|
|
210
208
|
self.filter_or_view_selected_value(self.get_cidx_name_value(), action="filter")
|
|
211
209
|
event.stop()
|
|
212
|
-
elif event.key == "
|
|
210
|
+
elif event.key == "right_curly_bracket": # '}'
|
|
211
|
+
# Move to the next visible row
|
|
212
|
+
ridx = self.ridx + 1
|
|
213
|
+
while ridx < len(self.df) and not self.dftable.visible_rows[ridx]:
|
|
214
|
+
ridx += 1
|
|
215
|
+
if ridx < len(self.df):
|
|
216
|
+
self.ridx = ridx
|
|
217
|
+
self.dftable.move_cursor_to(self.ridx)
|
|
218
|
+
self.build_table()
|
|
219
|
+
event.stop()
|
|
220
|
+
elif event.key == "left_curly_bracket": # '{'
|
|
221
|
+
# Move to the previous visible row
|
|
222
|
+
ridx = self.ridx - 1
|
|
223
|
+
while ridx >= 0 and not self.dftable.visible_rows[ridx]:
|
|
224
|
+
ridx -= 1
|
|
225
|
+
if ridx >= 0:
|
|
226
|
+
self.ridx = ridx
|
|
227
|
+
self.dftable.move_cursor_to(self.ridx)
|
|
228
|
+
self.build_table()
|
|
213
229
|
event.stop()
|
|
214
230
|
|
|
215
231
|
def get_cidx_name_value(self) -> tuple[int, str, Any] | None:
|
|
@@ -275,19 +291,9 @@ class StatisticsScreen(TableScreen):
|
|
|
275
291
|
# Add rows
|
|
276
292
|
for row in stats_df.rows():
|
|
277
293
|
stat_label, stat_value = row
|
|
278
|
-
value = stat_value
|
|
279
|
-
if stat_value is None:
|
|
280
|
-
value = NULL_DISPLAY
|
|
281
|
-
elif dc.gtype == "integer" and self.thousand_separator:
|
|
282
|
-
value = f"{stat_value:,}"
|
|
283
|
-
elif dc.gtype == "float":
|
|
284
|
-
value = format_float(stat_value, self.thousand_separator)
|
|
285
|
-
else:
|
|
286
|
-
value = str(stat_value)
|
|
287
|
-
|
|
288
294
|
self.table.add_row(
|
|
289
|
-
|
|
290
|
-
|
|
295
|
+
stat_label,
|
|
296
|
+
dc.format(stat_value, thousand_separator=self.thousand_separator),
|
|
291
297
|
)
|
|
292
298
|
|
|
293
299
|
def build_dataframe_stats(self) -> None:
|
|
@@ -329,17 +335,7 @@ class StatisticsScreen(TableScreen):
|
|
|
329
335
|
col_dtype = stats_df.dtypes[idx]
|
|
330
336
|
dc = DtypeConfig(col_dtype)
|
|
331
337
|
|
|
332
|
-
|
|
333
|
-
if stat_value is None:
|
|
334
|
-
value = NULL_DISPLAY
|
|
335
|
-
elif dc.gtype == "integer" and self.thousand_separator:
|
|
336
|
-
value = f"{stat_value:,}"
|
|
337
|
-
elif dc.gtype == "float":
|
|
338
|
-
value = format_float(stat_value, self.thousand_separator)
|
|
339
|
-
else:
|
|
340
|
-
value = str(stat_value)
|
|
341
|
-
|
|
342
|
-
formatted_row.append(Text(value, style=dc.style, justify=dc.justify))
|
|
338
|
+
formatted_row.append(dc.format(stat_value, thousand_separator=self.thousand_separator))
|
|
343
339
|
|
|
344
340
|
self.table.add_row(*formatted_row)
|
|
345
341
|
|
|
@@ -412,33 +408,18 @@ class FrequencyScreen(TableScreen):
|
|
|
412
408
|
self.table.add_column(Text(header_text, justify=justify), key=key)
|
|
413
409
|
|
|
414
410
|
# Get style config for Int64 and Float64
|
|
415
|
-
|
|
416
|
-
|
|
411
|
+
dc_int = DtypeConfig(pl.Int64)
|
|
412
|
+
dc_float = DtypeConfig(pl.Float64)
|
|
417
413
|
|
|
418
414
|
# Add rows to the frequency table
|
|
419
415
|
for row_idx, row in enumerate(self.df.rows()):
|
|
420
416
|
column, count = row
|
|
421
417
|
percentage = (count / self.total_count) * 100
|
|
422
418
|
|
|
423
|
-
if column is None:
|
|
424
|
-
value = NULL_DISPLAY
|
|
425
|
-
elif dc.gtype == "integer" and self.thousand_separator:
|
|
426
|
-
value = f"{column:,}"
|
|
427
|
-
elif dc.gtype == "float":
|
|
428
|
-
value = format_float(column, self.thousand_separator)
|
|
429
|
-
else:
|
|
430
|
-
value = str(column)
|
|
431
|
-
|
|
432
419
|
self.table.add_row(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
),
|
|
437
|
-
Text(
|
|
438
|
-
format_float(percentage, self.thousand_separator),
|
|
439
|
-
style=ds_float.style,
|
|
440
|
-
justify=ds_float.justify,
|
|
441
|
-
),
|
|
420
|
+
dc.format(column),
|
|
421
|
+
dc_int.format(count, thousand_separator=self.thousand_separator),
|
|
422
|
+
dc_float.format(percentage, thousand_separator=self.thousand_separator),
|
|
442
423
|
Bar(
|
|
443
424
|
highlight_range=(0.0, percentage / 100 * 10),
|
|
444
425
|
width=10,
|
|
@@ -455,7 +436,7 @@ class FrequencyScreen(TableScreen):
|
|
|
455
436
|
justify="right",
|
|
456
437
|
),
|
|
457
438
|
Text(
|
|
458
|
-
format_float(100.0, self.thousand_separator),
|
|
439
|
+
format_float(100.0, self.thousand_separator, precision=-2 if len(self.df) > 1 else 2),
|
|
459
440
|
style="bold",
|
|
460
441
|
justify="right",
|
|
461
442
|
),
|
|
@@ -503,3 +484,70 @@ class FrequencyScreen(TableScreen):
|
|
|
503
484
|
col_value = NULL if cell_value.plain == NULL_DISPLAY else DtypeConfig(col_dtype).convert(cell_value.plain)
|
|
504
485
|
|
|
505
486
|
return self.cidx, col_name, col_value
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class MetaShape(TableScreen):
|
|
490
|
+
"""Modal screen to display metadata about the dataframe."""
|
|
491
|
+
|
|
492
|
+
CSS = TableScreen.DEFAULT_CSS.replace("TableScreen", "MetadataScreen")
|
|
493
|
+
|
|
494
|
+
def on_mount(self) -> None:
|
|
495
|
+
"""Initialize the metadata screen.
|
|
496
|
+
|
|
497
|
+
Populates the table with metadata information about the dataframe,
|
|
498
|
+
including row and column counts.
|
|
499
|
+
"""
|
|
500
|
+
self.build_table()
|
|
501
|
+
|
|
502
|
+
def build_table(self) -> None:
|
|
503
|
+
"""Build the metadata table."""
|
|
504
|
+
self.table.clear(columns=True)
|
|
505
|
+
self.table.add_column("")
|
|
506
|
+
self.table.add_column(Text("Count", justify="right"))
|
|
507
|
+
|
|
508
|
+
# Get shape information
|
|
509
|
+
num_rows, num_cols = self.df.shape
|
|
510
|
+
dc_int = DtypeConfig(pl.Int64)
|
|
511
|
+
|
|
512
|
+
# Add rows to the table
|
|
513
|
+
self.table.add_row("Row", dc_int.format(num_rows, thousand_separator=self.thousand_separator))
|
|
514
|
+
self.table.add_row("Column", dc_int.format(num_cols, thousand_separator=self.thousand_separator))
|
|
515
|
+
|
|
516
|
+
self.table.cursor_type = "none"
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class MetaColumnScreen(TableScreen):
|
|
520
|
+
"""Modal screen to display metadata about the columns in the dataframe."""
|
|
521
|
+
|
|
522
|
+
CSS = TableScreen.DEFAULT_CSS.replace("TableScreen", "MetaColumnScreen")
|
|
523
|
+
|
|
524
|
+
def on_mount(self) -> None:
|
|
525
|
+
"""Initialize the column metadata screen.
|
|
526
|
+
|
|
527
|
+
Populates the table with information about each column in the dataframe,
|
|
528
|
+
including ID (1-based index), Name, and Type.
|
|
529
|
+
"""
|
|
530
|
+
self.build_table()
|
|
531
|
+
|
|
532
|
+
def build_table(self) -> None:
|
|
533
|
+
"""Build the column metadata table."""
|
|
534
|
+
self.table.clear(columns=True)
|
|
535
|
+
self.table.add_column("Column")
|
|
536
|
+
self.table.add_column("Name")
|
|
537
|
+
self.table.add_column("Type")
|
|
538
|
+
|
|
539
|
+
# Get schema information
|
|
540
|
+
schema = self.df.schema
|
|
541
|
+
dc_int = DtypeConfig(pl.Int64)
|
|
542
|
+
dc_str = DtypeConfig(pl.String)
|
|
543
|
+
|
|
544
|
+
# Add a row for each column
|
|
545
|
+
for idx, (col_name, col_type) in enumerate(schema.items(), 1):
|
|
546
|
+
dc = DtypeConfig(col_type)
|
|
547
|
+
self.table.add_row(
|
|
548
|
+
dc_int.format(idx, thousand_separator=self.thousand_separator),
|
|
549
|
+
col_name,
|
|
550
|
+
dc_str.format(col_type, style=dc.style),
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
self.table.cursor_type = "none"
|
|
@@ -314,7 +314,7 @@ class SaveFileScreen(YesNoScreen):
|
|
|
314
314
|
if self.input:
|
|
315
315
|
input_filename = self.input.value.strip()
|
|
316
316
|
if input_filename:
|
|
317
|
-
return input_filename, self.all_tabs
|
|
317
|
+
return input_filename, self.all_tabs, True # Overwrite prompt
|
|
318
318
|
else:
|
|
319
319
|
self.notify("Filename cannot be empty", title="Save", severity="error")
|
|
320
320
|
return None
|
|
@@ -359,7 +359,7 @@ class EditCellScreen(YesNoScreen):
|
|
|
359
359
|
|
|
360
360
|
# Input
|
|
361
361
|
df_value = df.item(ridx, cidx)
|
|
362
|
-
self.input_value =
|
|
362
|
+
self.input_value = NULL if df_value is None else str(df_value)
|
|
363
363
|
|
|
364
364
|
super().__init__(
|
|
365
365
|
title="Edit Cell",
|
|
@@ -373,20 +373,20 @@ class EditCellScreen(YesNoScreen):
|
|
|
373
373
|
|
|
374
374
|
def _validate_input(self) -> None:
|
|
375
375
|
"""Validate and save the edited value."""
|
|
376
|
-
new_value_str = self.input.value
|
|
376
|
+
new_value_str = self.input.value # Do not strip to preserve spaces
|
|
377
377
|
|
|
378
378
|
# Handle empty input
|
|
379
379
|
if not new_value_str:
|
|
380
|
-
new_value =
|
|
380
|
+
new_value = ""
|
|
381
381
|
self.notify(
|
|
382
|
-
"Empty value provided. If you want to clear the cell, press [$accent]
|
|
383
|
-
title="Edit",
|
|
382
|
+
"Empty value provided. If you want to clear the cell, press [$accent]Delete[/].",
|
|
383
|
+
title="Edit Cell",
|
|
384
384
|
severity="warning",
|
|
385
385
|
)
|
|
386
386
|
# Check if value changed
|
|
387
387
|
elif new_value_str == self.input_value:
|
|
388
388
|
new_value = None
|
|
389
|
-
self.notify("No changes made", title="Edit", severity="warning")
|
|
389
|
+
self.notify("No changes made", title="Edit Cell", severity="warning")
|
|
390
390
|
else:
|
|
391
391
|
# Parse and validate based on column dtype
|
|
392
392
|
try:
|
|
@@ -394,7 +394,7 @@ class EditCellScreen(YesNoScreen):
|
|
|
394
394
|
except Exception as e:
|
|
395
395
|
self.notify(
|
|
396
396
|
f"Failed to convert [$accent]{new_value_str}[/] to [$error]{self.dtype}[/]: {str(e)}",
|
|
397
|
-
title="Edit",
|
|
397
|
+
title="Edit Cell",
|
|
398
398
|
severity="error",
|
|
399
399
|
)
|
|
400
400
|
return None
|
|
@@ -477,7 +477,7 @@ class SearchScreen(YesNoScreen):
|
|
|
477
477
|
|
|
478
478
|
def _validate_input(self) -> tuple[str, int, bool, bool]:
|
|
479
479
|
"""Validate the input and return it."""
|
|
480
|
-
term = self.input.value
|
|
480
|
+
term = self.input.value # Do not strip to preserve spaces
|
|
481
481
|
|
|
482
482
|
if not term:
|
|
483
483
|
self.notify("Term cannot be empty", title=self.title, severity="error")
|
|
@@ -508,7 +508,7 @@ class FilterScreen(YesNoScreen):
|
|
|
508
508
|
|
|
509
509
|
def _get_input(self) -> tuple[str, int, bool, bool]:
|
|
510
510
|
"""Get input."""
|
|
511
|
-
term = self.input.value
|
|
511
|
+
term = self.input.value # Do not strip to preserve spaces
|
|
512
512
|
match_nocase = self.checkbox.value
|
|
513
513
|
match_whole = self.checkbox2.value
|
|
514
514
|
|
|
@@ -590,7 +590,7 @@ class EditColumnScreen(YesNoScreen):
|
|
|
590
590
|
|
|
591
591
|
def _get_input(self) -> tuple[str, int]:
|
|
592
592
|
"""Get input."""
|
|
593
|
-
term = self.input.value
|
|
593
|
+
term = self.input.value # Do not strip to preserve spaces
|
|
594
594
|
return term, self.cidx
|
|
595
595
|
|
|
596
596
|
|
|
@@ -606,19 +606,19 @@ class AddColumnScreen(YesNoScreen):
|
|
|
606
606
|
self.existing_columns = set(df.columns)
|
|
607
607
|
super().__init__(
|
|
608
608
|
title="Add Column",
|
|
609
|
-
label="
|
|
610
|
-
input="Link" if link else "
|
|
611
|
-
label2="
|
|
609
|
+
label="Column name",
|
|
610
|
+
input="Link" if link else "Name",
|
|
611
|
+
label2="Link template, e.g., https://example.com/$_/id/$1, PC/compound/$cid"
|
|
612
612
|
if link
|
|
613
|
-
else "
|
|
614
|
-
input2="Link template" if link else "
|
|
613
|
+
else "Value or Polars expression, e.g., abc, pl.lit(123), NULL, $_ * 2, $1 + $total, $_ + '_suffix', $_.str.to_uppercase()",
|
|
614
|
+
input2="Link template" if link else "Value or expression",
|
|
615
615
|
on_yes_callback=self._get_input,
|
|
616
616
|
)
|
|
617
617
|
|
|
618
618
|
def _get_input(self) -> tuple[int, str, str] | None:
|
|
619
619
|
"""Validate and return the new column configuration."""
|
|
620
620
|
col_name = self.input.value.strip()
|
|
621
|
-
term = self.input2.value
|
|
621
|
+
term = self.input2.value # Do not strip to preserve spaces
|
|
622
622
|
|
|
623
623
|
# Validate column name
|
|
624
624
|
if not col_name:
|
|
@@ -680,7 +680,11 @@ class FindReplaceScreen(YesNoScreen):
|
|
|
680
680
|
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "ReplaceScreen")
|
|
681
681
|
|
|
682
682
|
def __init__(self, dftable: "DataFrameTable", title: str = "Find and Replace"):
|
|
683
|
-
|
|
683
|
+
if (cursor_value := dftable.cursor_value) is None:
|
|
684
|
+
term_find = NULL
|
|
685
|
+
else:
|
|
686
|
+
term_find = str(cursor_value)
|
|
687
|
+
|
|
684
688
|
super().__init__(
|
|
685
689
|
title=title,
|
|
686
690
|
label="Find",
|
|
@@ -698,8 +702,8 @@ class FindReplaceScreen(YesNoScreen):
|
|
|
698
702
|
|
|
699
703
|
def _get_input(self) -> tuple[str, str, bool, bool, bool]:
|
|
700
704
|
"""Get input."""
|
|
701
|
-
term_find = self.input.value
|
|
702
|
-
term_replace = self.input2.value
|
|
705
|
+
term_find = self.input.value # Do not strip to preserve spaces
|
|
706
|
+
term_replace = self.input2.value # Do not strip to preserve spaces
|
|
703
707
|
match_nocase = self.checkbox.value
|
|
704
708
|
match_whole = self.checkbox2.value
|
|
705
709
|
replace_all = False
|
|
@@ -708,8 +712,8 @@ class FindReplaceScreen(YesNoScreen):
|
|
|
708
712
|
|
|
709
713
|
def _get_input_replace_all(self) -> tuple[str, str, bool, bool, bool]:
|
|
710
714
|
"""Get input for 'Replace All'."""
|
|
711
|
-
term_find = self.input.value
|
|
712
|
-
term_replace = self.input2.value
|
|
715
|
+
term_find = self.input.value # Do not strip to preserve spaces
|
|
716
|
+
term_replace = self.input2.value # Do not strip to preserve spaces
|
|
713
717
|
match_nocase = self.checkbox.value
|
|
714
718
|
match_whole = self.checkbox2.value
|
|
715
719
|
replace_all = True
|