clack-tui 0.1.1__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.
- clack_tui-0.1.1/.github/workflows/publish.yml +37 -0
- clack_tui-0.1.1/.gitignore +9 -0
- clack_tui-0.1.1/LICENSE +21 -0
- clack_tui-0.1.1/PKG-INFO +132 -0
- clack_tui-0.1.1/README.md +106 -0
- clack_tui-0.1.1/docs/releasing.md +71 -0
- clack_tui-0.1.1/pyproject.toml +59 -0
- clack_tui-0.1.1/src/clack/__init__.py +1 -0
- clack_tui-0.1.1/src/clack/__main__.py +12 -0
- clack_tui-0.1.1/src/clack/app.py +142 -0
- clack_tui-0.1.1/src/clack/css/app.tcss +104 -0
- clack_tui-0.1.1/src/clack/db.py +652 -0
- clack_tui-0.1.1/src/clack/html_export.py +192 -0
- clack_tui-0.1.1/src/clack/models.py +59 -0
- clack_tui-0.1.1/src/clack/tmux.py +283 -0
- clack_tui-0.1.1/src/clack/widgets/__init__.py +0 -0
- clack_tui-0.1.1/src/clack/widgets/dashboard.py +295 -0
- clack_tui-0.1.1/src/clack/widgets/dialog_viewer.py +223 -0
- clack_tui-0.1.1/src/clack/widgets/query_console.py +85 -0
- clack_tui-0.1.1/src/clack/widgets/stats.py +105 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
with:
|
|
18
|
+
enable-cache: true
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
run: uv python install 3.11
|
|
22
|
+
|
|
23
|
+
- name: Build package
|
|
24
|
+
run: uv build
|
|
25
|
+
|
|
26
|
+
- name: Validate package metadata
|
|
27
|
+
run: uvx twine check dist/*
|
|
28
|
+
|
|
29
|
+
- name: Smoke test installed CLI
|
|
30
|
+
run: |
|
|
31
|
+
uv venv .smoke-test
|
|
32
|
+
. .smoke-test/bin/activate
|
|
33
|
+
uv pip install dist/*.whl
|
|
34
|
+
python -c "import shutil; import clack; import clack.__main__; assert shutil.which('clack'); assert shutil.which('clack-tui')"
|
|
35
|
+
|
|
36
|
+
- name: Publish to PyPI
|
|
37
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
clack_tui-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Janine
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
clack_tui-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clack-tui
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: TUI for browsing, searching, and resuming Claude Code sessions
|
|
5
|
+
Project-URL: Homepage, https://github.com/jcc-ne/clack
|
|
6
|
+
Project-URL: Repository, https://github.com/jcc-ne/clack
|
|
7
|
+
Project-URL: Issues, https://github.com/jcc-ne/clack/issues
|
|
8
|
+
Author: Janine
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,claude,claude-code,terminal,textual,tui
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Terminals
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: duckdb>=1.1.0
|
|
22
|
+
Requires-Dist: jinja2>=3.1.0
|
|
23
|
+
Requires-Dist: libtmux>=0.40.0
|
|
24
|
+
Requires-Dist: textual>=1.0.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# clack
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/clack-tui/)
|
|
30
|
+
[](https://pypi.org/project/clack-tui/)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
|
|
33
|
+
A terminal UI for browsing, searching, and resuming [Claude Code](https://claude.ai/code) sessions.
|
|
34
|
+
|
|
35
|
+
Browse your full session history, read past conversations, jump into stats, and resume any session — all without leaving the terminal.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# pipx
|
|
43
|
+
pipx install clack-tui
|
|
44
|
+
|
|
45
|
+
# uvx (run without installing)
|
|
46
|
+
uvx clack-tui
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The package name is `clack-tui` because `clack` is already taken on PyPI. The package installs both `clack` and `clack-tui` executables.
|
|
50
|
+
|
|
51
|
+
Requires Python 3.11+ and [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) installed.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
clack
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
clack reads your Claude Code session files directly from `~/.claude/projects/` — no configuration needed.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
|
|
67
|
+
| Tab | Key | What it does |
|
|
68
|
+
|-----|-----|--------------|
|
|
69
|
+
| Dashboard | `1` | Browse all sessions, search with full-text search (DuckDB FTS / BM25) |
|
|
70
|
+
| Stats | `2` | Token usage and model breakdown, daily sparklines |
|
|
71
|
+
| Dialog | `3` | Read any conversation turn-by-turn, export to HTML |
|
|
72
|
+
| Query | `4` | Write SQL directly against your session data (DuckDB) |
|
|
73
|
+
|
|
74
|
+
### Dashboard key bindings
|
|
75
|
+
|
|
76
|
+
| Key | Action |
|
|
77
|
+
|-----|--------|
|
|
78
|
+
| `/` | Focus search |
|
|
79
|
+
| `Esc` | Clear search |
|
|
80
|
+
| `Enter` | Resume session (opens `claude --resume`) |
|
|
81
|
+
| `v` | View full conversation |
|
|
82
|
+
| `r` | Refresh session list |
|
|
83
|
+
| `q` | Quit |
|
|
84
|
+
|
|
85
|
+
**tmux:** If clack is running inside a tmux session, resuming opens the session in a new tmux window. Otherwise it suspends the TUI, runs `claude --resume`, and returns when you exit.
|
|
86
|
+
|
|
87
|
+
If the DuckDB FTS extension is unavailable, dashboard search falls back to simple substring matching.
|
|
88
|
+
|
|
89
|
+
### Query console
|
|
90
|
+
|
|
91
|
+
The Query console exposes your session data as DuckDB SQL views:
|
|
92
|
+
|
|
93
|
+
| View | Contents |
|
|
94
|
+
|------|----------|
|
|
95
|
+
| `v_sessions` | One row per session — date, project, summary, model, turn count |
|
|
96
|
+
| `v_assistant_turns` | Individual assistant turns with token counts |
|
|
97
|
+
| `v_stats` | Aggregated usage by model |
|
|
98
|
+
| `v_sessions_by_day` | Daily session and token totals |
|
|
99
|
+
| `raw_records` | Raw JSONL records |
|
|
100
|
+
|
|
101
|
+
Example queries:
|
|
102
|
+
|
|
103
|
+
```sql
|
|
104
|
+
-- Sessions from the last week
|
|
105
|
+
SELECT title, cwd, turn_count FROM v_sessions
|
|
106
|
+
WHERE last_active > now() - INTERVAL '7 days';
|
|
107
|
+
|
|
108
|
+
-- Most token-heavy sessions
|
|
109
|
+
SELECT sessionId, SUM(output_tokens) AS total
|
|
110
|
+
FROM v_assistant_turns GROUP BY 1 ORDER BY 2 DESC LIMIT 10;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Dev setup
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
git clone https://github.com/jcc-ne/clack
|
|
119
|
+
cd clack
|
|
120
|
+
uv sync
|
|
121
|
+
uv run clack
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Release notes for TestPyPI and Trusted Publishing live in [docs/releasing.md](docs/releasing.md).
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Requirements
|
|
129
|
+
|
|
130
|
+
- Python 3.11+
|
|
131
|
+
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) (session files at `~/.claude/projects/`)
|
|
132
|
+
- tmux (optional — enables opening resumed sessions in a new window)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# clack
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/clack-tui/)
|
|
4
|
+
[](https://pypi.org/project/clack-tui/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
A terminal UI for browsing, searching, and resuming [Claude Code](https://claude.ai/code) sessions.
|
|
8
|
+
|
|
9
|
+
Browse your full session history, read past conversations, jump into stats, and resume any session — all without leaving the terminal.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# pipx
|
|
17
|
+
pipx install clack-tui
|
|
18
|
+
|
|
19
|
+
# uvx (run without installing)
|
|
20
|
+
uvx clack-tui
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The package name is `clack-tui` because `clack` is already taken on PyPI. The package installs both `clack` and `clack-tui` executables.
|
|
24
|
+
|
|
25
|
+
Requires Python 3.11+ and [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) installed.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
clack
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
clack reads your Claude Code session files directly from `~/.claude/projects/` — no configuration needed.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
| Tab | Key | What it does |
|
|
42
|
+
|-----|-----|--------------|
|
|
43
|
+
| Dashboard | `1` | Browse all sessions, search with full-text search (DuckDB FTS / BM25) |
|
|
44
|
+
| Stats | `2` | Token usage and model breakdown, daily sparklines |
|
|
45
|
+
| Dialog | `3` | Read any conversation turn-by-turn, export to HTML |
|
|
46
|
+
| Query | `4` | Write SQL directly against your session data (DuckDB) |
|
|
47
|
+
|
|
48
|
+
### Dashboard key bindings
|
|
49
|
+
|
|
50
|
+
| Key | Action |
|
|
51
|
+
|-----|--------|
|
|
52
|
+
| `/` | Focus search |
|
|
53
|
+
| `Esc` | Clear search |
|
|
54
|
+
| `Enter` | Resume session (opens `claude --resume`) |
|
|
55
|
+
| `v` | View full conversation |
|
|
56
|
+
| `r` | Refresh session list |
|
|
57
|
+
| `q` | Quit |
|
|
58
|
+
|
|
59
|
+
**tmux:** If clack is running inside a tmux session, resuming opens the session in a new tmux window. Otherwise it suspends the TUI, runs `claude --resume`, and returns when you exit.
|
|
60
|
+
|
|
61
|
+
If the DuckDB FTS extension is unavailable, dashboard search falls back to simple substring matching.
|
|
62
|
+
|
|
63
|
+
### Query console
|
|
64
|
+
|
|
65
|
+
The Query console exposes your session data as DuckDB SQL views:
|
|
66
|
+
|
|
67
|
+
| View | Contents |
|
|
68
|
+
|------|----------|
|
|
69
|
+
| `v_sessions` | One row per session — date, project, summary, model, turn count |
|
|
70
|
+
| `v_assistant_turns` | Individual assistant turns with token counts |
|
|
71
|
+
| `v_stats` | Aggregated usage by model |
|
|
72
|
+
| `v_sessions_by_day` | Daily session and token totals |
|
|
73
|
+
| `raw_records` | Raw JSONL records |
|
|
74
|
+
|
|
75
|
+
Example queries:
|
|
76
|
+
|
|
77
|
+
```sql
|
|
78
|
+
-- Sessions from the last week
|
|
79
|
+
SELECT title, cwd, turn_count FROM v_sessions
|
|
80
|
+
WHERE last_active > now() - INTERVAL '7 days';
|
|
81
|
+
|
|
82
|
+
-- Most token-heavy sessions
|
|
83
|
+
SELECT sessionId, SUM(output_tokens) AS total
|
|
84
|
+
FROM v_assistant_turns GROUP BY 1 ORDER BY 2 DESC LIMIT 10;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Dev setup
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/jcc-ne/clack
|
|
93
|
+
cd clack
|
|
94
|
+
uv sync
|
|
95
|
+
uv run clack
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Release notes for TestPyPI and Trusted Publishing live in [docs/releasing.md](docs/releasing.md).
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Requirements
|
|
103
|
+
|
|
104
|
+
- Python 3.11+
|
|
105
|
+
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) (session files at `~/.claude/projects/`)
|
|
106
|
+
- tmux (optional — enables opening resumed sessions in a new window)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Releasing clack-tui
|
|
2
|
+
|
|
3
|
+
## Manual dry run with TestPyPI
|
|
4
|
+
|
|
5
|
+
Create a TestPyPI account, enable 2FA, and create a TestPyPI API token.
|
|
6
|
+
|
|
7
|
+
Store the token in `~/.pypirc`:
|
|
8
|
+
|
|
9
|
+
```ini
|
|
10
|
+
[distutils]
|
|
11
|
+
index-servers =
|
|
12
|
+
pypi
|
|
13
|
+
testpypi
|
|
14
|
+
|
|
15
|
+
[pypi]
|
|
16
|
+
username = __token__
|
|
17
|
+
password = pypi-REPLACE_ME
|
|
18
|
+
|
|
19
|
+
[testpypi]
|
|
20
|
+
repository = https://test.pypi.org/legacy/
|
|
21
|
+
username = __token__
|
|
22
|
+
password = pypi-REPLACE_ME
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Lock the file down:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
chmod 600 ~/.pypirc
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Build and upload:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv build
|
|
35
|
+
uvx twine check dist/*
|
|
36
|
+
uvx twine upload -r testpypi dist/*
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Smoke-test install from TestPyPI:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python3 -m venv /tmp/clack-test
|
|
43
|
+
source /tmp/clack-test/bin/activate
|
|
44
|
+
python -m pip install --upgrade pip
|
|
45
|
+
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple clack-tui
|
|
46
|
+
clack
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If `0.1.0` has already been uploaded to TestPyPI, bump `version` in `pyproject.toml` before uploading again.
|
|
50
|
+
|
|
51
|
+
## Trusted Publishing to PyPI
|
|
52
|
+
|
|
53
|
+
The repo includes a GitHub Actions workflow at `.github/workflows/publish.yml` that publishes on GitHub Release publication.
|
|
54
|
+
|
|
55
|
+
Configure PyPI Trusted Publishing for the project:
|
|
56
|
+
|
|
57
|
+
1. Create the `clack-tui` project on PyPI if it does not exist yet, either by an initial manual upload or by creating the project through PyPI's publisher flow.
|
|
58
|
+
2. In PyPI, open the project, then go to `Manage` -> `Publishing`.
|
|
59
|
+
3. Add a GitHub publisher with:
|
|
60
|
+
- Owner: `jcc-ne`
|
|
61
|
+
- Repository: `clack`
|
|
62
|
+
- Workflow filename: `publish.yml`
|
|
63
|
+
- Environment name: `pypi`
|
|
64
|
+
4. In GitHub, create an environment named `pypi` for the repository. Add approval rules if you want a manual gate.
|
|
65
|
+
5. Publish a GitHub Release. The workflow will build with `uv build` and publish to PyPI via OIDC.
|
|
66
|
+
|
|
67
|
+
Notes:
|
|
68
|
+
|
|
69
|
+
- Trusted Publishing does not require a PyPI API token in GitHub secrets.
|
|
70
|
+
- The workflow requests `id-token: write`, which PyPI uses to mint a short-lived upload token.
|
|
71
|
+
- You can keep using the manual TestPyPI flow locally even after enabling Trusted Publishing for production releases.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "clack-tui"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "TUI for browsing, searching, and resuming Claude Code sessions"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [{ name = "Janine" }]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
keywords = ["claude", "tui", "terminal", "ai", "textual", "claude-code"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Topic :: Terminals",
|
|
18
|
+
"Topic :: Utilities",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"textual>=1.0.0",
|
|
22
|
+
"duckdb>=1.1.0",
|
|
23
|
+
"libtmux>=0.40.0",
|
|
24
|
+
"jinja2>=3.1.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/jcc-ne/clack"
|
|
29
|
+
Repository = "https://github.com/jcc-ne/clack"
|
|
30
|
+
Issues = "https://github.com/jcc-ne/clack/issues"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
clack = "clack.__main__:main"
|
|
34
|
+
clack-tui = "clack.__main__:main"
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/clack"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.sdist]
|
|
44
|
+
exclude = [
|
|
45
|
+
"/.claude",
|
|
46
|
+
"/.envrc",
|
|
47
|
+
"/uv.lock",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
target-version = "py311"
|
|
52
|
+
src = ["src"]
|
|
53
|
+
line-length = 100
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
57
|
+
|
|
58
|
+
[tool.ty.environment]
|
|
59
|
+
root = "src"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""clack — TUI for Claude Code session management."""
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Main Textual application for clack."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import duckdb
|
|
6
|
+
from textual import work
|
|
7
|
+
from textual.actions import SkipAction
|
|
8
|
+
from textual.app import App, ComposeResult
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual.events import Key
|
|
11
|
+
from textual.widgets import Footer, Header, Input, LoadingIndicator, TabbedContent, TabPane, Tree
|
|
12
|
+
from textual.worker import Worker, WorkerState
|
|
13
|
+
|
|
14
|
+
from clack.widgets.dashboard import DashboardTab
|
|
15
|
+
from clack.widgets.dialog_viewer import DialogViewer
|
|
16
|
+
from clack.widgets.query_console import QueryConsole
|
|
17
|
+
from clack.widgets.stats import StatsTab
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ClackApp(App):
|
|
21
|
+
CSS_PATH = "css/app.tcss"
|
|
22
|
+
TITLE = "clack"
|
|
23
|
+
BINDINGS = [
|
|
24
|
+
Binding("1", "show_tab('dashboard')", "Dashboard", show=True),
|
|
25
|
+
Binding("2", "show_tab('stats')", "Stats", show=True),
|
|
26
|
+
Binding("3", "show_tab('dialog')", "Dialog", show=True),
|
|
27
|
+
Binding("4", "show_tab('query')", "Query", show=True),
|
|
28
|
+
Binding("q", "quit", "Quit", show=True),
|
|
29
|
+
Binding("t", "switch_theme", "Theme", show=True),
|
|
30
|
+
Binding("G", "nav_end", show=False),
|
|
31
|
+
Binding("ctrl+f", "nav_page_down", show=False),
|
|
32
|
+
Binding("ctrl+b", "nav_page_up", show=False),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
THEMES = ("solarized-dark", "solarized-light")
|
|
36
|
+
|
|
37
|
+
db: duckdb.DuckDBPyConnection | None = None
|
|
38
|
+
_dashboard: DashboardTab | None = None
|
|
39
|
+
_g_pending: bool = False
|
|
40
|
+
|
|
41
|
+
def compose(self) -> ComposeResult:
|
|
42
|
+
yield Header()
|
|
43
|
+
yield LoadingIndicator(id="loading-indicator")
|
|
44
|
+
with TabbedContent(id="tabs"):
|
|
45
|
+
with TabPane("Dashboard", id="dashboard"):
|
|
46
|
+
yield DashboardTab()
|
|
47
|
+
with TabPane("Stats", id="stats"):
|
|
48
|
+
yield StatsTab()
|
|
49
|
+
with TabPane("Dialog", id="dialog"):
|
|
50
|
+
yield DialogViewer()
|
|
51
|
+
with TabPane("Query", id="query"):
|
|
52
|
+
yield QueryConsole()
|
|
53
|
+
yield Footer()
|
|
54
|
+
|
|
55
|
+
def on_mount(self) -> None:
|
|
56
|
+
self.theme = "solarized-dark"
|
|
57
|
+
self.query_one("#tabs").display = False
|
|
58
|
+
self._load_data()
|
|
59
|
+
self.set_interval(60, self._auto_refresh_dashboard)
|
|
60
|
+
|
|
61
|
+
@work(thread=True, group="db_init")
|
|
62
|
+
def _load_data(self) -> duckdb.DuckDBPyConnection:
|
|
63
|
+
from clack.db import get_connection
|
|
64
|
+
|
|
65
|
+
return get_connection()
|
|
66
|
+
|
|
67
|
+
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
|
|
68
|
+
if event.worker.group == "db_init" and event.state == WorkerState.SUCCESS:
|
|
69
|
+
self.db = event.worker.result
|
|
70
|
+
self.query_one("#loading-indicator").display = False
|
|
71
|
+
self.query_one("#tabs").display = True
|
|
72
|
+
assert self.db is not None
|
|
73
|
+
self._dashboard = self.query_one(DashboardTab)
|
|
74
|
+
self._dashboard.load_data(self.db)
|
|
75
|
+
self.query_one(StatsTab).load_data(self.db)
|
|
76
|
+
self.query_one(QueryConsole).set_db(self.db)
|
|
77
|
+
|
|
78
|
+
def _auto_refresh_dashboard(self) -> None:
|
|
79
|
+
"""Periodic refresh for the dashboard tab."""
|
|
80
|
+
if self.db is not None and self._dashboard is not None:
|
|
81
|
+
try:
|
|
82
|
+
self._dashboard._refresh_data()
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def on_key(self, event: Key) -> None:
|
|
87
|
+
# Skip vim nav when an Input widget has focus
|
|
88
|
+
if isinstance(self.focused, Input):
|
|
89
|
+
self._g_pending = False
|
|
90
|
+
return
|
|
91
|
+
if event.key == "g":
|
|
92
|
+
if self._g_pending:
|
|
93
|
+
# gg -> go to top
|
|
94
|
+
self._g_pending = False
|
|
95
|
+
event.prevent_default()
|
|
96
|
+
self.action_nav_home()
|
|
97
|
+
else:
|
|
98
|
+
self._g_pending = True
|
|
99
|
+
event.prevent_default()
|
|
100
|
+
return
|
|
101
|
+
self._g_pending = False
|
|
102
|
+
|
|
103
|
+
def _nav_action(self, *actions: str) -> None:
|
|
104
|
+
"""Try navigation actions on the focused widget, using the first one found."""
|
|
105
|
+
widget = self.focused
|
|
106
|
+
if widget is None:
|
|
107
|
+
return
|
|
108
|
+
for action in actions:
|
|
109
|
+
method = getattr(widget, f"action_{action}", None)
|
|
110
|
+
if method is not None:
|
|
111
|
+
try:
|
|
112
|
+
method()
|
|
113
|
+
except SkipAction:
|
|
114
|
+
continue
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
def action_nav_home(self) -> None:
|
|
118
|
+
self._nav_action("scroll_top", "scroll_home")
|
|
119
|
+
|
|
120
|
+
def action_nav_end(self) -> None:
|
|
121
|
+
self._nav_action("scroll_bottom", "scroll_end")
|
|
122
|
+
|
|
123
|
+
def action_nav_page_down(self) -> None:
|
|
124
|
+
self._nav_action("page_down")
|
|
125
|
+
|
|
126
|
+
def action_nav_page_up(self) -> None:
|
|
127
|
+
self._nav_action("page_up")
|
|
128
|
+
|
|
129
|
+
def action_switch_theme(self) -> None:
|
|
130
|
+
current = self.THEMES.index(self.theme) if self.theme in self.THEMES else -1
|
|
131
|
+
self.theme = self.THEMES[(current + 1) % len(self.THEMES)]
|
|
132
|
+
|
|
133
|
+
def action_show_tab(self, tab_id: str) -> None:
|
|
134
|
+
self.query_one(TabbedContent).active = tab_id
|
|
135
|
+
|
|
136
|
+
def show_dialog(self, session_id: str, title: str) -> None:
|
|
137
|
+
"""Switch to dialog tab and load a session."""
|
|
138
|
+
self.query_one(TabbedContent).active = "dialog"
|
|
139
|
+
assert self.db is not None
|
|
140
|
+
viewer = self.query_one(DialogViewer)
|
|
141
|
+
viewer.load_session(self.db, session_id, title)
|
|
142
|
+
viewer.query_one("#dialog-tree", Tree).focus()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Screen {
|
|
2
|
+
background: $surface;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
#loading-indicator {
|
|
6
|
+
width: 100%;
|
|
7
|
+
height: 100%;
|
|
8
|
+
content-align: center middle;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* Dashboard */
|
|
12
|
+
DashboardTab {
|
|
13
|
+
height: 1fr;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
DashboardTab DataTable {
|
|
17
|
+
height: 1fr;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
DashboardTab #detail-bar {
|
|
21
|
+
height: 3;
|
|
22
|
+
padding: 0 1;
|
|
23
|
+
background: $surface-darken-1;
|
|
24
|
+
color: $text-muted;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
DashboardTab #search-input {
|
|
28
|
+
dock: top;
|
|
29
|
+
width: 100%;
|
|
30
|
+
margin: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Stats */
|
|
34
|
+
StatsTab {
|
|
35
|
+
height: 1fr;
|
|
36
|
+
layout: grid;
|
|
37
|
+
grid-size: 2;
|
|
38
|
+
grid-gutter: 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
StatsTab #model-table {
|
|
42
|
+
height: 1fr;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
StatsTab #stats-summary {
|
|
46
|
+
height: 1fr;
|
|
47
|
+
padding: 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Dialog Viewer */
|
|
51
|
+
DialogViewer {
|
|
52
|
+
height: 1fr;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
DialogViewer #dialog-header {
|
|
56
|
+
height: 2;
|
|
57
|
+
padding: 0 1;
|
|
58
|
+
background: $primary-darken-2;
|
|
59
|
+
color: $text;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
DialogViewer #dialog-search {
|
|
63
|
+
dock: top;
|
|
64
|
+
width: 100%;
|
|
65
|
+
margin: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
DialogViewer #dialog-tree {
|
|
69
|
+
height: 1fr;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
DialogViewer #dialog-footer {
|
|
73
|
+
height: 1;
|
|
74
|
+
padding: 0 1;
|
|
75
|
+
background: $surface-darken-1;
|
|
76
|
+
color: $text-muted;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Query Console */
|
|
80
|
+
QueryConsole {
|
|
81
|
+
height: 1fr;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
QueryConsole #query-results {
|
|
85
|
+
height: 1fr;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
QueryConsole #query-input {
|
|
89
|
+
height: 5;
|
|
90
|
+
border: tall $primary;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
QueryConsole #query-status {
|
|
94
|
+
height: 1;
|
|
95
|
+
padding: 0 1;
|
|
96
|
+
background: $surface-darken-1;
|
|
97
|
+
color: $text-muted;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
QueryConsole #views-help {
|
|
101
|
+
height: 1;
|
|
102
|
+
padding: 0 1;
|
|
103
|
+
color: $text-muted;
|
|
104
|
+
}
|