jnav 1.0.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.
- jnav/__init__.py +0 -0
- jnav/__main__.py +108 -0
- jnav/app.py +398 -0
- jnav/buffer.py +48 -0
- jnav/detail_tree.py +242 -0
- jnav/field_mapping.py +136 -0
- jnav/filter_manager_screen.py +100 -0
- jnav/filter_provider.py +103 -0
- jnav/filter_tree.py +344 -0
- jnav/filtering.py +191 -0
- jnav/header.py +48 -0
- jnav/help_screen.py +79 -0
- jnav/inline_tree.py +75 -0
- jnav/key_sequences.py +211 -0
- jnav/log_entry_item.py +96 -0
- jnav/log_entry_renderer.py +90 -0
- jnav/log_list_view.py +292 -0
- jnav/log_model.py +114 -0
- jnav/logging.py +52 -0
- jnav/manager_screen_common.py +27 -0
- jnav/model.py +13 -0
- jnav/offset_group.py +30 -0
- jnav/parsing.py +101 -0
- jnav/reading.py +52 -0
- jnav/role_mapper.py +53 -0
- jnav/scrollbar_overlay.py +79 -0
- jnav/search_engine.py +87 -0
- jnav/selector_manager_screen.py +187 -0
- jnav/selector_provider.py +64 -0
- jnav/store.py +37 -0
- jnav/text_input_screen.py +63 -0
- jnav/tree_rendering.py +194 -0
- jnav/virtual_list_view.py +383 -0
- jnav-1.0.0.dist-info/METADATA +186 -0
- jnav-1.0.0.dist-info/RECORD +37 -0
- jnav-1.0.0.dist-info/WHEEL +4 -0
- jnav-1.0.0.dist-info/entry_points.txt +3 -0
jnav/inline_tree.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from rich.style import Style
|
|
2
|
+
from rich.text import Text
|
|
3
|
+
from rich.tree import Tree as RichTree
|
|
4
|
+
|
|
5
|
+
from .filtering import get_nested
|
|
6
|
+
from .parsing import ParsedEntry
|
|
7
|
+
from .search_engine import SearchEngine
|
|
8
|
+
from .tree_rendering import TreeBuildVisitor, walk_tree
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _add_branch(
|
|
12
|
+
parent: RichTree,
|
|
13
|
+
label: Text,
|
|
14
|
+
path: str,
|
|
15
|
+
value: object,
|
|
16
|
+
) -> RichTree:
|
|
17
|
+
del path, value # unused
|
|
18
|
+
return parent.add(label)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _add_leaf(
|
|
22
|
+
parent: RichTree,
|
|
23
|
+
label: Text,
|
|
24
|
+
path: str,
|
|
25
|
+
value: object,
|
|
26
|
+
) -> None:
|
|
27
|
+
del path, value # unused
|
|
28
|
+
parent.add(label)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def render_inline_tree(
|
|
32
|
+
parsed: ParsedEntry,
|
|
33
|
+
*,
|
|
34
|
+
custom_fields: set[str],
|
|
35
|
+
search: SearchEngine | None = None,
|
|
36
|
+
key_style: Style | None = None,
|
|
37
|
+
selected_style: Style | None = None,
|
|
38
|
+
value_style: Style | None = None,
|
|
39
|
+
value_null_style: Style | None = None,
|
|
40
|
+
json_string_style: Style | None = None,
|
|
41
|
+
search_highlight_style: Style | None = None,
|
|
42
|
+
) -> RichTree | None:
|
|
43
|
+
if not custom_fields:
|
|
44
|
+
return None
|
|
45
|
+
_default = Style()
|
|
46
|
+
values = [(f, get_nested(parsed.expanded, f)) for f in custom_fields]
|
|
47
|
+
|
|
48
|
+
# Filter out selectors with no matches
|
|
49
|
+
filtered = {f: v for f, v in values if v is not None}
|
|
50
|
+
|
|
51
|
+
# Don't render the tree if there are no selectors with matches
|
|
52
|
+
if not filtered:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
tree = RichTree("", guide_style="dim", hide_root=True)
|
|
56
|
+
visitor = TreeBuildVisitor(
|
|
57
|
+
root=tree,
|
|
58
|
+
add_branch=_add_branch,
|
|
59
|
+
add_leaf=_add_leaf,
|
|
60
|
+
selected=custom_fields,
|
|
61
|
+
key_style=key_style or _default,
|
|
62
|
+
selected_style=selected_style or _default,
|
|
63
|
+
value_style=value_style or _default,
|
|
64
|
+
value_null_style=value_null_style or _default,
|
|
65
|
+
json_string_style=json_string_style or _default,
|
|
66
|
+
search_highlight_style=search_highlight_style or _default,
|
|
67
|
+
search_term=search.term if search else None,
|
|
68
|
+
)
|
|
69
|
+
walk_tree(
|
|
70
|
+
value=filtered,
|
|
71
|
+
path="",
|
|
72
|
+
visitor=visitor,
|
|
73
|
+
json_paths=parsed.expanded_paths,
|
|
74
|
+
)
|
|
75
|
+
return tree
|
jnav/key_sequences.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
3
|
+
|
|
4
|
+
from textual.binding import Binding, BindingsMap
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from textual.events import Key
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# pyright: reportUninitializedInstanceVariable=false
|
|
11
|
+
# pyright: reportAttributeAccessIssue=false
|
|
12
|
+
# pyright: reportUnknownMemberType=false
|
|
13
|
+
# pyright: reportUnknownVariableType=false
|
|
14
|
+
# pyright: reportUnknownArgumentType=false
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class KeySequence:
|
|
19
|
+
keys: str
|
|
20
|
+
action: str
|
|
21
|
+
description: str = ""
|
|
22
|
+
show: bool = True
|
|
23
|
+
id: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class KeySequenceMixin:
|
|
27
|
+
"""Mixin that adds multi-key sequence support to a Textual widget.
|
|
28
|
+
|
|
29
|
+
Usage::
|
|
30
|
+
|
|
31
|
+
class MyWidget(KeySequenceMixin, Tree):
|
|
32
|
+
SEQUENCES = [
|
|
33
|
+
KeySequence("ff", "filter_and", "Filter AND"),
|
|
34
|
+
KeySequence("fo", "filter_or", "Filter OR"),
|
|
35
|
+
]
|
|
36
|
+
SEQUENCE_GROUPS = {"f": "Filter..."}
|
|
37
|
+
|
|
38
|
+
async def on_key(self, event: Key) -> None:
|
|
39
|
+
if await self._handle_sequence_key(event):
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
SEQUENCES maps key strings (min 2 chars) to actions.
|
|
43
|
+
SEQUENCE_GROUPS maps a prefix character to a label shown in the
|
|
44
|
+
footer (e.g. ``"f"`` -> ``"Filter..."``). Prefixes without an
|
|
45
|
+
entry still work but won't show a footer hint.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
SEQUENCES: ClassVar[list[KeySequence]] = []
|
|
49
|
+
SEQUENCE_GROUPS: ClassVar[dict[str, str]] = {}
|
|
50
|
+
|
|
51
|
+
_seq_buffer: str = ""
|
|
52
|
+
_seq_pending: bool = False
|
|
53
|
+
_seq_saved_bindings: BindingsMap | None = None
|
|
54
|
+
_seq_saved_ancestor_bindings: list[tuple[object, BindingsMap]] = [] # noqa: RUF012
|
|
55
|
+
_seq_keymap: dict[str, str] = {} # noqa: RUF012
|
|
56
|
+
_seq_lookup: dict[str, KeySequence] = {} # noqa: RUF012
|
|
57
|
+
_seq_prefixes: dict[str, str] = {} # noqa: RUF012
|
|
58
|
+
_seq_base_bindings: BindingsMap | None = None
|
|
59
|
+
|
|
60
|
+
async def on_mount(self) -> None:
|
|
61
|
+
self._seq_buffer = ""
|
|
62
|
+
self._seq_pending = False
|
|
63
|
+
self._seq_saved_bindings = None
|
|
64
|
+
self._seq_base_bindings = None
|
|
65
|
+
self._rebuild_sequences()
|
|
66
|
+
|
|
67
|
+
def _rebuild_sequences(self) -> None:
|
|
68
|
+
if self._seq_pending:
|
|
69
|
+
self._seq_pending = False
|
|
70
|
+
self._seq_buffer = ""
|
|
71
|
+
if self._seq_saved_bindings is not None:
|
|
72
|
+
self._bindings = self._seq_saved_bindings
|
|
73
|
+
self._seq_saved_bindings = None
|
|
74
|
+
|
|
75
|
+
self._seq_lookup = self._build_seq_lookup()
|
|
76
|
+
self._seq_prefixes = self._build_seq_prefixes()
|
|
77
|
+
|
|
78
|
+
if self._seq_base_bindings is None:
|
|
79
|
+
self._seq_base_bindings = self._bindings.copy()
|
|
80
|
+
else:
|
|
81
|
+
self._bindings = self._seq_base_bindings.copy()
|
|
82
|
+
|
|
83
|
+
self._inject_prefix_bindings()
|
|
84
|
+
|
|
85
|
+
def _build_seq_lookup(self) -> dict[str, KeySequence]:
|
|
86
|
+
lookup: dict[str, KeySequence] = {}
|
|
87
|
+
for seq in self.SEQUENCES:
|
|
88
|
+
keys = seq.keys
|
|
89
|
+
if seq.id and seq.id in self._seq_keymap:
|
|
90
|
+
keys = self._seq_keymap[seq.id]
|
|
91
|
+
if len(keys) < 2:
|
|
92
|
+
msg = f"Sequence keys must be at least 2 characters, got {keys!r}"
|
|
93
|
+
raise ValueError(msg)
|
|
94
|
+
lookup[keys] = seq
|
|
95
|
+
return lookup
|
|
96
|
+
|
|
97
|
+
def _build_seq_prefixes(self) -> dict[str, str]:
|
|
98
|
+
prefixes: dict[str, str] = {}
|
|
99
|
+
for keys in self._seq_lookup:
|
|
100
|
+
char = keys[0]
|
|
101
|
+
if char not in prefixes:
|
|
102
|
+
prefixes[char] = self.SEQUENCE_GROUPS.get(char, "")
|
|
103
|
+
return prefixes
|
|
104
|
+
|
|
105
|
+
def _inject_prefix_bindings(self) -> None:
|
|
106
|
+
# Build prefix bindings first, then prepend them so they appear before
|
|
107
|
+
# the widget's own bindings in the footer.
|
|
108
|
+
prefix_entries: list[tuple[str, Binding]] = []
|
|
109
|
+
for char, label in self._seq_prefixes.items():
|
|
110
|
+
self._bindings.key_to_bindings.pop(char, None)
|
|
111
|
+
if label:
|
|
112
|
+
prefix_entries.append((
|
|
113
|
+
char,
|
|
114
|
+
Binding(char, f"_seq_prefix_{char}", label),
|
|
115
|
+
))
|
|
116
|
+
if not prefix_entries:
|
|
117
|
+
return
|
|
118
|
+
existing = dict(self._bindings.key_to_bindings)
|
|
119
|
+
self._bindings.key_to_bindings.clear()
|
|
120
|
+
for char, binding in prefix_entries:
|
|
121
|
+
self._bindings.key_to_bindings[char] = [binding]
|
|
122
|
+
for key, bindings in existing.items():
|
|
123
|
+
self._bindings.key_to_bindings[key] = bindings
|
|
124
|
+
|
|
125
|
+
async def _handle_sequence_key(self, event: Key) -> bool:
|
|
126
|
+
if self._seq_pending:
|
|
127
|
+
event.prevent_default()
|
|
128
|
+
event.stop()
|
|
129
|
+
|
|
130
|
+
if event.key == "escape":
|
|
131
|
+
self._reset_sequence()
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
self._seq_buffer += event.key
|
|
135
|
+
|
|
136
|
+
has_longer = any(
|
|
137
|
+
k.startswith(self._seq_buffer) and k != self._seq_buffer
|
|
138
|
+
for k in self._seq_lookup
|
|
139
|
+
)
|
|
140
|
+
if has_longer:
|
|
141
|
+
self._show_continuations(self._seq_buffer)
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
if self._seq_buffer in self._seq_lookup:
|
|
145
|
+
seq = self._seq_lookup[self._seq_buffer]
|
|
146
|
+
self._reset_sequence()
|
|
147
|
+
await self.run_action(seq.action)
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
self._reset_sequence()
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
if event.key in self._seq_prefixes:
|
|
154
|
+
self._seq_pending = True
|
|
155
|
+
self._seq_buffer = event.key
|
|
156
|
+
event.prevent_default()
|
|
157
|
+
event.stop()
|
|
158
|
+
self._show_continuations(event.key)
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
def _show_continuations(self, prefix: str) -> None:
|
|
164
|
+
if self._seq_saved_bindings is None:
|
|
165
|
+
self._seq_saved_bindings = self._bindings
|
|
166
|
+
self._hide_ancestor_bindings()
|
|
167
|
+
|
|
168
|
+
continuations: list[Binding] = []
|
|
169
|
+
seen: set[str] = set()
|
|
170
|
+
for keys, seq in self._seq_lookup.items():
|
|
171
|
+
if keys.startswith(prefix) and len(keys) > len(prefix):
|
|
172
|
+
next_char = keys[len(prefix)]
|
|
173
|
+
if next_char not in seen:
|
|
174
|
+
seen.add(next_char)
|
|
175
|
+
continuations.append(
|
|
176
|
+
Binding(
|
|
177
|
+
next_char,
|
|
178
|
+
f"_seq_cont_{seq.action}",
|
|
179
|
+
seq.description,
|
|
180
|
+
show=seq.show,
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
continuations.append(Binding("escape", "_seq_cancel", "Cancel", show=False))
|
|
184
|
+
self._bindings = BindingsMap(continuations)
|
|
185
|
+
self.refresh_bindings()
|
|
186
|
+
|
|
187
|
+
def _reset_sequence(self) -> None:
|
|
188
|
+
self._seq_pending = False
|
|
189
|
+
self._seq_buffer = ""
|
|
190
|
+
if self._seq_saved_bindings is not None:
|
|
191
|
+
self._bindings = self._seq_saved_bindings
|
|
192
|
+
self._seq_saved_bindings = None
|
|
193
|
+
self._restore_ancestor_bindings()
|
|
194
|
+
self.refresh_bindings()
|
|
195
|
+
|
|
196
|
+
def _hide_ancestor_bindings(self) -> None:
|
|
197
|
+
self._seq_saved_ancestor_bindings = []
|
|
198
|
+
for ancestor in self.ancestors_with_self[1:]:
|
|
199
|
+
if hasattr(ancestor, "_bindings"):
|
|
200
|
+
self._seq_saved_ancestor_bindings.append((ancestor, ancestor._bindings))
|
|
201
|
+
ancestor._bindings = BindingsMap([])
|
|
202
|
+
|
|
203
|
+
def _restore_ancestor_bindings(self) -> None:
|
|
204
|
+
for ancestor, bindings in self._seq_saved_ancestor_bindings:
|
|
205
|
+
ancestor._bindings = bindings
|
|
206
|
+
self._seq_saved_ancestor_bindings = []
|
|
207
|
+
|
|
208
|
+
def set_sequence_keymap(self, keymap: dict[str, str]) -> None:
|
|
209
|
+
self._seq_keymap = keymap
|
|
210
|
+
self._rebuild_sequences()
|
|
211
|
+
self.refresh_bindings()
|
jnav/log_entry_item.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from datetime import UTC, datetime
|
|
2
|
+
|
|
3
|
+
from rich.style import Style
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
|
|
6
|
+
from .field_mapping import FieldMapping, TimestampFormat
|
|
7
|
+
from .parsing import ParsedEntry
|
|
8
|
+
from .search_engine import SearchEngine
|
|
9
|
+
from .tree_rendering import highlight_text
|
|
10
|
+
|
|
11
|
+
LEVEL_COMPONENTS = {
|
|
12
|
+
"error": "summary--level-error",
|
|
13
|
+
"fatal": "summary--level-fatal",
|
|
14
|
+
"critical": "summary--level-critical",
|
|
15
|
+
"warn": "summary--level-warning",
|
|
16
|
+
"warning": "summary--level-warning",
|
|
17
|
+
"info": "summary--level-info",
|
|
18
|
+
"debug": "summary--level-debug",
|
|
19
|
+
"trace": "summary--level-trace",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_EPOCH_DIVISORS: dict[TimestampFormat, int] = {
|
|
23
|
+
"epoch_s": 1,
|
|
24
|
+
"epoch_ms": 1_000,
|
|
25
|
+
"epoch_us": 1_000_000,
|
|
26
|
+
"epoch_ns": 1_000_000_000,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def format_timestamp(value: object, fmt: TimestampFormat) -> str:
|
|
31
|
+
"""Format a timestamp value as HH:MM:SS.mmm. Falls back to the raw string
|
|
32
|
+
representation on any parsing failure."""
|
|
33
|
+
try:
|
|
34
|
+
if fmt == "iso8601":
|
|
35
|
+
dt = datetime.fromisoformat(str(value))
|
|
36
|
+
else:
|
|
37
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
38
|
+
return str(value)
|
|
39
|
+
dt = datetime.fromtimestamp(float(value) / _EPOCH_DIVISORS[fmt], tz=UTC)
|
|
40
|
+
return dt.strftime("%H:%M:%S") + f".{dt.microsecond // 1000:03d}"
|
|
41
|
+
except ValueError, TypeError, OSError, OverflowError:
|
|
42
|
+
return str(value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _get_level_shorthand(value: str) -> str:
|
|
46
|
+
s = value[0]
|
|
47
|
+
remainder = value[1:].lower()
|
|
48
|
+
vowels = set("aeiouy")
|
|
49
|
+
s += "".join(c for c in remainder if c not in vowels)
|
|
50
|
+
s = s.upper()
|
|
51
|
+
if len(s) > 3:
|
|
52
|
+
s = s[:3]
|
|
53
|
+
return s
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def render_summary(
|
|
57
|
+
parsed: ParsedEntry,
|
|
58
|
+
mapping: FieldMapping,
|
|
59
|
+
search: SearchEngine | None = None,
|
|
60
|
+
*,
|
|
61
|
+
text_style: Style | None = None,
|
|
62
|
+
level_styles: dict[str, Style] | None = None,
|
|
63
|
+
highlight_style: Style | None = None,
|
|
64
|
+
) -> Text:
|
|
65
|
+
_ts = text_style or Style()
|
|
66
|
+
_hl = highlight_style or Style()
|
|
67
|
+
parts: list[str | tuple[str, str | Style]] = [" "]
|
|
68
|
+
|
|
69
|
+
if mapping.timestamp is not None:
|
|
70
|
+
ts_val = parsed.expanded.get(mapping.timestamp.path)
|
|
71
|
+
if ts_val not in (None, ""):
|
|
72
|
+
parts.append((format_timestamp(ts_val, mapping.timestamp.format), _ts))
|
|
73
|
+
parts.append(" ")
|
|
74
|
+
|
|
75
|
+
if mapping.level is not None:
|
|
76
|
+
level_val = parsed.expanded.get(mapping.level)
|
|
77
|
+
level_str = str(level_val) if level_val else ""
|
|
78
|
+
if level_str:
|
|
79
|
+
component = LEVEL_COMPONENTS.get(level_str.strip().lower())
|
|
80
|
+
short = _get_level_shorthand(level_str)
|
|
81
|
+
if component and level_styles and component in level_styles:
|
|
82
|
+
parts.append((short, level_styles[component]))
|
|
83
|
+
else:
|
|
84
|
+
parts.append((short, _ts))
|
|
85
|
+
parts.append(" ")
|
|
86
|
+
|
|
87
|
+
if mapping.message is not None:
|
|
88
|
+
msg_val = parsed.expanded.get(mapping.message)
|
|
89
|
+
msg_str = str(msg_val) if msg_val or msg_val == 0 else ""
|
|
90
|
+
parts.append((msg_str, _ts))
|
|
91
|
+
|
|
92
|
+
text = Text.assemble(*parts)
|
|
93
|
+
text.no_wrap = True
|
|
94
|
+
text.overflow = "ellipsis"
|
|
95
|
+
search_term = search.term if search else None
|
|
96
|
+
return highlight_text(text, search_term, _hl)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from rich.console import Group, RenderableType
|
|
4
|
+
from rich.padding import Padding
|
|
5
|
+
from rich.style import Style
|
|
6
|
+
from textual.color import Color
|
|
7
|
+
|
|
8
|
+
from .inline_tree import render_inline_tree
|
|
9
|
+
from .log_entry_item import render_summary
|
|
10
|
+
from .role_mapper import RoleMapper
|
|
11
|
+
from .search_engine import SearchEngine
|
|
12
|
+
from .selector_provider import SelectorProvider
|
|
13
|
+
from .store import IndexedEntry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class EntryStyles:
|
|
18
|
+
text: Style
|
|
19
|
+
levels: dict[str, Style]
|
|
20
|
+
highlight: Style
|
|
21
|
+
cursor_bg: Style
|
|
22
|
+
tree_key: Style
|
|
23
|
+
tree_key_selected: Style
|
|
24
|
+
tree_value: Style
|
|
25
|
+
tree_value_null: Style
|
|
26
|
+
tree_json_string: Style
|
|
27
|
+
tree_search_highlight: Style
|
|
28
|
+
tree_bg: Color
|
|
29
|
+
cursor_color: Color
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LogEntryRenderer:
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
search: SearchEngine,
|
|
37
|
+
role_mapper: RoleMapper,
|
|
38
|
+
selectors: SelectorProvider,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._search = search
|
|
41
|
+
self._role_mapper = role_mapper
|
|
42
|
+
self._selectors = selectors
|
|
43
|
+
|
|
44
|
+
def render(
|
|
45
|
+
self,
|
|
46
|
+
ie: IndexedEntry,
|
|
47
|
+
*,
|
|
48
|
+
styles: EntryStyles,
|
|
49
|
+
is_cursor: bool,
|
|
50
|
+
expanded: bool,
|
|
51
|
+
width: int,
|
|
52
|
+
) -> RenderableType:
|
|
53
|
+
text = render_summary(
|
|
54
|
+
ie.entry,
|
|
55
|
+
self._role_mapper.mapping,
|
|
56
|
+
self._search,
|
|
57
|
+
text_style=styles.text,
|
|
58
|
+
level_styles=styles.levels,
|
|
59
|
+
highlight_style=styles.highlight,
|
|
60
|
+
)
|
|
61
|
+
text.truncate(width - 2, overflow="ellipsis", pad=False)
|
|
62
|
+
|
|
63
|
+
if is_cursor:
|
|
64
|
+
text.stylize(styles.cursor_bg)
|
|
65
|
+
text.pad_right(width - text.cell_len)
|
|
66
|
+
text.style = styles.cursor_bg
|
|
67
|
+
|
|
68
|
+
if not expanded:
|
|
69
|
+
return text
|
|
70
|
+
|
|
71
|
+
tree = render_inline_tree(
|
|
72
|
+
ie.entry,
|
|
73
|
+
custom_fields=self._selectors.active_selectors,
|
|
74
|
+
search=self._search,
|
|
75
|
+
key_style=styles.tree_key,
|
|
76
|
+
selected_style=styles.tree_key_selected,
|
|
77
|
+
value_style=styles.tree_value,
|
|
78
|
+
value_null_style=styles.tree_value_null,
|
|
79
|
+
json_string_style=styles.tree_json_string,
|
|
80
|
+
search_highlight_style=styles.tree_search_highlight,
|
|
81
|
+
)
|
|
82
|
+
if tree is None:
|
|
83
|
+
return text
|
|
84
|
+
|
|
85
|
+
tree_bg = styles.tree_bg
|
|
86
|
+
if is_cursor:
|
|
87
|
+
tree_bg = tree_bg.blend(styles.cursor_color, styles.cursor_color.a * 0.66)
|
|
88
|
+
tree_style = Style(bgcolor=tree_bg.rich_color)
|
|
89
|
+
padded_tree = Padding(tree, (0, 2, 0, 5), style=tree_style, expand=True)
|
|
90
|
+
return Group(text, padded_tree)
|