octotui 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.
- octotui-0.1.1/.gitignore +22 -0
- octotui-0.1.1/PKG-INFO +207 -0
- octotui-0.1.1/README.md +180 -0
- octotui-0.1.1/octotui/__init__.py +1 -0
- octotui-0.1.1/octotui/__main__.py +6 -0
- octotui-0.1.1/octotui/custom_figlet_widget.py +0 -0
- octotui-0.1.1/octotui/diff_markdown.py +187 -0
- octotui-0.1.1/octotui/gac_config_modal.py +369 -0
- octotui-0.1.1/octotui/gac_integration.py +183 -0
- octotui-0.1.1/octotui/gac_provider_registry.py +199 -0
- octotui-0.1.1/octotui/git_diff_viewer.py +1346 -0
- octotui-0.1.1/octotui/git_status_sidebar.py +1010 -0
- octotui-0.1.1/octotui/main.py +15 -0
- octotui-0.1.1/octotui/octotui_logo.py +52 -0
- octotui-0.1.1/octotui/style.tcss +431 -0
- octotui-0.1.1/octotui/syntax_utils.py +0 -0
- octotui-0.1.1/pyproject.toml +52 -0
octotui-0.1.1/.gitignore
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
.coverage
|
|
13
|
+
|
|
14
|
+
# Session memory
|
|
15
|
+
.puppy_session_memory.json
|
|
16
|
+
|
|
17
|
+
# Pytest cache
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
|
|
20
|
+
dummy_path
|
|
21
|
+
|
|
22
|
+
.idea/
|
octotui-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: octotui
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A blazing-fast TUI replacement for GitKraken - manage your Git repos with style!
|
|
5
|
+
Project-URL: Repository, https://github.com/never-use-gui/octotui
|
|
6
|
+
Project-URL: Homepage, https://github.com/never-use-gui/octotui
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/never-use-gui/octotui/issues
|
|
8
|
+
Author-email: Michael Pfaffenberger <michael@pfaffenberger.dev>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: git,gitkraken,terminal,textual,tui,version-control
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
20
|
+
Classifier: Topic :: Terminals
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: gac>0.18.0
|
|
23
|
+
Requires-Dist: gitpython>=3.1.42
|
|
24
|
+
Requires-Dist: pyfiglet>=1.0.2
|
|
25
|
+
Requires-Dist: textual>=6.1.0
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
<div align="center">
|
|
29
|
+
|
|
30
|
+
<img src="cute-octo.png" alt="OctoTUI Logo" width="300">
|
|
31
|
+
|
|
32
|
+
# 🐙 OctoTUI
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/octotui/)
|
|
36
|
+
[](https://pypi.org/project/octotui/)
|
|
37
|
+
[](https://opensource.org/licenses/MIT)
|
|
38
|
+
[](https://github.com/astral-sh/ruff)
|
|
39
|
+
[](http://makeapullrequest.com)
|
|
40
|
+
|
|
41
|
+
**A Textual TUI For GitKraken Lovers**
|
|
42
|
+
|
|
43
|
+
[Installation](#-installation) • [AI Commits](#-ai-powered-commits) • [Keybindings](#️-keybindings)
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 🚀 OctoTUI
|
|
50
|
+
|
|
51
|
+
> **We love GitKraken so much, we wanted to bring that beautiful experience to the terminal!**
|
|
52
|
+
|
|
53
|
+
GitKraken is amazing - it's gorgeous, intuitive, and makes Git feel approachable. But as terminal enthusiasts, we found ourselves constantly context-switching between our editor and GitKraken. We wanted that same delightful experience without ever leaving the command line.
|
|
54
|
+
|
|
55
|
+
**OctoTUI is our love letter to both GitKraken and the terminal.**
|
|
56
|
+
|
|
57
|
+
### 💙 What We Kept from GitKraken
|
|
58
|
+
- ✅ Beautiful, intuitive visual diffs
|
|
59
|
+
- ✅ Hunk-level staging control
|
|
60
|
+
- ✅ Branch visualization and management
|
|
61
|
+
- ✅ Commit history browsing
|
|
62
|
+
|
|
63
|
+
### 🎯 What We Added for Terminal Lovers
|
|
64
|
+
- 🤖 AI-powered commit messages (via GAC)
|
|
65
|
+
- 🆓 100% free and open source
|
|
66
|
+
- 🏠 Never leave your terminal flow
|
|
67
|
+
|
|
68
|
+
## 📦 Installation
|
|
69
|
+
|
|
70
|
+
### Quick Start (Recommended)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Using uvx (isolated, recommended)
|
|
74
|
+
uvx octotui
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### From Source (For Contributors)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git clone https://github.com/never-use-gui/octotui.git
|
|
81
|
+
cd octotui
|
|
82
|
+
uv run octotui
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### System Requirements
|
|
86
|
+
|
|
87
|
+
- 🐍 Python 3.11+
|
|
88
|
+
- 🔧 Git
|
|
89
|
+
- 💻 Any terminal with 256+ colors
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
### First-Time Workflow
|
|
93
|
+
|
|
94
|
+
1. **Review Changes**: See your diffs in beautiful color
|
|
95
|
+
2. **Stage Hunks**: Click or use `s` to stage individual changes
|
|
96
|
+
3. **Generate Commit**: Press `g` for AI-powered message (optional)
|
|
97
|
+
4. **Commit**: Press `c` to commit with your message
|
|
98
|
+
5. **Push**: Press `p` to push to remote
|
|
99
|
+
|
|
100
|
+
**Pro tip**: Press `h` anytime to see all available keybindings! 🚀
|
|
101
|
+
|
|
102
|
+
## 🤖 AI-Powered Commits
|
|
103
|
+
|
|
104
|
+
### Setup (Optional but Awesome)
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Install GAC (Git Auto Commit)
|
|
108
|
+
uv pip install 'gac>=0.18.0'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Configuration
|
|
112
|
+
|
|
113
|
+
1. Press `Ctrl+G` in OctoTUI
|
|
114
|
+
2. Choose your provider (we recommend **Cerebras** for free tier)
|
|
115
|
+
3. Select your model
|
|
116
|
+
4. Paste your API key
|
|
117
|
+
5. Save & enjoy AI commit messages!
|
|
118
|
+
|
|
119
|
+
## ⌨️ Keybindings
|
|
120
|
+
|
|
121
|
+
### 📁 Navigation
|
|
122
|
+
| Key | Action |
|
|
123
|
+
|-----|--------|
|
|
124
|
+
| `↑/↓` | Navigate files/hunks |
|
|
125
|
+
| `Enter` | Select file |
|
|
126
|
+
| `Tab` / `Shift+Tab` | Cycle through UI elements |
|
|
127
|
+
| `1` / `Ctrl+1` | Switch to Unstaged tab |
|
|
128
|
+
| `2` / `Ctrl+2` | Switch to Staged tab |
|
|
129
|
+
|
|
130
|
+
### 🔄 Git Operations
|
|
131
|
+
| Key | Action |
|
|
132
|
+
|-----|--------|
|
|
133
|
+
| `s` | Stage selected file |
|
|
134
|
+
| `u` | Unstage selected file |
|
|
135
|
+
| `a` | Stage ALL unstaged changes |
|
|
136
|
+
| `x` | Unstage ALL staged changes |
|
|
137
|
+
| `c` | Commit staged changes |
|
|
138
|
+
|
|
139
|
+
### 🌿 Branch & Remote
|
|
140
|
+
| Key | Action |
|
|
141
|
+
|-----|--------|
|
|
142
|
+
| `b` | Switch branch |
|
|
143
|
+
| `r` | Refresh status |
|
|
144
|
+
| `p` | Push to remote |
|
|
145
|
+
| `o` | Pull from remote |
|
|
146
|
+
|
|
147
|
+
### 🤖 AI Features
|
|
148
|
+
| Key | Action |
|
|
149
|
+
|-----|--------|
|
|
150
|
+
| `g` | Generate AI commit message |
|
|
151
|
+
| `Ctrl+G` | Configure GAC settings |
|
|
152
|
+
|
|
153
|
+
### ⚙️ Application
|
|
154
|
+
| Key | Action |
|
|
155
|
+
|-----|--------|
|
|
156
|
+
| `h` | Show help modal |
|
|
157
|
+
| `q` | Quit application |
|
|
158
|
+
| `Ctrl+D` | Toggle dark mode |
|
|
159
|
+
|
|
160
|
+
## 🎨 Git Status Colors
|
|
161
|
+
|
|
162
|
+
| Color | Meaning |
|
|
163
|
+
|-------|----------|
|
|
164
|
+
| 🟢 **Green** | Staged files (ready to commit) |
|
|
165
|
+
| 🟡 **Yellow** | Modified files (unstaged) |
|
|
166
|
+
| 🔵 **Blue** | Directories |
|
|
167
|
+
| 🟣 **Purple** | Untracked files |
|
|
168
|
+
| 🔴 **Red** | Deleted files |
|
|
169
|
+
|
|
170
|
+
### Code Quality Standards
|
|
171
|
+
|
|
172
|
+
- ✅ Follow the Zen of Python
|
|
173
|
+
- ✅ DRY (Don't Repeat Yourself)
|
|
174
|
+
- ✅ YAGNI (You Aren't Gonna Need It)
|
|
175
|
+
- ✅ SOLID principles
|
|
176
|
+
- ✅ Keep files under 600 lines
|
|
177
|
+
- ✅ Write tests for new features
|
|
178
|
+
- ✅ Pass `ruff check` with zero errors
|
|
179
|
+
|
|
180
|
+
## 📚 Tech Stack
|
|
181
|
+
|
|
182
|
+
- **[Textual](https://textual.textualize.io/)**: Modern TUI framework
|
|
183
|
+
- **[GitPython](https://gitpython.readthedocs.io/)**: Git operations
|
|
184
|
+
- **[GAC](https://github.com/Dicklesworthstone/gac)**: AI commit generation
|
|
185
|
+
- **[Ruff](https://github.com/astral-sh/ruff)**: Lightning-fast Python linter
|
|
186
|
+
|
|
187
|
+
## 📜 License
|
|
188
|
+
|
|
189
|
+
MIT License - see [LICENSE](LICENSE) for details
|
|
190
|
+
|
|
191
|
+
## 🙏 Acknowledgments
|
|
192
|
+
|
|
193
|
+
- Built with ❤️ using [Textual](https://textual.textualize.io/)
|
|
194
|
+
- AI commits powered by [GAC](https://github.com/cellweb/gac)
|
|
195
|
+
|
|
196
|
+
## 💬 Community
|
|
197
|
+
|
|
198
|
+
- 🐛 **Issues**: [GitHub Issues](https://github.com/never-use-gui/octotui/issues)
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
<div align="center">
|
|
202
|
+
|
|
203
|
+
### 🌟 If you like OctoTUI, give us a star! 🌟
|
|
204
|
+
|
|
205
|
+
[⬆ Back to Top](#-octotui)
|
|
206
|
+
|
|
207
|
+
</div>
|
octotui-0.1.1/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="cute-octo.png" alt="OctoTUI Logo" width="300">
|
|
4
|
+
|
|
5
|
+
# 🐙 OctoTUI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
[](https://pypi.org/project/octotui/)
|
|
9
|
+
[](https://pypi.org/project/octotui/)
|
|
10
|
+
[](https://opensource.org/licenses/MIT)
|
|
11
|
+
[](https://github.com/astral-sh/ruff)
|
|
12
|
+
[](http://makeapullrequest.com)
|
|
13
|
+
|
|
14
|
+
**A Textual TUI For GitKraken Lovers**
|
|
15
|
+
|
|
16
|
+
[Installation](#-installation) • [AI Commits](#-ai-powered-commits) • [Keybindings](#️-keybindings)
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 🚀 OctoTUI
|
|
23
|
+
|
|
24
|
+
> **We love GitKraken so much, we wanted to bring that beautiful experience to the terminal!**
|
|
25
|
+
|
|
26
|
+
GitKraken is amazing - it's gorgeous, intuitive, and makes Git feel approachable. But as terminal enthusiasts, we found ourselves constantly context-switching between our editor and GitKraken. We wanted that same delightful experience without ever leaving the command line.
|
|
27
|
+
|
|
28
|
+
**OctoTUI is our love letter to both GitKraken and the terminal.**
|
|
29
|
+
|
|
30
|
+
### 💙 What We Kept from GitKraken
|
|
31
|
+
- ✅ Beautiful, intuitive visual diffs
|
|
32
|
+
- ✅ Hunk-level staging control
|
|
33
|
+
- ✅ Branch visualization and management
|
|
34
|
+
- ✅ Commit history browsing
|
|
35
|
+
|
|
36
|
+
### 🎯 What We Added for Terminal Lovers
|
|
37
|
+
- 🤖 AI-powered commit messages (via GAC)
|
|
38
|
+
- 🆓 100% free and open source
|
|
39
|
+
- 🏠 Never leave your terminal flow
|
|
40
|
+
|
|
41
|
+
## 📦 Installation
|
|
42
|
+
|
|
43
|
+
### Quick Start (Recommended)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Using uvx (isolated, recommended)
|
|
47
|
+
uvx octotui
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### From Source (For Contributors)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/never-use-gui/octotui.git
|
|
54
|
+
cd octotui
|
|
55
|
+
uv run octotui
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### System Requirements
|
|
59
|
+
|
|
60
|
+
- 🐍 Python 3.11+
|
|
61
|
+
- 🔧 Git
|
|
62
|
+
- 💻 Any terminal with 256+ colors
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
### First-Time Workflow
|
|
66
|
+
|
|
67
|
+
1. **Review Changes**: See your diffs in beautiful color
|
|
68
|
+
2. **Stage Hunks**: Click or use `s` to stage individual changes
|
|
69
|
+
3. **Generate Commit**: Press `g` for AI-powered message (optional)
|
|
70
|
+
4. **Commit**: Press `c` to commit with your message
|
|
71
|
+
5. **Push**: Press `p` to push to remote
|
|
72
|
+
|
|
73
|
+
**Pro tip**: Press `h` anytime to see all available keybindings! 🚀
|
|
74
|
+
|
|
75
|
+
## 🤖 AI-Powered Commits
|
|
76
|
+
|
|
77
|
+
### Setup (Optional but Awesome)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Install GAC (Git Auto Commit)
|
|
81
|
+
uv pip install 'gac>=0.18.0'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Configuration
|
|
85
|
+
|
|
86
|
+
1. Press `Ctrl+G` in OctoTUI
|
|
87
|
+
2. Choose your provider (we recommend **Cerebras** for free tier)
|
|
88
|
+
3. Select your model
|
|
89
|
+
4. Paste your API key
|
|
90
|
+
5. Save & enjoy AI commit messages!
|
|
91
|
+
|
|
92
|
+
## ⌨️ Keybindings
|
|
93
|
+
|
|
94
|
+
### 📁 Navigation
|
|
95
|
+
| Key | Action |
|
|
96
|
+
|-----|--------|
|
|
97
|
+
| `↑/↓` | Navigate files/hunks |
|
|
98
|
+
| `Enter` | Select file |
|
|
99
|
+
| `Tab` / `Shift+Tab` | Cycle through UI elements |
|
|
100
|
+
| `1` / `Ctrl+1` | Switch to Unstaged tab |
|
|
101
|
+
| `2` / `Ctrl+2` | Switch to Staged tab |
|
|
102
|
+
|
|
103
|
+
### 🔄 Git Operations
|
|
104
|
+
| Key | Action |
|
|
105
|
+
|-----|--------|
|
|
106
|
+
| `s` | Stage selected file |
|
|
107
|
+
| `u` | Unstage selected file |
|
|
108
|
+
| `a` | Stage ALL unstaged changes |
|
|
109
|
+
| `x` | Unstage ALL staged changes |
|
|
110
|
+
| `c` | Commit staged changes |
|
|
111
|
+
|
|
112
|
+
### 🌿 Branch & Remote
|
|
113
|
+
| Key | Action |
|
|
114
|
+
|-----|--------|
|
|
115
|
+
| `b` | Switch branch |
|
|
116
|
+
| `r` | Refresh status |
|
|
117
|
+
| `p` | Push to remote |
|
|
118
|
+
| `o` | Pull from remote |
|
|
119
|
+
|
|
120
|
+
### 🤖 AI Features
|
|
121
|
+
| Key | Action |
|
|
122
|
+
|-----|--------|
|
|
123
|
+
| `g` | Generate AI commit message |
|
|
124
|
+
| `Ctrl+G` | Configure GAC settings |
|
|
125
|
+
|
|
126
|
+
### ⚙️ Application
|
|
127
|
+
| Key | Action |
|
|
128
|
+
|-----|--------|
|
|
129
|
+
| `h` | Show help modal |
|
|
130
|
+
| `q` | Quit application |
|
|
131
|
+
| `Ctrl+D` | Toggle dark mode |
|
|
132
|
+
|
|
133
|
+
## 🎨 Git Status Colors
|
|
134
|
+
|
|
135
|
+
| Color | Meaning |
|
|
136
|
+
|-------|----------|
|
|
137
|
+
| 🟢 **Green** | Staged files (ready to commit) |
|
|
138
|
+
| 🟡 **Yellow** | Modified files (unstaged) |
|
|
139
|
+
| 🔵 **Blue** | Directories |
|
|
140
|
+
| 🟣 **Purple** | Untracked files |
|
|
141
|
+
| 🔴 **Red** | Deleted files |
|
|
142
|
+
|
|
143
|
+
### Code Quality Standards
|
|
144
|
+
|
|
145
|
+
- ✅ Follow the Zen of Python
|
|
146
|
+
- ✅ DRY (Don't Repeat Yourself)
|
|
147
|
+
- ✅ YAGNI (You Aren't Gonna Need It)
|
|
148
|
+
- ✅ SOLID principles
|
|
149
|
+
- ✅ Keep files under 600 lines
|
|
150
|
+
- ✅ Write tests for new features
|
|
151
|
+
- ✅ Pass `ruff check` with zero errors
|
|
152
|
+
|
|
153
|
+
## 📚 Tech Stack
|
|
154
|
+
|
|
155
|
+
- **[Textual](https://textual.textualize.io/)**: Modern TUI framework
|
|
156
|
+
- **[GitPython](https://gitpython.readthedocs.io/)**: Git operations
|
|
157
|
+
- **[GAC](https://github.com/Dicklesworthstone/gac)**: AI commit generation
|
|
158
|
+
- **[Ruff](https://github.com/astral-sh/ruff)**: Lightning-fast Python linter
|
|
159
|
+
|
|
160
|
+
## 📜 License
|
|
161
|
+
|
|
162
|
+
MIT License - see [LICENSE](LICENSE) for details
|
|
163
|
+
|
|
164
|
+
## 🙏 Acknowledgments
|
|
165
|
+
|
|
166
|
+
- Built with ❤️ using [Textual](https://textual.textualize.io/)
|
|
167
|
+
- AI commits powered by [GAC](https://github.com/cellweb/gac)
|
|
168
|
+
|
|
169
|
+
## 💬 Community
|
|
170
|
+
|
|
171
|
+
- 🐛 **Issues**: [GitHub Issues](https://github.com/never-use-gui/octotui/issues)
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
<div align="center">
|
|
175
|
+
|
|
176
|
+
### 🌟 If you like OctoTUI, give us a star! 🌟
|
|
177
|
+
|
|
178
|
+
[⬆ Back to Top](#-octotui)
|
|
179
|
+
|
|
180
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
File without changes
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable, List, Optional
|
|
7
|
+
|
|
8
|
+
from pygments.lexers import get_lexer_for_filename, guess_lexer
|
|
9
|
+
from pygments.util import ClassNotFound
|
|
10
|
+
from textual.content import Content
|
|
11
|
+
from textual.widgets import Markdown
|
|
12
|
+
from textual.widgets._markdown import MarkdownFence
|
|
13
|
+
|
|
14
|
+
from .git_status_sidebar import Hunk
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class DiffMarkdownConfig:
|
|
19
|
+
"""Configuration for how a diff hunk should be rendered."""
|
|
20
|
+
|
|
21
|
+
repo_root: Path
|
|
22
|
+
wrap: bool = False
|
|
23
|
+
prefer_diff_language: bool = False
|
|
24
|
+
code_block_theme: str = "tokyo-night"
|
|
25
|
+
show_headers: bool = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DiffMarkdownFence(MarkdownFence):
|
|
29
|
+
"""Fenced code block that decorates diff lines with background highlights."""
|
|
30
|
+
|
|
31
|
+
ADDITION_CLASS = ".diff-line--addition"
|
|
32
|
+
REMOVAL_CLASS = ".diff-line--removal"
|
|
33
|
+
ADDITION_STYLE = "on rgba(158, 206, 106, 0.45)"
|
|
34
|
+
REMOVAL_STYLE = "on rgba(140, 74, 126, 0.45)"
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def highlight(cls, code: str, language: str) -> Content:
|
|
38
|
+
"""Apply syntax highlighting and add diff-aware line highlights."""
|
|
39
|
+
content = super().highlight(code, language)
|
|
40
|
+
if not content:
|
|
41
|
+
return content
|
|
42
|
+
|
|
43
|
+
plain = content.plain
|
|
44
|
+
if not plain:
|
|
45
|
+
return content
|
|
46
|
+
|
|
47
|
+
cursor = 0
|
|
48
|
+
for line in plain.splitlines(keepends=True):
|
|
49
|
+
# Retain the newline so the highlight matches the selection effect.
|
|
50
|
+
marker = line[:1]
|
|
51
|
+
if marker == "+" and not line.startswith("+++"):
|
|
52
|
+
start, end = cursor, cursor + len(line)
|
|
53
|
+
content = content.stylize(cls.ADDITION_CLASS, start, end)
|
|
54
|
+
content = content.stylize(cls.ADDITION_STYLE, start, end)
|
|
55
|
+
elif marker == "-" and not line.startswith("---"):
|
|
56
|
+
start, end = cursor, cursor + len(line)
|
|
57
|
+
content = content.stylize(cls.REMOVAL_CLASS, start, end)
|
|
58
|
+
content = content.stylize(cls.REMOVAL_STYLE, start, end)
|
|
59
|
+
cursor += len(line)
|
|
60
|
+
return content
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DiffMarkdown(Markdown):
|
|
64
|
+
"""Markdown widget specialised for unified diff hunks with syntax highlighting.
|
|
65
|
+
|
|
66
|
+
The widget converts hunk headers and line payloads into a fenced Markdown block.
|
|
67
|
+
It attempts to strike a balance between diff semantics (+/- context) and
|
|
68
|
+
per-language syntax highlighting by dynamically picking an appropriate lexer.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
BLOCKS = {
|
|
72
|
+
**Markdown.BLOCKS,
|
|
73
|
+
"fence": DiffMarkdownFence,
|
|
74
|
+
"code_block": DiffMarkdownFence,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
DEFAULT_CSS = """
|
|
78
|
+
DiffMarkdown {
|
|
79
|
+
background: transparent;
|
|
80
|
+
border: none;
|
|
81
|
+
&:dark .diff-line--addition {
|
|
82
|
+
background: rgb(158, 206, 106);
|
|
83
|
+
}
|
|
84
|
+
&:light .diff-line--addition {
|
|
85
|
+
background: rgb(200, 230, 180);
|
|
86
|
+
}
|
|
87
|
+
&:dark .diff-line--removal {
|
|
88
|
+
background: rgb(140, 74, 126);
|
|
89
|
+
}
|
|
90
|
+
&:light .diff-line--removal {
|
|
91
|
+
background: rgb(200, 140, 180);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
file_path: str,
|
|
99
|
+
hunks: Iterable[Hunk],
|
|
100
|
+
*,
|
|
101
|
+
config: Optional[DiffMarkdownConfig] = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
self._file_path = file_path
|
|
104
|
+
self._config = config or DiffMarkdownConfig(repo_root=Path.cwd())
|
|
105
|
+
self._hunks_cache = list(hunks)
|
|
106
|
+
markdown_text = self._build_markdown(self._hunks_cache)
|
|
107
|
+
super().__init__(markdown_text)
|
|
108
|
+
if hasattr(self, "inline_code_theme"):
|
|
109
|
+
self.inline_code_theme = self._config.code_block_theme
|
|
110
|
+
|
|
111
|
+
def _build_markdown(self, hunks: List[Hunk]) -> str:
|
|
112
|
+
"""Construct the Markdown payload that encodes diff and syntax info."""
|
|
113
|
+
header_lines: List[str] = ["<!-- Octotui DiffMarkdown -->"]
|
|
114
|
+
|
|
115
|
+
if not hunks:
|
|
116
|
+
return "\n".join(header_lines + ["_No changes to display._"])
|
|
117
|
+
|
|
118
|
+
language = self._detect_language()
|
|
119
|
+
fence_language = (
|
|
120
|
+
"diff" if self._config.prefer_diff_language else language or "diff"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
for hunk in hunks:
|
|
124
|
+
if self._config.show_headers:
|
|
125
|
+
header_lines.append(f"### `{hunk.header or 'File contents'}`")
|
|
126
|
+
header_lines.append(self._render_hunk_block(hunk, fence_language))
|
|
127
|
+
|
|
128
|
+
return "\n\n".join(header_lines)
|
|
129
|
+
|
|
130
|
+
def _render_hunk_block(self, hunk: Hunk, fence_language: str) -> str:
|
|
131
|
+
"""Render a single hunk of diff lines inside a fenced Markdown block."""
|
|
132
|
+
fence_lines: List[str] = [f"```{fence_language}"]
|
|
133
|
+
for line in hunk.lines:
|
|
134
|
+
fence_lines.append(self._normalise_line(line))
|
|
135
|
+
fence_lines.append("```")
|
|
136
|
+
return "\n".join(fence_lines)
|
|
137
|
+
|
|
138
|
+
def _normalise_line(self, line: str) -> str:
|
|
139
|
+
"""Ensure markdown is well-formed while preserving diff semantics."""
|
|
140
|
+
if not line:
|
|
141
|
+
return ""
|
|
142
|
+
|
|
143
|
+
escaped = line.replace("```", "`\u200b``")
|
|
144
|
+
return escaped
|
|
145
|
+
|
|
146
|
+
def _detect_language(self) -> Optional[str]:
|
|
147
|
+
"""Best-effort inference of the target language for syntax highlighting."""
|
|
148
|
+
file_path = self._file_path
|
|
149
|
+
full_path = self._config.repo_root / file_path
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
lexer = _get_cached_lexer(str(full_path))
|
|
153
|
+
return lexer.aliases[0] if lexer.aliases else lexer.name.lower()
|
|
154
|
+
except ClassNotFound:
|
|
155
|
+
pass
|
|
156
|
+
except FileNotFoundError:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
sample = self._collect_sample()
|
|
160
|
+
if not sample:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
lexer = guess_lexer(sample)
|
|
165
|
+
return lexer.aliases[0] if lexer.aliases else lexer.name.lower()
|
|
166
|
+
except ClassNotFound:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
def _collect_sample(self) -> str:
|
|
170
|
+
"""Gather a short sample of code from the hunks to feed into pygments."""
|
|
171
|
+
snippets: List[str] = []
|
|
172
|
+
for hunk in self._hunks_cache:
|
|
173
|
+
for line in hunk.lines:
|
|
174
|
+
if line and line[:1] in {"+", "-", " "}:
|
|
175
|
+
snippets.append(line[1:])
|
|
176
|
+
else:
|
|
177
|
+
snippets.append(line)
|
|
178
|
+
if len("\n".join(snippets)) > 2048:
|
|
179
|
+
break
|
|
180
|
+
if len("\n".join(snippets)) > 2048:
|
|
181
|
+
break
|
|
182
|
+
return "\n".join(snippets)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@lru_cache(maxsize=128)
|
|
186
|
+
def _get_cached_lexer(file_path: str):
|
|
187
|
+
return get_lexer_for_filename(file_path)
|