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.
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/octotui.svg)](https://pypi.org/project/octotui/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/octotui.svg)](https://pypi.org/project/octotui/)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
38
+ [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
39
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](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>
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/octotui.svg)](https://pypi.org/project/octotui/)
9
+ [![Python](https://img.shields.io/pypi/pyversions/octotui.svg)](https://pypi.org/project/octotui/)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
+ [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
12
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](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"
@@ -0,0 +1,6 @@
1
+ """Entry point for running octotui as a module."""
2
+
3
+ from octotui.main import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
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)