dataframe-textual 0.1.0__py3-none-any.whl → 1.1.0__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.
Potentially problematic release.
This version of dataframe-textual might be problematic. Click here for more details.
- dataframe_textual/__main__.py +65 -0
- dataframe_textual/common.py +340 -0
- {dataframe_viewer → dataframe_textual}/data_frame_help_panel.py +22 -4
- dataframe_textual/data_frame_table.py +2768 -0
- dataframe_textual/data_frame_viewer.py +472 -0
- dataframe_textual/table_screen.py +490 -0
- dataframe_textual/yes_no_screen.py +672 -0
- dataframe_textual-1.1.0.dist-info/METADATA +726 -0
- dataframe_textual-1.1.0.dist-info/RECORD +13 -0
- dataframe_textual-1.1.0.dist-info/entry_points.txt +2 -0
- dataframe_textual-0.1.0.dist-info/METADATA +0 -522
- dataframe_textual-0.1.0.dist-info/RECORD +0 -13
- dataframe_textual-0.1.0.dist-info/entry_points.txt +0 -2
- dataframe_viewer/__main__.py +0 -48
- dataframe_viewer/common.py +0 -204
- dataframe_viewer/data_frame_table.py +0 -1395
- dataframe_viewer/data_frame_viewer.py +0 -320
- dataframe_viewer/table_screen.py +0 -311
- dataframe_viewer/yes_no_screen.py +0 -409
- {dataframe_viewer → dataframe_textual}/__init__.py +0 -0
- {dataframe_textual-0.1.0.dist-info → dataframe_textual-1.1.0.dist-info}/WHEEL +0 -0
- {dataframe_textual-0.1.0.dist-info → dataframe_textual-1.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Entry point for running DataFrameViewer as a module."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .data_frame_viewer import DataFrameViewer
|
|
8
|
+
|
|
9
|
+
SUPPORTED_FORMATS = ["csv", "excel", "tsv", "parquet", "json", "ndjson"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
"""Run the DataFrame Viewer application.
|
|
14
|
+
|
|
15
|
+
Parses command-line arguments to determine input files or stdin, validates
|
|
16
|
+
file existence, and launches the interactive DataFrame Viewer application.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
None
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
SystemExit: If invalid arguments are provided or required files are missing.
|
|
23
|
+
"""
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
description="Interactive terminal based viewer/editor for tabular data (e.g., CSV/Excel).",
|
|
26
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
27
|
+
epilog="Examples:\n"
|
|
28
|
+
" dataframe-textual data.csv\n"
|
|
29
|
+
" dataframe-textual file1.csv file2.csv file3.csv\n"
|
|
30
|
+
" dataframe-textual data.xlsx (opens all sheets in tabs)\n"
|
|
31
|
+
" cat data.csv | dataframe-textual --format csv\n",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument("files", nargs="*", help="Files to view (or read from stdin)")
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"-f",
|
|
36
|
+
"--format",
|
|
37
|
+
choices=SUPPORTED_FORMATS,
|
|
38
|
+
help="Specify the format of the input files (csv, excel, tsv etc.)",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument("-H", "--no-header", action="store_true", help="Specify that input files have no header row")
|
|
41
|
+
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
filenames = []
|
|
44
|
+
|
|
45
|
+
# Check if reading from stdin (pipe or redirect)
|
|
46
|
+
if not sys.stdin.isatty():
|
|
47
|
+
filenames.append("-")
|
|
48
|
+
if args.files:
|
|
49
|
+
# Validate all files exist
|
|
50
|
+
for filename in args.files:
|
|
51
|
+
if not Path(filename).exists():
|
|
52
|
+
print(f"File not found: {filename}")
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
filenames.extend(args.files)
|
|
55
|
+
|
|
56
|
+
if not filenames:
|
|
57
|
+
parser.print_help()
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
|
|
60
|
+
app = DataFrameViewer(*filenames, file_format=args.format, has_header=not args.no_header)
|
|
61
|
+
app.run()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Common utilities and constants for dataframe_viewer."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import polars as pl
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
# Special string to represent null value
|
|
11
|
+
NULL = "NULL"
|
|
12
|
+
NULL_DISPLAY = "-"
|
|
13
|
+
|
|
14
|
+
# Boolean string mappings
|
|
15
|
+
BOOLS = {
|
|
16
|
+
"true": True,
|
|
17
|
+
"t": True,
|
|
18
|
+
"yes": True,
|
|
19
|
+
"y": True,
|
|
20
|
+
"1": True,
|
|
21
|
+
"false": False,
|
|
22
|
+
"f": False,
|
|
23
|
+
"no": False,
|
|
24
|
+
"n": False,
|
|
25
|
+
"0": False,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class DtypeClass:
|
|
31
|
+
gtype: str # generic, high-level type
|
|
32
|
+
style: str
|
|
33
|
+
justify: str
|
|
34
|
+
itype: str
|
|
35
|
+
convert: Any
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# itype is used by Input widget for input validation
|
|
39
|
+
# fmt: off
|
|
40
|
+
STYLES = {
|
|
41
|
+
# str
|
|
42
|
+
pl.String: DtypeClass(gtype="string", style="green", justify="left", itype="text", convert=str),
|
|
43
|
+
# int
|
|
44
|
+
pl.Int8: DtypeClass(gtype="integer", style="cyan", justify="right", itype="integer", convert=int),
|
|
45
|
+
pl.Int16: DtypeClass(gtype="integer", style="cyan", justify="right", itype="integer", convert=int),
|
|
46
|
+
pl.Int32: DtypeClass(gtype="integer", style="cyan", justify="right", itype="integer", convert=int),
|
|
47
|
+
pl.Int64: DtypeClass(gtype="integer", style="cyan", justify="right", itype="integer", convert=int),
|
|
48
|
+
pl.Int128: DtypeClass(gtype="integer", style="cyan", justify="right", itype="integer", convert=int),
|
|
49
|
+
pl.UInt8: DtypeClass(gtype="integer", style="cyan", justify="right", itype="integer", convert=int),
|
|
50
|
+
pl.UInt16: DtypeClass(gtype="integer", style="cyan", justify="right", itype="integer", convert=int),
|
|
51
|
+
pl.UInt32: DtypeClass(gtype="integer", style="cyan", justify="right", itype="integer", convert=int),
|
|
52
|
+
pl.UInt64: DtypeClass(gtype="integer", style="cyan", justify="right", itype="integer", convert=int),
|
|
53
|
+
# float
|
|
54
|
+
pl.Float32: DtypeClass(gtype="float", style="magenta", justify="right", itype="number", convert=float),
|
|
55
|
+
pl.Float64: DtypeClass(gtype="float", style="magenta", justify="right", itype="number", convert=float),
|
|
56
|
+
pl.Decimal: DtypeClass(gtype="float", style="magenta", justify="right", itype="number", convert=float),
|
|
57
|
+
# bool
|
|
58
|
+
pl.Boolean: DtypeClass(gtype="boolean", style="blue", justify="center", itype="text", convert=lambda x: BOOLS[x.lower()]),
|
|
59
|
+
# temporal
|
|
60
|
+
pl.Date: DtypeClass(gtype="temporal", style="yellow", justify="center", itype="text", convert=str),
|
|
61
|
+
pl.Datetime: DtypeClass(gtype="temporal", style="yellow", justify="center", itype="text", convert=str),
|
|
62
|
+
pl.Time: DtypeClass(gtype="temporal", style="yellow", justify="center", itype="text", convert=str),
|
|
63
|
+
# unknown
|
|
64
|
+
pl.Unknown: DtypeClass(gtype="unknown", style="", justify="", itype="text", convert=str),
|
|
65
|
+
}
|
|
66
|
+
# fmt: on
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Subscript digits mapping for sort indicators
|
|
70
|
+
SUBSCRIPT_DIGITS = {
|
|
71
|
+
0: "₀",
|
|
72
|
+
1: "₁",
|
|
73
|
+
2: "₂",
|
|
74
|
+
3: "₃",
|
|
75
|
+
4: "₄",
|
|
76
|
+
5: "₅",
|
|
77
|
+
6: "₆",
|
|
78
|
+
7: "₇",
|
|
79
|
+
8: "₈",
|
|
80
|
+
9: "₉",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Cursor types ("none" removed)
|
|
84
|
+
CURSOR_TYPES = ["row", "column", "cell"]
|
|
85
|
+
|
|
86
|
+
# For row index column
|
|
87
|
+
RIDX = "^_ridx_^"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def DtypeConfig(dtype: pl.DataType) -> DtypeClass:
|
|
91
|
+
"""Get the DtypeClass configuration for a given Polars data type.
|
|
92
|
+
|
|
93
|
+
Retrieves styling and formatting configuration based on the Polars data type,
|
|
94
|
+
including style (color), justification, and type conversion function.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
dtype: A Polars data type to get configuration for.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
A DtypeClass containing style, justification, input type, and conversion function.
|
|
101
|
+
"""
|
|
102
|
+
if dc := STYLES.get(dtype):
|
|
103
|
+
return dc
|
|
104
|
+
elif isinstance(dtype, pl.Datetime):
|
|
105
|
+
return STYLES[pl.Datetime]
|
|
106
|
+
elif isinstance(dtype, pl.Date):
|
|
107
|
+
return STYLES[pl.Date]
|
|
108
|
+
elif isinstance(dtype, pl.Time):
|
|
109
|
+
return STYLES[pl.Time]
|
|
110
|
+
else:
|
|
111
|
+
return STYLES[pl.Unknown]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def format_float(value: float, thousand_separator: bool = False, precision: int = 2) -> str:
|
|
115
|
+
"""Format a float value, keeping integers without decimal point.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
val: The float value to format.
|
|
119
|
+
thousand_separator: Whether to include thousand separators. Defaults to False.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
The formatted float as a string.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
if (val := int(value)) == value:
|
|
126
|
+
return f"{val:,}" if thousand_separator else str(val)
|
|
127
|
+
else:
|
|
128
|
+
if precision > 0:
|
|
129
|
+
return f"{value:,.{precision}f}" if thousand_separator else f"{value:.{precision}f}"
|
|
130
|
+
else:
|
|
131
|
+
return f"{value:,f}" if thousand_separator else str(value)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def format_row(vals, dtypes, apply_justify=True, thousand_separator=False) -> list[Text]:
|
|
135
|
+
"""Format a single row with proper styling and justification.
|
|
136
|
+
|
|
137
|
+
Converts raw row values to formatted Rich Text objects with appropriate
|
|
138
|
+
styling (colors), justification, and null value handling based on data types.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
vals: The list of values in the row.
|
|
142
|
+
dtypes: The list of data types corresponding to each value.
|
|
143
|
+
apply_justify: Whether to apply justification styling. Defaults to True.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
A list of Rich Text objects with proper formatting applied.
|
|
147
|
+
"""
|
|
148
|
+
formatted_row = []
|
|
149
|
+
|
|
150
|
+
for val, dtype in zip(vals, dtypes, strict=True):
|
|
151
|
+
dc = DtypeConfig(dtype)
|
|
152
|
+
|
|
153
|
+
# Format the value
|
|
154
|
+
if val is None:
|
|
155
|
+
text_val = NULL_DISPLAY
|
|
156
|
+
elif dc.gtype == "integer" and thousand_separator:
|
|
157
|
+
text_val = f"{val:,}"
|
|
158
|
+
elif dc.gtype == "float":
|
|
159
|
+
text_val = format_float(val, thousand_separator)
|
|
160
|
+
else:
|
|
161
|
+
text_val = str(val)
|
|
162
|
+
|
|
163
|
+
formatted_row.append(
|
|
164
|
+
Text(
|
|
165
|
+
text_val,
|
|
166
|
+
style=dc.style,
|
|
167
|
+
justify=dc.justify if apply_justify else "",
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return formatted_row
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def rindex(lst: list, value) -> int:
|
|
175
|
+
"""Return the last index of value in a list. Return -1 if not found.
|
|
176
|
+
|
|
177
|
+
Searches through the list in reverse order to find the last occurrence
|
|
178
|
+
of the given value.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
lst: The list to search through.
|
|
182
|
+
value: The value to find.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The index (0-based) of the last occurrence, or -1 if not found.
|
|
186
|
+
"""
|
|
187
|
+
for i, item in enumerate(reversed(lst)):
|
|
188
|
+
if item == value:
|
|
189
|
+
return len(lst) - 1 - i
|
|
190
|
+
return -1
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_next_item(lst: list[Any], current, offset=1) -> Any:
|
|
194
|
+
"""Return the next item in the list after the current item, cycling if needed.
|
|
195
|
+
|
|
196
|
+
Finds the current item in the list and returns the item at position (current_index + offset),
|
|
197
|
+
wrapping around to the beginning if necessary.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
lst: The list to cycle through.
|
|
201
|
+
current: The current item (must be in the list).
|
|
202
|
+
offset: The number of positions to advance. Defaults to 1.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
The next item in the list after advancing by the offset.
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
ValueError: If the current item is not found in the list.
|
|
209
|
+
"""
|
|
210
|
+
if current not in lst:
|
|
211
|
+
raise ValueError("Current item not in list")
|
|
212
|
+
current_index = lst.index(current)
|
|
213
|
+
next_index = (current_index + offset) % len(lst)
|
|
214
|
+
return lst[next_index]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def parse_polars_expression(expression: str, df: pl.DataFrame, current_col_idx: int) -> str:
|
|
218
|
+
"""Parse and convert an expression to Polars syntax.
|
|
219
|
+
|
|
220
|
+
Replaces column references with Polars col() expressions:
|
|
221
|
+
- $_ - Current selected column
|
|
222
|
+
- $# - Row index (1-based, requires '^__ridx__^' column to be present)
|
|
223
|
+
- $1, $2, etc. - Column by 1-based index
|
|
224
|
+
- $col_name - Column by name (valid identifier starting with _ or letter)
|
|
225
|
+
|
|
226
|
+
Examples:
|
|
227
|
+
- "$_ > 50" -> "pl.col('current_col') > 50"
|
|
228
|
+
- "$# > 10" -> "pl.col('^__ridx__^') > 10"
|
|
229
|
+
- "$1 > 50" -> "pl.col('col0') > 50"
|
|
230
|
+
- "$name == 'Alex'" -> "pl.col('name') == 'Alex'"
|
|
231
|
+
- "$age < $salary" -> "pl.col('age') < pl.col('salary')"
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
expression: The input expression as a string.
|
|
235
|
+
df: The DataFrame to validate column references.
|
|
236
|
+
current_col_idx: The index of the currently selected column (0-based). Used for $_ reference.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
A Python expression string with $references replaced by pl.col() calls.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
ValueError: If a column reference is invalid.
|
|
243
|
+
"""
|
|
244
|
+
# Early return if no $ present
|
|
245
|
+
if "$" not in expression:
|
|
246
|
+
if "pl." in expression:
|
|
247
|
+
# This may be valid Polars expression already
|
|
248
|
+
return expression
|
|
249
|
+
else:
|
|
250
|
+
# Return as a literal string
|
|
251
|
+
return f"pl.lit({expression})"
|
|
252
|
+
|
|
253
|
+
# Pattern to match $ followed by either:
|
|
254
|
+
# - _ (single underscore)
|
|
255
|
+
# - # (hash for row index)
|
|
256
|
+
# - digits (integer)
|
|
257
|
+
# - identifier (starts with letter or _, followed by letter/digit/_)
|
|
258
|
+
pattern = r"\$(_|#|\d+|[a-zA-Z_]\w*)"
|
|
259
|
+
|
|
260
|
+
def replace_column_ref(match):
|
|
261
|
+
col_ref = match.group(1)
|
|
262
|
+
|
|
263
|
+
if col_ref == "_":
|
|
264
|
+
# Current selected column
|
|
265
|
+
col_name = df.columns[current_col_idx]
|
|
266
|
+
elif col_ref == "#":
|
|
267
|
+
# RIDX is used to store 0-based row index; add 1 for 1-based index
|
|
268
|
+
return f"(pl.col('{RIDX}') + 1)"
|
|
269
|
+
elif col_ref.isdigit():
|
|
270
|
+
# Column by 1-based index
|
|
271
|
+
col_idx = int(col_ref) - 1
|
|
272
|
+
if col_idx < 0 or col_idx >= len(df.columns):
|
|
273
|
+
raise ValueError(f"Column index out of range: ${col_ref}")
|
|
274
|
+
col_name = df.columns[col_idx]
|
|
275
|
+
else:
|
|
276
|
+
# Column by name
|
|
277
|
+
if col_ref not in df.columns:
|
|
278
|
+
raise ValueError(f"Column not found: ${col_ref}")
|
|
279
|
+
col_name = col_ref
|
|
280
|
+
|
|
281
|
+
return f"pl.col('{col_name}')"
|
|
282
|
+
|
|
283
|
+
result = re.sub(pattern, replace_column_ref, expression)
|
|
284
|
+
return result
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def tentative_expr(term: str) -> bool:
|
|
288
|
+
"""Check if the given term could be a Polars expression.
|
|
289
|
+
|
|
290
|
+
Heuristically determines whether a string might represent a Polars expression
|
|
291
|
+
based on common patterns like column references ($) or direct Polars syntax (pl.).
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
term: The string to check.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
True if the term appears to be a Polars expression, False otherwise.
|
|
298
|
+
"""
|
|
299
|
+
if "$" in term and not term.endswith("$"):
|
|
300
|
+
return True
|
|
301
|
+
if "pl." in term:
|
|
302
|
+
return True
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def validate_expr(term: str, df: pl.DataFrame, current_col_idx: int) -> pl.Expr | None:
|
|
307
|
+
"""Validate and return the expression.
|
|
308
|
+
|
|
309
|
+
Parses a user-provided expression string and validates it as a valid Polars expression.
|
|
310
|
+
Converts special syntax like $_ references to proper Polars col() expressions.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
term: The input expression as a string.
|
|
314
|
+
df: The DataFrame to validate column references against.
|
|
315
|
+
current_col_idx: The index of the currently selected column (0-based). Used for $_ reference.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
A valid Polars expression object if validation succeeds.
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
ValueError: If the expression is invalid, contains non-existent column references, or cannot be evaluated.
|
|
322
|
+
"""
|
|
323
|
+
term = term.strip()
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
# Parse the expression
|
|
327
|
+
expr_str = parse_polars_expression(term, df, current_col_idx)
|
|
328
|
+
|
|
329
|
+
# Validate by evaluating it
|
|
330
|
+
try:
|
|
331
|
+
expr = eval(expr_str, {"pl": pl})
|
|
332
|
+
if not isinstance(expr, pl.Expr):
|
|
333
|
+
raise ValueError(f"Expression evaluated to `{type(expr).__name__}` instead of a Polars expression")
|
|
334
|
+
|
|
335
|
+
# Expression is valid
|
|
336
|
+
return expr
|
|
337
|
+
except Exception as e:
|
|
338
|
+
raise ValueError(f"Failed to evaluate expression `{expr_str}`: {e}") from e
|
|
339
|
+
except Exception as ve:
|
|
340
|
+
raise ValueError(f"Failed to validate expression `{term}`: {ve}") from ve
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from textwrap import dedent
|
|
4
4
|
|
|
5
|
+
from textual.app import ComposeResult
|
|
5
6
|
from textual.containers import VerticalScroll
|
|
6
7
|
from textual.css.query import NoMatches
|
|
7
8
|
from textual.widget import Widget
|
|
@@ -19,8 +20,8 @@ class DataFrameHelpPanel(Widget):
|
|
|
19
20
|
DataFrameHelpPanel {
|
|
20
21
|
split: right;
|
|
21
22
|
width: 33%;
|
|
22
|
-
min-width:
|
|
23
|
-
max-width:
|
|
23
|
+
min-width: 40;
|
|
24
|
+
max-width: 80;
|
|
24
25
|
border-left: vkey $foreground 30%;
|
|
25
26
|
padding: 0 1;
|
|
26
27
|
height: 1fr;
|
|
@@ -68,7 +69,16 @@ class DataFrameHelpPanel(Widget):
|
|
|
68
69
|
|
|
69
70
|
DEFAULT_CLASSES = "-textual-system"
|
|
70
71
|
|
|
71
|
-
def on_mount(self):
|
|
72
|
+
def on_mount(self) -> None:
|
|
73
|
+
"""Set up help panel when mounted.
|
|
74
|
+
|
|
75
|
+
Initializes the help panel by setting up a watcher for focused widget changes
|
|
76
|
+
to dynamically update help text based on which widget has focus.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
None
|
|
80
|
+
"""
|
|
81
|
+
|
|
72
82
|
def update_help(focused_widget: Widget | None):
|
|
73
83
|
self.update_help(focused_widget)
|
|
74
84
|
|
|
@@ -94,5 +104,13 @@ class DataFrameHelpPanel(Widget):
|
|
|
94
104
|
except NoMatches:
|
|
95
105
|
pass
|
|
96
106
|
|
|
97
|
-
def compose(self):
|
|
107
|
+
def compose(self) -> ComposeResult:
|
|
108
|
+
"""Compose the help panel widget structure.
|
|
109
|
+
|
|
110
|
+
Creates and returns the widget hierarchy for the help panel,
|
|
111
|
+
including a VerticalScroll container with a Markdown display area.
|
|
112
|
+
|
|
113
|
+
Yields:
|
|
114
|
+
VerticalScroll: The main container with Markdown widget for help text.
|
|
115
|
+
"""
|
|
98
116
|
yield VerticalScroll(Markdown(id="widget-help"))
|