jnav 1.0.0__tar.gz

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.
Files changed (36) hide show
  1. jnav-1.0.0/PKG-INFO +186 -0
  2. jnav-1.0.0/README.md +159 -0
  3. jnav-1.0.0/pyproject.toml +69 -0
  4. jnav-1.0.0/src/jnav/__init__.py +0 -0
  5. jnav-1.0.0/src/jnav/__main__.py +108 -0
  6. jnav-1.0.0/src/jnav/app.py +398 -0
  7. jnav-1.0.0/src/jnav/buffer.py +48 -0
  8. jnav-1.0.0/src/jnav/detail_tree.py +242 -0
  9. jnav-1.0.0/src/jnav/field_mapping.py +136 -0
  10. jnav-1.0.0/src/jnav/filter_manager_screen.py +100 -0
  11. jnav-1.0.0/src/jnav/filter_provider.py +103 -0
  12. jnav-1.0.0/src/jnav/filter_tree.py +344 -0
  13. jnav-1.0.0/src/jnav/filtering.py +191 -0
  14. jnav-1.0.0/src/jnav/header.py +48 -0
  15. jnav-1.0.0/src/jnav/help_screen.py +79 -0
  16. jnav-1.0.0/src/jnav/inline_tree.py +75 -0
  17. jnav-1.0.0/src/jnav/key_sequences.py +211 -0
  18. jnav-1.0.0/src/jnav/log_entry_item.py +96 -0
  19. jnav-1.0.0/src/jnav/log_entry_renderer.py +90 -0
  20. jnav-1.0.0/src/jnav/log_list_view.py +292 -0
  21. jnav-1.0.0/src/jnav/log_model.py +114 -0
  22. jnav-1.0.0/src/jnav/logging.py +52 -0
  23. jnav-1.0.0/src/jnav/manager_screen_common.py +27 -0
  24. jnav-1.0.0/src/jnav/model.py +13 -0
  25. jnav-1.0.0/src/jnav/offset_group.py +30 -0
  26. jnav-1.0.0/src/jnav/parsing.py +101 -0
  27. jnav-1.0.0/src/jnav/reading.py +52 -0
  28. jnav-1.0.0/src/jnav/role_mapper.py +53 -0
  29. jnav-1.0.0/src/jnav/scrollbar_overlay.py +79 -0
  30. jnav-1.0.0/src/jnav/search_engine.py +87 -0
  31. jnav-1.0.0/src/jnav/selector_manager_screen.py +187 -0
  32. jnav-1.0.0/src/jnav/selector_provider.py +64 -0
  33. jnav-1.0.0/src/jnav/store.py +37 -0
  34. jnav-1.0.0/src/jnav/text_input_screen.py +63 -0
  35. jnav-1.0.0/src/jnav/tree_rendering.py +194 -0
  36. jnav-1.0.0/src/jnav/virtual_list_view.py +383 -0
jnav-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: jnav
3
+ Version: 1.0.0
4
+ Summary: Interactive JSON log viewer with jq filtering for the terminal
5
+ Keywords: json,log,viewer,jq,tui,terminal
6
+ Author: Tibo Clausen
7
+ License-Expression: GPL-3.0-only
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: System :: Logging
15
+ Classifier: Topic :: Utilities
16
+ Requires-Dist: textual>=1.0.0
17
+ Requires-Dist: click>=8.1.0
18
+ Requires-Dist: jq>=1.8.0
19
+ Requires-Dist: python-json-logger>=4.0.0
20
+ Requires-Dist: aioreactive>=0.20.0
21
+ Requires-Dist: orjson>=3.11.8
22
+ Requires-Dist: pydantic>=2.12.5
23
+ Requires-Dist: platformdirs>=4.0.0
24
+ Requires-Python: >=3.14
25
+ Project-URL: Repository, https://github.com/XenotriX/jnav
26
+ Description-Content-Type: text/markdown
27
+
28
+ <p>
29
+ <picture>
30
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/XenotriX/jnav/blob/d28e3701ec1fa795958db0da7293169ff2887e58/docs/logo/jnav_logo_dark.svg">
31
+ <source media="(prefers-color-scheme: light)" srcset="https://github.com/XenotriX/jnav/blob/d28e3701ec1fa795958db0da7293169ff2887e58/docs/logo/jnav_logo_light.svg">
32
+ <img alt="jnav" src="https://github.com/XenotriX/jnav/blob/d28e3701ec1fa795958db0da7293169ff2887e58/docs/logo/jnav_logo_dark.svg">
33
+ </picture>
34
+ </p>
35
+
36
+ Interactive JSON log viewer with [jq](https://jqlang.github.io/jq/) filtering. Navigate, search, filter, and inspect structured logs in the terminal.
37
+
38
+ ![screenshot](https://raw.githubusercontent.com/XenotriX/jnav/360911e954d91e267616508384ca2e7814c22913/screenshot.png)
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ uv tool install jnav
44
+ ```
45
+
46
+ or
47
+
48
+ ```bash
49
+ pipx install jnav
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ```bash
55
+ # Open a log file
56
+ $ jnav app.log
57
+
58
+ # Follow the file for new entries (like tail -f)
59
+ $ jnav --follow app.log
60
+
61
+ # Pipe JSONL from stdin
62
+ $ cat logs.jsonl | jnav
63
+ ```
64
+
65
+ ## Features
66
+
67
+ ### Filtering
68
+
69
+ Filters use jq expressions. Clauses are organized in a tree of AND/OR groups, which gets compiled into a single jq expression used to evaluate each entry.
70
+
71
+ ```python
72
+ .level == "error" # exact match
73
+ .data.user_id | IN("abc123", "def456", "ghi789") # match any of
74
+ .message | contains("timeout") # substring
75
+ .message | test("connection (refused|reset)") # regex
76
+ .status >= 500 # numeric comparison
77
+ .items | any(.name == "widget") # list predicate
78
+ ```
79
+
80
+ https://github.com/user-attachments/assets/4c5fa668-7c37-4b59-97f5-4962aeb12504
81
+
82
+ ### Selected Fields
83
+
84
+ Selectors are jq expressions evaluated per entry, rendered inline next to the summary. They can index arrays, aggregate, or reshape strings.
85
+
86
+ ```python
87
+ .status # plain field
88
+ .items[0].name # array index
89
+ .items | length # computed
90
+ .items | map("\(.name): \(.qty)") # string interpolation over a list
91
+ .build | split("-")[1] | tonumber # extract a number from a string
92
+ .request.headers.user_agent # deep path
93
+ ```
94
+
95
+ Add from the detail panel with `s`, or open the selectors manager with `S` to add, edit, reorder, and toggle them.
96
+
97
+
98
+ https://github.com/user-attachments/assets/a9e93048-c3e8-44e6-9f82-727b8e9f8d10
99
+
100
+ ### JSON String Expansion
101
+
102
+ Values that are JSON-encoded strings (e.g. `"data": "{\"key\": \"value\"}"`) are automatically parsed and displayed as nested objects. The tree view shows `"{}"` in italic to distinguish them from real objects. Filters and selectors work through expanded JSON strings transparently.
103
+
104
+ ### Live Tailing
105
+
106
+ With `--follow`, jnav watches the file for new lines (like `tail -f`). New entries are parsed, filtered, and appended to the view. If you're at the bottom of the list, it auto-scrolls to show new entries.
107
+
108
+ ### Session Persistence
109
+
110
+ Filters, selected fields, scroll position, panel state, and search terms are saved when you quit and restored when reopening the same file. State is stored in `~/.local/share/jnav/`.
111
+
112
+ ## Keybindings
113
+
114
+ ### Navigation
115
+
116
+ | Key | Action |
117
+ | ------------------- | --------------------------- |
118
+ | `j` / `k` | Move up/down |
119
+ | `gg` / `G` | Jump to first/last entry |
120
+ | `Ctrl+D` / `Ctrl+U` | Half-page scroll |
121
+ | `h` / `l` | Focus list / detail panel |
122
+ | `Enter` | Inspect entry (open detail) |
123
+ | `Escape` | Back |
124
+
125
+ ### Search and Filter
126
+
127
+ | Key | Action |
128
+ | --------- | -------------------------------------------------- |
129
+ | `/` | Search (highlights matches, does not hide entries) |
130
+ | `n` / `N` | Next / previous search match |
131
+ | `F` | Filter manager (jq expressions) |
132
+ | `ft` | Quick text filter |
133
+ | `fp` | Pause / resume filters |
134
+ | `Escape` | Clear search |
135
+ | `r` | Reset filters, fields, and search |
136
+
137
+ ### Display
138
+
139
+ | Key | Action |
140
+ | ---- | ---------------------------------------- |
141
+ | `S` | Selected fields manager |
142
+ | `vi` | Toggle inline expanded tree in list rows |
143
+ | `d` | Toggle detail panel |
144
+ | `?` | Help |
145
+ | `q` | Quit |
146
+
147
+ ### Detail Panel
148
+
149
+ | Key | Action |
150
+ | ---- | ---------------------------------------- |
151
+ | `s` | Select (or deselect) the field |
152
+ | `ff` | Filter by value of the highlighted field |
153
+ | `fn` | Filter: entries that have this field |
154
+ | `ve` | View the value in `$EDITOR` |
155
+ | `vo` | Show only selected fields |
156
+
157
+ ### Filter Manager
158
+
159
+ | Key | Action |
160
+ | --------------- | -------------------------- |
161
+ | `a` | Add filter clause |
162
+ | `g` | Add group |
163
+ | `e` | Edit clause |
164
+ | `d` / `y` / `p` | Cut / yank / paste |
165
+ | `t` | Enable / disable |
166
+ | `o` | Toggle AND / OR on a group |
167
+ | `n` | Negate |
168
+ | `c` | Flatten group |
169
+ | `r` | Rename |
170
+ | `Escape` | Close |
171
+
172
+ ### Selected Fields Manager
173
+
174
+ | Key | Action |
175
+ | --------------- | ------------------ |
176
+ | `a` | Add selector |
177
+ | `e` | Edit selector |
178
+ | `d` / `y` / `p` | Cut / yank / paste |
179
+ | `t` | Enable / disable |
180
+ | `Escape` | Close |
181
+
182
+ ## Credits
183
+
184
+ - [lnav](https://github.com/tstack/lnav): the main inspiration: an interactive log viewer with structured querying
185
+ - [hl](https://github.com/pamburus/hl): shaped several design decisions around how JSON log entries should look
186
+ - [Open Color](https://yeun.github.io/open-color/): the palette the theme is built on
jnav-1.0.0/README.md ADDED
@@ -0,0 +1,159 @@
1
+ <p>
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/XenotriX/jnav/blob/d28e3701ec1fa795958db0da7293169ff2887e58/docs/logo/jnav_logo_dark.svg">
4
+ <source media="(prefers-color-scheme: light)" srcset="https://github.com/XenotriX/jnav/blob/d28e3701ec1fa795958db0da7293169ff2887e58/docs/logo/jnav_logo_light.svg">
5
+ <img alt="jnav" src="https://github.com/XenotriX/jnav/blob/d28e3701ec1fa795958db0da7293169ff2887e58/docs/logo/jnav_logo_dark.svg">
6
+ </picture>
7
+ </p>
8
+
9
+ Interactive JSON log viewer with [jq](https://jqlang.github.io/jq/) filtering. Navigate, search, filter, and inspect structured logs in the terminal.
10
+
11
+ ![screenshot](https://raw.githubusercontent.com/XenotriX/jnav/360911e954d91e267616508384ca2e7814c22913/screenshot.png)
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ uv tool install jnav
17
+ ```
18
+
19
+ or
20
+
21
+ ```bash
22
+ pipx install jnav
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ # Open a log file
29
+ $ jnav app.log
30
+
31
+ # Follow the file for new entries (like tail -f)
32
+ $ jnav --follow app.log
33
+
34
+ # Pipe JSONL from stdin
35
+ $ cat logs.jsonl | jnav
36
+ ```
37
+
38
+ ## Features
39
+
40
+ ### Filtering
41
+
42
+ Filters use jq expressions. Clauses are organized in a tree of AND/OR groups, which gets compiled into a single jq expression used to evaluate each entry.
43
+
44
+ ```python
45
+ .level == "error" # exact match
46
+ .data.user_id | IN("abc123", "def456", "ghi789") # match any of
47
+ .message | contains("timeout") # substring
48
+ .message | test("connection (refused|reset)") # regex
49
+ .status >= 500 # numeric comparison
50
+ .items | any(.name == "widget") # list predicate
51
+ ```
52
+
53
+ https://github.com/user-attachments/assets/4c5fa668-7c37-4b59-97f5-4962aeb12504
54
+
55
+ ### Selected Fields
56
+
57
+ Selectors are jq expressions evaluated per entry, rendered inline next to the summary. They can index arrays, aggregate, or reshape strings.
58
+
59
+ ```python
60
+ .status # plain field
61
+ .items[0].name # array index
62
+ .items | length # computed
63
+ .items | map("\(.name): \(.qty)") # string interpolation over a list
64
+ .build | split("-")[1] | tonumber # extract a number from a string
65
+ .request.headers.user_agent # deep path
66
+ ```
67
+
68
+ Add from the detail panel with `s`, or open the selectors manager with `S` to add, edit, reorder, and toggle them.
69
+
70
+
71
+ https://github.com/user-attachments/assets/a9e93048-c3e8-44e6-9f82-727b8e9f8d10
72
+
73
+ ### JSON String Expansion
74
+
75
+ Values that are JSON-encoded strings (e.g. `"data": "{\"key\": \"value\"}"`) are automatically parsed and displayed as nested objects. The tree view shows `"{}"` in italic to distinguish them from real objects. Filters and selectors work through expanded JSON strings transparently.
76
+
77
+ ### Live Tailing
78
+
79
+ With `--follow`, jnav watches the file for new lines (like `tail -f`). New entries are parsed, filtered, and appended to the view. If you're at the bottom of the list, it auto-scrolls to show new entries.
80
+
81
+ ### Session Persistence
82
+
83
+ Filters, selected fields, scroll position, panel state, and search terms are saved when you quit and restored when reopening the same file. State is stored in `~/.local/share/jnav/`.
84
+
85
+ ## Keybindings
86
+
87
+ ### Navigation
88
+
89
+ | Key | Action |
90
+ | ------------------- | --------------------------- |
91
+ | `j` / `k` | Move up/down |
92
+ | `gg` / `G` | Jump to first/last entry |
93
+ | `Ctrl+D` / `Ctrl+U` | Half-page scroll |
94
+ | `h` / `l` | Focus list / detail panel |
95
+ | `Enter` | Inspect entry (open detail) |
96
+ | `Escape` | Back |
97
+
98
+ ### Search and Filter
99
+
100
+ | Key | Action |
101
+ | --------- | -------------------------------------------------- |
102
+ | `/` | Search (highlights matches, does not hide entries) |
103
+ | `n` / `N` | Next / previous search match |
104
+ | `F` | Filter manager (jq expressions) |
105
+ | `ft` | Quick text filter |
106
+ | `fp` | Pause / resume filters |
107
+ | `Escape` | Clear search |
108
+ | `r` | Reset filters, fields, and search |
109
+
110
+ ### Display
111
+
112
+ | Key | Action |
113
+ | ---- | ---------------------------------------- |
114
+ | `S` | Selected fields manager |
115
+ | `vi` | Toggle inline expanded tree in list rows |
116
+ | `d` | Toggle detail panel |
117
+ | `?` | Help |
118
+ | `q` | Quit |
119
+
120
+ ### Detail Panel
121
+
122
+ | Key | Action |
123
+ | ---- | ---------------------------------------- |
124
+ | `s` | Select (or deselect) the field |
125
+ | `ff` | Filter by value of the highlighted field |
126
+ | `fn` | Filter: entries that have this field |
127
+ | `ve` | View the value in `$EDITOR` |
128
+ | `vo` | Show only selected fields |
129
+
130
+ ### Filter Manager
131
+
132
+ | Key | Action |
133
+ | --------------- | -------------------------- |
134
+ | `a` | Add filter clause |
135
+ | `g` | Add group |
136
+ | `e` | Edit clause |
137
+ | `d` / `y` / `p` | Cut / yank / paste |
138
+ | `t` | Enable / disable |
139
+ | `o` | Toggle AND / OR on a group |
140
+ | `n` | Negate |
141
+ | `c` | Flatten group |
142
+ | `r` | Rename |
143
+ | `Escape` | Close |
144
+
145
+ ### Selected Fields Manager
146
+
147
+ | Key | Action |
148
+ | --------------- | ------------------ |
149
+ | `a` | Add selector |
150
+ | `e` | Edit selector |
151
+ | `d` / `y` / `p` | Cut / yank / paste |
152
+ | `t` | Enable / disable |
153
+ | `Escape` | Close |
154
+
155
+ ## Credits
156
+
157
+ - [lnav](https://github.com/tstack/lnav): the main inspiration: an interactive log viewer with structured querying
158
+ - [hl](https://github.com/pamburus/hl): shaped several design decisions around how JSON log entries should look
159
+ - [Open Color](https://yeun.github.io/open-color/): the palette the theme is built on
@@ -0,0 +1,69 @@
1
+ [project]
2
+ name = "jnav"
3
+ version = "1.0.0"
4
+ description = "Interactive JSON log viewer with jq filtering for the terminal"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ license = "GPL-3.0-only"
8
+ authors = [{ name = "Tibo Clausen" }]
9
+ urls = { Repository = "https://github.com/XenotriX/jnav" }
10
+ keywords = ["json", "log", "viewer", "jq", "tui", "terminal"]
11
+ classifiers = [
12
+ "Development Status :: 5 - Production/Stable",
13
+ "Environment :: Console",
14
+ "Intended Audience :: Developers",
15
+ "Intended Audience :: System Administrators",
16
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
17
+ "Programming Language :: Python :: 3.14",
18
+ "Topic :: System :: Logging",
19
+ "Topic :: Utilities",
20
+ ]
21
+
22
+ dependencies = [
23
+ "textual>=1.0.0",
24
+ "click>=8.1.0",
25
+ "jq>=1.8.0",
26
+ "python-json-logger>=4.0.0",
27
+ "aioreactive>=0.20.0",
28
+ "orjson>=3.11.8",
29
+ "pydantic>=2.12.5",
30
+ "platformdirs>=4.0.0",
31
+ ]
32
+
33
+ [project.scripts]
34
+ jnav = "jnav.__main__:main"
35
+
36
+ [build-system]
37
+ requires = ["uv_build>=0.8.0,<0.9"]
38
+ build-backend = "uv_build"
39
+
40
+ [tool.basedpyright]
41
+ reportAny = false
42
+ reportExplicitAny = false
43
+ reportUnannotatedClassAttribute = false
44
+ reportIncompatibleUnannotatedOverride = true
45
+ reportUnusedCallResult = false
46
+
47
+ [tool.pytest.ini_options]
48
+ asyncio_mode = "strict"
49
+ filterwarnings = [
50
+ "ignore::DeprecationWarning:aioreactive",
51
+ "ignore::DeprecationWarning:expression",
52
+ ]
53
+
54
+ [tool.ruff]
55
+ preview = true
56
+
57
+ [tool.coverage.run]
58
+ source = ["src/jnav"]
59
+
60
+ [dependency-groups]
61
+ dev = [
62
+ "basedpyright>=1.39.0",
63
+ "hypothesis>=6.152.1",
64
+ "lefthook>=2.1.4",
65
+ "pytest>=9.0.2",
66
+ "pytest-asyncio>=1.3.0",
67
+ "pytest-cov>=7.1.0",
68
+ "ruff>=0.15.10",
69
+ ]
File without changes
@@ -0,0 +1,108 @@
1
+ import asyncio
2
+ import hashlib
3
+ import logging
4
+ import sys
5
+ from collections.abc import AsyncIterator
6
+ from pathlib import Path
7
+ from typing import cast
8
+
9
+ import aioreactive as rx
10
+ import click
11
+ from platformdirs import user_data_dir
12
+
13
+ from jnav.filter_provider import FilterProvider
14
+ from jnav.log_model import LogModel
15
+ from jnav.role_mapper import RoleMapper
16
+ from jnav.search_engine import SearchEngine
17
+ from jnav.selector_provider import SelectorProvider
18
+ from jnav.store import Store
19
+
20
+ from .app import JnavApp
21
+ from .buffer import buffer_time_or_count
22
+ from .logging import init_logging
23
+ from .parsing import ParsedEntry, parse_entry
24
+ from .reading import read_file, read_pipe, setup_stdin_pipe
25
+
26
+ logging.getLogger("aioreactive").setLevel(logging.WARNING)
27
+
28
+ _STATE_DIR = Path(user_data_dir("jnav"))
29
+
30
+
31
+ def _state_file_for(file_path: str) -> Path:
32
+ key = hashlib.sha256(str(Path(file_path).resolve()).encode()).hexdigest()[:16]
33
+ return _STATE_DIR / f"{key}.json"
34
+
35
+
36
+ def _get_input_iterator(file: str | None) -> AsyncIterator[str] | None:
37
+ if file:
38
+ return read_file(file)
39
+ elif not sys.stdin.isatty():
40
+ pipe = setup_stdin_pipe()
41
+ return read_pipe(pipe)
42
+
43
+ return None
44
+
45
+
46
+ async def _run(file: str | None, follow: bool) -> None:
47
+ lines = _get_input_iterator(file)
48
+
49
+ if lines is None:
50
+ click.echo("Usage: jnav [FILE] or pipe JSONL via stdin", err=True)
51
+ raise SystemExit(1)
52
+
53
+ filter_provider = FilterProvider()
54
+ role_mapper = RoleMapper()
55
+ selectors = SelectorProvider()
56
+ store = Store()
57
+ model = LogModel(
58
+ store=store,
59
+ filter_provider=filter_provider,
60
+ )
61
+
62
+ search = SearchEngine(model)
63
+
64
+ await model.start()
65
+ await search.start()
66
+
67
+ entry_stream = rx.from_async_iterable(
68
+ buffer_time_or_count(
69
+ rx.pipe(
70
+ rx.from_async_iterable(lines),
71
+ rx.map(lambda line: parse_entry(line)),
72
+ rx.filter(lambda result: result is not None),
73
+ rx.map(lambda entry: cast(ParsedEntry, entry)),
74
+ ),
75
+ max_count=10000,
76
+ timeout=0.1,
77
+ )
78
+ )
79
+
80
+ await entry_stream.subscribe_async(store.append_entries)
81
+
82
+ state_file = _state_file_for(file) if file else None
83
+ app = JnavApp(
84
+ model=model,
85
+ filter_provider=filter_provider,
86
+ role_mapper=role_mapper,
87
+ selectors=selectors,
88
+ search=search,
89
+ state_file=state_file,
90
+ follow=follow,
91
+ file_name=file if file else "<stdin>",
92
+ )
93
+ app.title = "jnav"
94
+ await app.run_async()
95
+
96
+
97
+ @click.command()
98
+ @click.argument("file", required=False, type=click.Path(exists=True))
99
+ @click.option("--follow", "-f", is_flag=True, help="Follow the file for new entries")
100
+ @click.version_option()
101
+ def main(file: str | None, follow: bool) -> None:
102
+ """Interactive JSON log viewer with jq filtering."""
103
+ init_logging()
104
+ asyncio.run(_run(file, follow=follow))
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()