seejx 0.3.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.
seejx/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """seejx - Interactive JSONL browser with rich rendering and full-screen scrolling.
2
+
3
+ Fork of `seej` (MIT, © 2025 Lei Zhang) with an in-app full-screen scroll
4
+ viewer: trackpad / mouse-wheel / arrow-key scrolling, wcwidth-aware wrapping,
5
+ and a soft status bar. See NOTICE for details.
6
+ """
7
+
8
+ __version__ = "0.3.0"
9
+
10
+ from .see_jsonl import main
11
+ from .renderers import register_renderer, get_renderer
12
+ from .conditions import SafeExampleProxy, build_condition_function
13
+
14
+ __all__ = [
15
+ "main",
16
+ "register_renderer",
17
+ "get_renderer",
18
+ "SafeExampleProxy",
19
+ "build_condition_function",
20
+ ]
seejx/conditions.py ADDED
@@ -0,0 +1,178 @@
1
+ """Condition parsing and evaluation for filtering examples."""
2
+
3
+ import ast
4
+ import operator
5
+ from typing import Any, Callable, Optional
6
+ from rich.console import Console
7
+
8
+ console = Console()
9
+
10
+
11
+ class SafeExampleProxy:
12
+ """安全的样本代理,支持 ex['key'] 和 ex.key 两种语法"""
13
+
14
+ def __init__(self, data: dict):
15
+ self._data = data
16
+
17
+ def __getitem__(self, key):
18
+ """支持 ex['key'] 语法"""
19
+ return self._data.get(key)
20
+
21
+ def __getattr__(self, key):
22
+ """支持 ex.key 语法"""
23
+ if key.startswith("_"):
24
+ raise AttributeError(f"Cannot access private attribute: {key}")
25
+ return self._data.get(key)
26
+
27
+ def __contains__(self, key):
28
+ """支持 'key' in ex"""
29
+ return key in self._data
30
+
31
+ def get(self, key, default=None):
32
+ """支持 ex.get('key', default)"""
33
+ return self._data.get(key, default)
34
+
35
+
36
+ def create_safe_namespace():
37
+ """创建安全的执行命名空间"""
38
+ # 只允许安全的内置函数和操作符
39
+ safe_builtins = {
40
+ "len": len,
41
+ "str": str,
42
+ "int": int,
43
+ "float": float,
44
+ "bool": bool,
45
+ "list": list,
46
+ "dict": dict,
47
+ "tuple": tuple,
48
+ "set": set,
49
+ "abs": abs,
50
+ "min": min,
51
+ "max": max,
52
+ "sum": sum,
53
+ "any": any,
54
+ "all": all,
55
+ "isinstance": isinstance,
56
+ "True": True,
57
+ "False": False,
58
+ "None": None,
59
+ }
60
+ return safe_builtins
61
+
62
+
63
+ def validate_condition_syntax(condition: str) -> tuple[bool, Optional[str]]:
64
+ """
65
+ 验证条件表达式的语法安全性
66
+
67
+ Returns:
68
+ (is_valid, error_message)
69
+ """
70
+ # 检查是否包含 'ex'
71
+ if "ex" not in condition:
72
+ return False, "Condition must reference 'ex' (e.g., ex['field'] == 'value')"
73
+
74
+ # 尝试解析 AST
75
+ try:
76
+ tree = ast.parse(condition, mode="eval")
77
+ except SyntaxError as e:
78
+ return False, f"Syntax error: {e}"
79
+
80
+ # 检查危险操作
81
+ dangerous_nodes = (
82
+ ast.Import,
83
+ ast.ImportFrom, # 禁止导入
84
+ ast.Call, # 暂时禁止所有函数调用(可以放宽)
85
+ )
86
+
87
+ for node in ast.walk(tree):
88
+ # 允许安全的函数调用
89
+ if isinstance(node, ast.Call):
90
+ if isinstance(node.func, ast.Name):
91
+ func_name = node.func.id
92
+ if func_name not in create_safe_namespace():
93
+ return False, f"Function '{func_name}' is not allowed"
94
+
95
+ # 禁止其他危险操作
96
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
97
+ return False, "Import statements are not allowed"
98
+
99
+ return True, None
100
+
101
+
102
+ def build_condition_function(conditions: list[str]) -> Optional[Callable]:
103
+ """
104
+ 构建条件过滤函数
105
+
106
+ Args:
107
+ conditions: 条件表达式列表,例如 ["ex['type'] == 'chat'", "len(ex['messages']) > 5"]
108
+
109
+ Returns:
110
+ 过滤函数或 None
111
+ """
112
+ if not conditions:
113
+ return None
114
+
115
+ console.print("[bold]Building filter conditions:[/bold]")
116
+
117
+ # 验证并处理每个条件
118
+ validated_conditions = []
119
+ for idx, cond in enumerate(conditions, 1):
120
+ cond = cond.strip()
121
+ if not cond:
122
+ continue
123
+
124
+ is_valid, error = validate_condition_syntax(cond)
125
+ if not is_valid:
126
+ console.print(f"[red]✗[/red] Condition {idx}: {cond}")
127
+ console.print(f" [red]Error: {error}[/red]")
128
+ continue
129
+
130
+ validated_conditions.append(cond)
131
+ console.print(f"[green]✓[/green] Condition {idx}: [cyan]{cond}[/cyan]")
132
+
133
+ if not validated_conditions:
134
+ console.print("[yellow]Warning: No valid conditions, showing all examples[/yellow]")
135
+ return None
136
+
137
+ # 组合所有条件
138
+ combined_expr = " and ".join(f"({c})" for c in validated_conditions)
139
+ console.print(f"\n[bold]Combined:[/bold] [dim]{combined_expr}[/dim]\n")
140
+
141
+ # 编译表达式
142
+ try:
143
+ compiled_code = compile(combined_expr, "<condition>", "eval")
144
+ except Exception as e:
145
+ console.print(f"[red]Failed to compile conditions: {e}[/red]")
146
+ return None
147
+
148
+ # 创建安全的命名空间
149
+ safe_namespace = create_safe_namespace()
150
+
151
+ def condition_filter(example: dict) -> bool:
152
+ """实际的过滤函数"""
153
+ try:
154
+ # 创建代理对象
155
+ ex = SafeExampleProxy(example)
156
+ # 在安全命名空间中执行
157
+ namespace = {**safe_namespace, "ex": ex}
158
+ result = eval(compiled_code, {"__builtins__": {}}, namespace)
159
+ return bool(result)
160
+ except KeyError as e:
161
+ # 字段不存在时返回 False
162
+ return False
163
+ except Exception as e:
164
+ # 其他错误:打印警告但不中断
165
+ # console.print(f"[yellow]Warning: Condition evaluation error: {e}[/yellow]")
166
+ return False
167
+
168
+ try:
169
+ console.print(f"\n[red][bold]>>>Press any key to see jsonl!<<<\n[/bold][/red]")
170
+ input()
171
+ except KeyboardInterrupt:
172
+ console.print(f"\n[bold]Keyboard Interruptted[/bold]\n")
173
+ exit(0)
174
+
175
+ return condition_filter
176
+
177
+
178
+ # ============ 向后兼容的旧版解析(可选保留) ============
seejx/config.py ADDED
@@ -0,0 +1,82 @@
1
+ """Configuration parsing for seejx."""
2
+
3
+ from typing import Dict, Callable
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from .renderers.base import get_renderer, list_renderers, get_all_renderer_names
7
+
8
+ console = Console()
9
+
10
+
11
+ def parse_render_mappings(render_args) -> Dict[str, Callable]:
12
+ """解析渲染映射参数"""
13
+ field_mapper = {}
14
+ if not render_args:
15
+ return field_mapper
16
+
17
+ for mapping in render_args:
18
+ if "=" not in mapping:
19
+ console.print(f"[yellow]Warning: Invalid mapping format '{mapping}', expected 'renderer=field1,field2'[/yellow]")
20
+ continue
21
+
22
+ renderer_name, fields_str = mapping.split("=", 1)
23
+ renderer = get_renderer(renderer_name.strip())
24
+
25
+ if renderer is None:
26
+ console.print(f"[yellow]Warning: Unknown renderer '{renderer_name}'[/yellow]")
27
+
28
+ # 尝试给出建议
29
+ all_names = get_all_renderer_names()
30
+ suggestions = [name for name in all_names if renderer_name.lower() in name.lower()]
31
+ if suggestions:
32
+ console.print(f"[dim]Did you mean: {', '.join(suggestions[:3])}?[/dim]")
33
+ console.print(f"[dim]Use --list-renderers to see all available options[/dim]")
34
+ continue
35
+
36
+ for field in fields_str.split(","):
37
+ field = field.strip()
38
+ if field:
39
+ field_mapper[field] = renderer
40
+ console.print(f"[green]✓[/green] Mapped '{field}' → {renderer_name}")
41
+
42
+ return field_mapper
43
+
44
+
45
+ def print_available_renderers():
46
+ """打印所有可用的渲染器"""
47
+ renderers = list_renderers()
48
+
49
+ if not renderers:
50
+ console.print("[yellow]No renderers available[/yellow]")
51
+ return
52
+
53
+ table = Table(title="Available Renderers", show_header=True, header_style="bold magenta")
54
+ table.add_column("Renderer", style="cyan", no_wrap=True)
55
+ table.add_column("Source", style="yellow", width=12)
56
+ table.add_column("Aliases", style="dim")
57
+ table.add_column("Description", style="green")
58
+
59
+ # 按来源分组排序
60
+ builtin_renderers = []
61
+ plugin_renderers = []
62
+
63
+ for name, info in renderers.items():
64
+ if info.source == "builtin":
65
+ builtin_renderers.append((name, info))
66
+ else:
67
+ plugin_renderers.append((name, info))
68
+
69
+ # 先显示内置,再显示插件
70
+ for name, info in sorted(builtin_renderers) + sorted(plugin_renderers):
71
+ source = info.source
72
+ if info.plugin_name:
73
+ source = f"📦 {info.plugin_name}"
74
+
75
+ aliases = ", ".join(info.aliases) if info.aliases else "-"
76
+ desc = info.description or "-"
77
+
78
+ table.add_row(name, source, aliases, desc)
79
+
80
+ console.print(table)
81
+ console.print("\n[bold]Usage:[/bold] seejx data.jsonl -r [cyan]<renderer>[/cyan]=[green]<field1>[/green],[green]<field2>[/green]...")
82
+ console.print("[bold]Example:[/bold] seejx data.jsonl -r messages=chat,conversation")
seejx/navigator.py ADDED
@@ -0,0 +1,419 @@
1
+ """Example navigator for browsing JSONL data.
2
+
3
+ 内建全屏滚动视图:整条记录常驻内存,只显示一个窗口。
4
+ - 两指滑动 / 鼠标滚轮 / ↑↓ / j k :上下滚动(永远能到最顶最底)
5
+ - PgUp/PgDn / 空格 / b :整页滚动
6
+ - g / G :跳到开头 / 结尾
7
+ - → / ←(或 n / p / 回车) :下一条 / 上一条记录
8
+ - t :切换 rich / raw 渲染
9
+ - q(或 Ctrl-C) :退出
10
+ """
11
+
12
+ import io
13
+ import os
14
+ import sys
15
+ import shutil
16
+ import termios
17
+ import tty
18
+ import select
19
+ from contextlib import redirect_stdout
20
+ from pathlib import Path
21
+
22
+ import readchar # noqa: F401 (保留以兼容其它模块)
23
+ from rich.console import Console
24
+ from rich.panel import Panel
25
+ from rich.text import Text
26
+
27
+ from .tools import get_terminal_width, load_jsonl_iter
28
+ from .renderers.base import get_renderer
29
+ from .renderers.base import console as _base_console
30
+ from .renderers import console
31
+
32
+ try:
33
+ from wcwidth import wcwidth as _wcwidth
34
+
35
+ def _cw(c):
36
+ w = _wcwidth(c)
37
+ return 1 if w < 0 else w
38
+ except Exception: # pragma: no cover
39
+
40
+ def _cw(c):
41
+ return 2 if ord(c) >= 0x1100 else 1
42
+
43
+
44
+ console = Console()
45
+
46
+ ESC = "\x1b"
47
+
48
+
49
+ def control_str(s, wrap=False, raw=False):
50
+ """打印控制信息(保留给非交互路径使用)"""
51
+ if raw:
52
+ console.print(s)
53
+ return
54
+ content = Text(s, style="bold red", overflow="fold")
55
+ if wrap:
56
+ content = Panel(content, expand=False, title="Control")
57
+ console.print(content)
58
+
59
+
60
+ def _wrap_ansi(line, width):
61
+ """按可见宽度硬换行,保留 ANSI 颜色(跨行续色)。"""
62
+ if width <= 0:
63
+ return [line]
64
+ line = line.rstrip("\r")
65
+ rows = []
66
+ cur = ""
67
+ vis = 0
68
+ active = [] # 当前生效的 SGR 序列,换行时续上
69
+ i, n = 0, len(line)
70
+
71
+ def flush(newline_active):
72
+ nonlocal cur, vis
73
+ rows.append(cur + (ESC + "[0m" if active else ""))
74
+ cur = "".join(active) if newline_active else ""
75
+ vis = 0
76
+
77
+ while i < n:
78
+ c = line[i]
79
+ if c == ESC and i + 1 < n and line[i + 1] == "[":
80
+ j = i + 2
81
+ while j < n and not ("@" <= line[j] <= "~"):
82
+ j += 1
83
+ seq = line[i : j + 1]
84
+ cur += seq
85
+ if seq.endswith("m"):
86
+ params = seq[2:-1]
87
+ if params in ("", "0"):
88
+ active = []
89
+ else:
90
+ active.append(seq)
91
+ i = j + 1
92
+ continue
93
+ if c == "\t":
94
+ spaces = 8 - (vis % 8)
95
+ for _ in range(spaces):
96
+ if vis >= width:
97
+ flush(True)
98
+ cur += " "
99
+ vis += 1
100
+ i += 1
101
+ continue
102
+ cwid = _cw(c)
103
+ if vis + cwid > width:
104
+ flush(True)
105
+ cur += c
106
+ vis += cwid
107
+ i += 1
108
+ rows.append(cur + (ESC + "[0m" if active else ""))
109
+ return rows
110
+
111
+
112
+ class ExampleNavigator:
113
+ """JSONL 示例浏览器(全屏滚动版)"""
114
+
115
+ def __init__(
116
+ self,
117
+ file_path,
118
+ start_idx=0,
119
+ no_rich=False,
120
+ condition_fn=None,
121
+ condition_display=None,
122
+ show_tool_call_details=False,
123
+ custom_mapper=None,
124
+ ):
125
+ self.file_path = file_path
126
+ self.start_idx = start_idx
127
+ self.no_rich = no_rich
128
+ self.condition_fn = condition_fn
129
+ self.condition_display = condition_display or "None"
130
+ self.custom_mapper = custom_mapper or {}
131
+
132
+ self._render_configs = {
133
+ "show_tool_call_details": show_tool_call_details,
134
+ }
135
+
136
+ self.current_idx = -1
137
+ self.iterator = load_jsonl_iter(file_path, skip=start_idx)
138
+ self.example_history = []
139
+
140
+ self.top = 0
141
+ self.status = ""
142
+ self._cache_key = None
143
+ self._wrapped = []
144
+
145
+ if not self._advance("next"):
146
+ print("No examples matched.")
147
+ return
148
+ self.run()
149
+
150
+ # ---------- 渲染 ----------
151
+
152
+ def get_renderer_for_field(self, field_name):
153
+ if field_name in self.custom_mapper:
154
+ return self.custom_mapper[field_name]
155
+ renderer = get_renderer(field_name)
156
+ if renderer is not None:
157
+ return renderer
158
+ return get_renderer("eye")
159
+
160
+ def _render_current_lines(self, cols):
161
+ """把当前记录渲染成带颜色的逻辑行列表。"""
162
+ file_idx, ex = self.example_history[self.current_idx]
163
+ consoles = [console, _base_console]
164
+ saved = [(c._force_terminal, c._width) for c in consoles]
165
+ for c in consoles:
166
+ c._force_terminal = True
167
+ c._width = cols
168
+ buf = io.StringIO()
169
+ try:
170
+ with redirect_stdout(buf):
171
+ console.rule(f"[bold cyan]▼ IDX={file_idx}[/bold cyan]", style="cyan")
172
+ for k, v in ex.items():
173
+ renderer = self.get_renderer_for_field(k)
174
+ console.print(
175
+ f"[[red][bold]{k}[/bold][/red] [dim]@{renderer.__name__}[/dim]]"
176
+ )
177
+ renderer(v, no_rich=self.no_rich, **self._render_configs)
178
+ print("-" * cols)
179
+ finally:
180
+ for c, (ft, w) in zip(consoles, saved):
181
+ c._force_terminal = ft
182
+ c._width = w
183
+ return buf.getvalue().split("\n")
184
+
185
+ def _get_wrapped(self, cols):
186
+ key = (cols, self.current_idx, self.no_rich)
187
+ if key != self._cache_key:
188
+ logical = self._render_current_lines(cols)
189
+ wrapped = []
190
+ for ln in logical:
191
+ wrapped.extend(_wrap_ansi(ln, cols))
192
+ self._wrapped = wrapped
193
+ self._cache_key = key
194
+ return self._wrapped
195
+
196
+ def _draw(self, wrapped, cols, rows, view_h, max_top):
197
+ out = [ESC + "[H"]
198
+ for i in range(view_h):
199
+ idx = self.top + i
200
+ row = wrapped[idx] if idx < len(wrapped) else ""
201
+ out.append(f"{ESC}[{i + 1};1H{ESC}[2K" + row)
202
+ total = len(wrapped)
203
+ bot = min(total, self.top + view_h)
204
+ pct = 100 if max_top == 0 else int(self.top / max_top * 100)
205
+ file_idx, _ = self.example_history[self.current_idx]
206
+ mode = "raw" if self.no_rich else "rich"
207
+
208
+ # 柔和状态条:深灰底 + 分色前景(不再用刺眼的反显白底黑字)
209
+ BG = f"{ESC}[48;5;236m"
210
+ segs = []
211
+ vis = 0
212
+
213
+ def add(text, fg):
214
+ """按剩余宽度截断后着色追加,保证整条不超过一行。"""
215
+ nonlocal vis
216
+ room = cols - vis
217
+ if room <= 0:
218
+ return
219
+ out_chars = []
220
+ for ch in text:
221
+ w = _cw(ch)
222
+ if w > room:
223
+ break
224
+ out_chars.append(ch)
225
+ room -= w
226
+ vis += w
227
+ if out_chars:
228
+ segs.append(f"{ESC}[38;5;{fg}m" + "".join(out_chars))
229
+
230
+ add(f" IDX={file_idx} ", 81) # 青色
231
+ add(f" {self.top + 1}-{bot}/{total} {pct:3d}% ", 252) # 浅灰
232
+ add(f" [{mode}] ", 179) # 暖黄
233
+ add(" ↕滚动 ←→上/下条 g/G首尾 t切换 q退出 ", 244) # 暗灰提示
234
+ if self.status:
235
+ add(f" {self.status} ", 210) # 浅红提醒
236
+ if vis < cols:
237
+ segs.append(" " * (cols - vis)) # 用底色补齐整行
238
+ bar = BG + "".join(segs) + ESC + "[0m"
239
+ out.append(f"{ESC}[{rows};1H{ESC}[2K" + bar)
240
+
241
+ sys.stdout.write("".join(out))
242
+ sys.stdout.flush()
243
+
244
+ # ---------- 记录导航 ----------
245
+
246
+ def _advance(self, direction):
247
+ if direction == "prev":
248
+ if self.current_idx > 0:
249
+ self.current_idx -= 1
250
+ self.status = ""
251
+ return True
252
+ self.status = "已是第一条"
253
+ return False
254
+ # next
255
+ if self.current_idx < len(self.example_history) - 1:
256
+ self.current_idx += 1
257
+ self.status = ""
258
+ return True
259
+ try:
260
+ with open(os.devnull, "w") as dn, redirect_stdout(dn):
261
+ while True:
262
+ new_example = next(self.iterator)
263
+ if self.condition_fn is None or self.condition_fn(new_example):
264
+ break
265
+ self.current_idx += 1
266
+ self.example_history.append((self.current_idx + self.start_idx, new_example))
267
+ self.status = ""
268
+ return True
269
+ except StopIteration:
270
+ self.status = "已到最后一条"
271
+ return False
272
+
273
+ # ---------- 输入 ----------
274
+
275
+ def _read_event(self):
276
+ fd = self.fd
277
+
278
+ def more(timeout=0.03):
279
+ r, _, _ = select.select([fd], [], [], timeout)
280
+ return os.read(fd, 1) if r else b""
281
+
282
+ ch = os.read(fd, 1)
283
+ if not ch or ch == b"\x03":
284
+ return "quit"
285
+ if ch in (b"q", b"Q", b"x", b"X"):
286
+ return "quit"
287
+ if ch in (b"t", b"T"):
288
+ return "toggle"
289
+ if ch == b" ":
290
+ return "pgdn"
291
+ if ch in (b"b", b"B"):
292
+ return "pgup"
293
+ if ch == b"g":
294
+ return "home"
295
+ if ch == b"G":
296
+ return "end"
297
+ if ch in (b"j",):
298
+ return "down"
299
+ if ch in (b"k",):
300
+ return "up"
301
+ if ch in (b"n", b"N", b"\r", b"\n"):
302
+ return "nextrec"
303
+ if ch in (b"p", b"P"):
304
+ return "prevrec"
305
+ if ch != b"\x1b":
306
+ return "none"
307
+
308
+ b1 = more()
309
+ if b1 == b"":
310
+ return "quit" # 单独 ESC
311
+ if b1 not in (b"[", b"O"):
312
+ return "none"
313
+ nb = more()
314
+ if nb == b"<": # SGR 鼠标/滚轮
315
+ data = b""
316
+ while True:
317
+ c = more()
318
+ if not c or c in (b"M", b"m"):
319
+ break
320
+ data += c
321
+ try:
322
+ cb = int(data.split(b";")[0])
323
+ except (ValueError, IndexError):
324
+ return "none"
325
+ if cb == 64:
326
+ return "wheelup"
327
+ if cb == 65:
328
+ return "wheeldown"
329
+ return "none"
330
+ seq = nb
331
+ while seq[-1:].isdigit() or seq[-1:] == b";":
332
+ c = more()
333
+ if not c:
334
+ break
335
+ seq += c
336
+ final = seq[-1:]
337
+ if final == b"A":
338
+ return "up"
339
+ if final == b"B":
340
+ return "down"
341
+ if final == b"C":
342
+ return "nextrec"
343
+ if final == b"D":
344
+ return "prevrec"
345
+ if final == b"H":
346
+ return "home"
347
+ if final == b"F":
348
+ return "end"
349
+ if final == b"~":
350
+ n = seq[:-1]
351
+ if n == b"5":
352
+ return "pgup"
353
+ if n == b"6":
354
+ return "pgdn"
355
+ if n in (b"1", b"7"):
356
+ return "home"
357
+ if n in (b"4", b"8"):
358
+ return "end"
359
+ return "none"
360
+
361
+ # ---------- 主循环 ----------
362
+
363
+ def run(self):
364
+ self.fd = sys.stdin.fileno()
365
+ try:
366
+ old = termios.tcgetattr(self.fd)
367
+ except termios.error:
368
+ # 非 tty(如管道):退回一次性打印
369
+ cols = get_terminal_width()
370
+ for ln in self._get_wrapped(cols):
371
+ print(ln)
372
+ return
373
+ try:
374
+ tty.setcbreak(self.fd)
375
+ # 备用屏 + 隐藏光标 + 开启鼠标滚轮上报(SGR)
376
+ sys.stdout.write(ESC + "[?1049h" + ESC + "[?25l" + ESC + "[?1000h" + ESC + "[?1006h")
377
+ sys.stdout.flush()
378
+ while True:
379
+ cols, rows = shutil.get_terminal_size((80, 24))
380
+ wrapped = self._get_wrapped(cols)
381
+ view_h = max(1, rows - 1)
382
+ max_top = max(0, len(wrapped) - view_h)
383
+ self.top = max(0, min(self.top, max_top))
384
+ self._draw(wrapped, cols, rows, view_h, max_top)
385
+
386
+ ev = self._read_event()
387
+ if ev == "quit":
388
+ break
389
+ elif ev == "toggle":
390
+ self.no_rich = not self.no_rich
391
+ self.top = 0
392
+ elif ev == "up":
393
+ self.top -= 1
394
+ elif ev == "down":
395
+ self.top += 1
396
+ elif ev == "wheelup":
397
+ self.top -= 3
398
+ elif ev == "wheeldown":
399
+ self.top += 3
400
+ elif ev == "pgup":
401
+ self.top -= max(1, view_h - 1)
402
+ elif ev == "pgdn":
403
+ self.top += max(1, view_h - 1)
404
+ elif ev == "home":
405
+ self.top = 0
406
+ elif ev == "end":
407
+ self.top = max_top
408
+ elif ev == "nextrec":
409
+ if self._advance("next"):
410
+ self.top = 0
411
+ elif ev == "prevrec":
412
+ if self._advance("prev"):
413
+ self.top = 0
414
+ finally:
415
+ sys.stdout.write(
416
+ ESC + "[?1000l" + ESC + "[?1006l" + ESC + "[?25h" + ESC + "[?1049l"
417
+ )
418
+ sys.stdout.flush()
419
+ termios.tcsetattr(self.fd, termios.TCSADRAIN, old)