gitux 0.1.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.
@@ -0,0 +1,212 @@
1
+ """Repo stats bar — one-line repository statistics below the top app bar."""
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+ from rich.cells import cell_len
7
+ from rich.text import Text
8
+ from textual.widgets import Static
9
+
10
+ from gitux.domain import FileCounts, HeadSummary
11
+
12
+ _MAX_USER_CHARS: int = 16
13
+ _MAX_REPO_CHARS: int = 32
14
+ _MAX_REMOTE_CHARS: int = 32
15
+ _MAX_HEAD_SUBJECT_CHARS: int = 40
16
+ _DEFAULT_RENDER_WIDTH: int = 200 # unmounted fallback so bare _render_text() is full
17
+ _PADDING_COLS: int = 2 # matches `padding: 0 1` on #stats-bar
18
+
19
+ # drop priority, LOWEST first (head is never dropped)
20
+ _SEGMENT_PRIORITY: tuple[str, ...] = ("ahead_behind", "remote", "repo", "user", "files", "date", "head")
21
+
22
+ _PREFIX_CELLS: int = 13 # " ■ REPO STATS"
23
+ _SEPARATOR_CELLS: int = 3 # " │ "
24
+
25
+
26
+ def _format_relative_time(epoch: int, *, now: float | None = None) -> str:
27
+ """Format an epoch seconds as a relative English string (floor division)."""
28
+ now = time.time() if now is None else now
29
+ delta = max(0, int(now - epoch))
30
+ if delta < 60:
31
+ return "just now"
32
+ if delta < 90:
33
+ return "1 minute ago"
34
+ if delta < 2700:
35
+ return f"{delta // 60} minutes ago"
36
+ if delta < 5400:
37
+ return "1 hour ago"
38
+ if delta < 86400:
39
+ return f"{delta // 3600} hours ago"
40
+ if delta < 129600:
41
+ return "1 day ago"
42
+ if delta < 604800:
43
+ return f"{delta // 86400} days ago"
44
+ if delta < 2592000:
45
+ return f"{delta // 604800} weeks ago"
46
+ if delta < 31536000:
47
+ return f"{delta // 2592000} months ago"
48
+ return f"{delta // 31536000} years ago"
49
+
50
+
51
+ class RepoStatsBar(Static):
52
+ """Single-line stats bar fed by ``GituxApp._refresh_all``.
53
+
54
+ Renders: `` ■ REPO STATS │ user │ hash subject │ date │ +s ~m ?u [!c] │ ↑a ↓b │ remote``.
55
+ Uses hybrid B3 truncation: hard caps + width-adaptive segment drop.
56
+ """
57
+
58
+ def __init__(self, **kwargs) -> None:
59
+ super().__init__("", **kwargs)
60
+ self._user: str = ""
61
+ self._head_summary: HeadSummary | None = None
62
+ self._file_counts: FileCounts | None = None
63
+ self._ahead = 0
64
+ self._behind = 0
65
+ self._remote = "(local)"
66
+ self._remote_branch = ""
67
+ self._repo_name: str = ""
68
+
69
+ def update_stats(
70
+ self,
71
+ user: str = "",
72
+ head_summary: HeadSummary | None = None,
73
+ file_counts: FileCounts | None = None,
74
+ ahead: int = 0,
75
+ behind: int = 0,
76
+ remote: str = "(local)",
77
+ remote_branch: str = "",
78
+ repo_name: str = "",
79
+ ) -> None:
80
+ """Render the stats bar with the given repo state (safe defaults included)."""
81
+ self._remote = remote or "(local)"
82
+ self._remote_branch = remote_branch if remote else ""
83
+ self._user = user
84
+ self._head_summary = head_summary
85
+ self._file_counts = file_counts
86
+ self._ahead = ahead
87
+ self._behind = behind
88
+ self._repo_name = repo_name
89
+ self.update(self._render_text())
90
+
91
+ def _build_segments(self) -> list[dict[str, Any]]:
92
+ """Build segments in locked visual order with text, style, and drop info."""
93
+ segments: list[dict[str, Any]] = []
94
+
95
+ if self._repo_name:
96
+ segments.append({
97
+ "name": "repo",
98
+ "plain": self._repo_name[:_MAX_REPO_CHARS],
99
+ "style": "bold #e4e1ed",
100
+ })
101
+
102
+ segments.append({
103
+ "name": "user",
104
+ "plain": (self._user or "unknown")[:_MAX_USER_CHARS],
105
+ "style": "#e4e1ed",
106
+ })
107
+
108
+ summary = self._head_summary
109
+ if summary is None:
110
+ segments.append({
111
+ "name": "head",
112
+ "plain": "(no commits)",
113
+ "hash": "",
114
+ "subject": "(no commits)",
115
+ "style": "#e4e1ed",
116
+ })
117
+ else:
118
+ subject = summary.subject
119
+ if len(subject) > _MAX_HEAD_SUBJECT_CHARS:
120
+ subject = subject[: _MAX_HEAD_SUBJECT_CHARS - 1] + "\u2026"
121
+ segments.append({
122
+ "name": "head",
123
+ "plain": subject,
124
+ "hash": "",
125
+ "subject": subject,
126
+ "style": "#e4e1ed",
127
+ })
128
+
129
+ if summary is not None:
130
+ segments.append({
131
+ "name": "date",
132
+ "plain": _format_relative_time(summary.epoch),
133
+ "style": "#c7c4d7",
134
+ })
135
+
136
+ counts = self._file_counts or FileCounts(0, 0, 0, 0)
137
+ files = f"+{counts.staged} ~{counts.modified} ?{counts.untracked}"
138
+ if counts.conflicts > 0:
139
+ files += f" !{counts.conflicts}"
140
+ segments.append({
141
+ "name": "files",
142
+ "plain": files,
143
+ "style": "#c7c4d7",
144
+ })
145
+
146
+ segments.append({
147
+ "name": "ahead_behind",
148
+ "plain": f"\u2191{self._ahead} \u2193{self._behind}",
149
+ "style": "bold #c0c1ff",
150
+ })
151
+
152
+ remote_slug = (
153
+ f"{self._remote}/{self._remote_branch}" if self._remote_branch else self._remote
154
+ )
155
+ segments.append({
156
+ "name": "remote",
157
+ "plain": remote_slug[:_MAX_REMOTE_CHARS],
158
+ "style": "#e4e1ed",
159
+ })
160
+
161
+ return segments
162
+
163
+ def _total_cells(self, segments: list[dict[str, Any]]) -> int:
164
+ """Total rendered cell width of all segments plus prefix and separators."""
165
+ return (
166
+ _PREFIX_CELLS
167
+ + sum(cell_len(seg["plain"]) for seg in segments)
168
+ + _SEPARATOR_CELLS * (len(segments) - 1)
169
+ )
170
+
171
+ def _fit_segments(self, segments: list[dict[str, Any]], width: int) -> list[dict[str, Any]]:
172
+ """Drop lowest-priority segments, then elide the head subject to fit width."""
173
+ total = self._total_cells(segments)
174
+ while total > width and len(segments) > 1:
175
+ droppable = [seg for seg in segments if seg["name"] != "head"]
176
+ if not droppable:
177
+ break
178
+ to_drop = min(droppable, key=lambda seg: _SEGMENT_PRIORITY.index(seg["name"]))
179
+ segments.remove(to_drop)
180
+ total = self._total_cells(segments)
181
+
182
+ if len(segments) == 1 and total > width:
183
+ head = segments[0]
184
+ hash_len = cell_len(head["hash"])
185
+ avail = width - _PREFIX_CELLS - hash_len - 1
186
+ if avail < 1:
187
+ if head["hash"]:
188
+ head["plain"] = head["hash"]
189
+ else:
190
+ head["plain"] = head["plain"][: max(0, width - _PREFIX_CELLS)]
191
+ else:
192
+ elided = head["subject"][: avail - 1] + "\u2026"
193
+ head["plain"] = (
194
+ f"{head['hash']} {elided}" if head["hash"] else elided
195
+ )
196
+ return segments
197
+
198
+ def _render_text(self, width: int | None = None) -> Text:
199
+ """Render the stats line, dropping segments to fit the given width."""
200
+ if width is None:
201
+ width = self.size.width - _PADDING_COLS if self.is_mounted else 0
202
+ if width <= 0:
203
+ width = _DEFAULT_RENDER_WIDTH
204
+ segments = self._build_segments()
205
+ segments = self._fit_segments(segments, width)
206
+
207
+ text = Text(no_wrap=True)
208
+ text.append(" \u25a0 REPO STATS", style="bold #c0c1ff")
209
+ for seg in segments:
210
+ text.append(" \u2502 ", style="#c7c4d7")
211
+ text.append(seg["plain"], style=seg["style"])
212
+ return text
@@ -0,0 +1 @@
1
+ """Utility functions, config, and exceptions."""
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.5
2
+ Name: gitux
3
+ Version: 0.1.0
4
+ Summary: A beautiful, minimalist TUI for Git.
5
+ Project-URL: Homepage, https://github.com/Hector-2710/gitux
6
+ Project-URL: Repository, https://github.com/Hector-2710/gitux
7
+ Project-URL: Issues, https://github.com/Hector-2710/gitux/issues
8
+ Project-URL: Changelog, https://github.com/Hector-2710/gitux/releases
9
+ Author: Hector-2710
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: developer-tools,git,terminal,textual,tui,version-control
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Version Control :: Git
22
+ Classifier: Topic :: Terminals
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: psutil>=7.2.2
25
+ Requires-Dist: textual>=8.2.8
26
+ Requires-Dist: typer>=0.27.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
29
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # GITUX
33
+
34
+ [![PyPI version](https://img.shields.io/pypi/v/gitux.svg)](https://pypi.org/project/gitux/)
35
+ [![Python versions](https://img.shields.io/pypi/pyversions/gitux.svg)](https://pypi.org/project/gitux/)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Hector-2710/gitux/blob/main/LICENSE)
37
+
38
+ A beautiful, minimalist TUI for Git.
39
+
40
+ GITUX is a terminal user interface for Git that prioritizes design and user experience. It wraps Git commands in a clean, visually appealing interface — so you can work with Git without leaving the terminal, and enjoy doing it.
41
+
42
+ ## Why GITUX?
43
+
44
+ Most Git TUIs are functional but not beautiful. GITUX is built with the belief that developer tools should be both powerful and pleasant to look at.
45
+
46
+ - **Minimalist** — No clutter, no overwhelming menus. Just what you need.
47
+ - **Beautiful** — Clean colors, thoughtful spacing, modern aesthetics.
48
+ - **Fast** — Direct subprocess calls to Git, no heavy abstractions.
49
+
50
+ ## Features
51
+
52
+ - View repository status with color-coded file states
53
+ - Browse commit history with details
54
+ - Stage and commit files with messages
55
+ - View diffs between files
56
+ - Manage branches (create, switch, delete)
57
+ - Merge branches
58
+ - Push to remote
59
+ - Performance metrics (startup time, memory usage)
60
+
61
+ ## Installation
62
+
63
+ **Recommended — with [pipx](https://pipx.pypa.io/) or [uv](https://docs.astral.sh/uv/) (isolated install):**
64
+
65
+ ```bash
66
+ # With pipx
67
+ pipx install gitux
68
+
69
+ # Or with uv
70
+ uv tool install gitux
71
+ ```
72
+
73
+ **Or with pip:**
74
+
75
+ ```bash
76
+ pip install gitux
77
+ ```
78
+
79
+ > **Requirements:** Python 3.12+ and `git` installed on your system.
80
+
81
+ <details>
82
+ <summary>Install from source (for development)</summary>
83
+
84
+ ```bash
85
+ git clone https://github.com/Hector-2710/gitux.git
86
+ cd gitux
87
+ uv sync --all-extras
88
+ uv run gitux
89
+ ```
90
+ </details>
91
+
92
+ ## Usage
93
+
94
+ Inside any Git repository, just run:
95
+
96
+ ```bash
97
+ gitux
98
+
99
+ # Show version
100
+ gitux --version
101
+ ```
102
+
103
+ ### Keybindings
104
+
105
+ | Key | Action |
106
+ |-----|--------|
107
+ | `Tab` | Next section (Files / Commits / Diff) |
108
+ | `↑` / `↓` or `j` / `k` | Scroll active section |
109
+ | `s` / `Enter` | Stage / unstage file |
110
+ | `a` / `A` | Stage all / unstage all files |
111
+ | `c` | Focus commit message |
112
+ | `Ctrl+Enter` | Create commit |
113
+ | `Ctrl+p` | Push to remote |
114
+ | `r` | Refresh status |
115
+ | `b` | Branches panel |
116
+ | `?` | Show help |
117
+ | `q` | Quit GITUX |
118
+
119
+ ## Stack
120
+
121
+ | Component | Technology | Purpose |
122
+ |-----------|------------|---------|
123
+ | Language | Python 3.12+ | Core runtime |
124
+ | TUI | Textual | Terminal interface with CSS styling |
125
+ | CLI | Typer | Command-line entry point |
126
+ | Git | subprocess + git CLI | Git operations |
127
+ | Metrics | psutil | Performance measurement |
128
+
129
+ ## Development
130
+
131
+ ```bash
132
+ # Install dev dependencies
133
+ uv sync --all-extras
134
+
135
+ # Run tests
136
+ uv run pytest
137
+
138
+ # Run linter
139
+ uv run ruff check .
140
+
141
+ # Format code
142
+ uv run ruff format .
143
+ ```
144
+
145
+ ## Project Structure
146
+
147
+ ```
148
+ gitux/
149
+ ├── src/
150
+ │ └── gitux/
151
+ │ ├── __init__.py
152
+ │ ├── __main__.py
153
+ │ ├── cli.py # Typer entry point
154
+ │ ├── domain/ # Data models
155
+ │ ├── git/ # Git infrastructure
156
+ │ ├── presenter/ # Business logic
157
+ │ ├── ui/ # Textual TUI
158
+ │ ├── metrics/ # Performance metrics
159
+ │ └── utils/ # Config, exceptions
160
+ ├── tests/
161
+ │ ├── unit/
162
+ │ ├── integration/
163
+ │ └── e2e/
164
+ ├── docs/
165
+ │ ├── context.md
166
+ │ ├── container.md
167
+ │ └── diagrams/
168
+ ├── pyproject.toml
169
+ └── README.md
170
+ ```
171
+
172
+ ## License
173
+
174
+ MIT
@@ -0,0 +1,32 @@
1
+ gitux/__init__.py,sha256=ZtE4fY4VU33_y0OW_7530O4vAqxBRLt45K8zWxX8IbI,74
2
+ gitux/__main__.py,sha256=75MHbiT5XYsOtCTWw1ju6L8Xz2zPxVJiMt27KL9baKI,117
3
+ gitux/cli.py,sha256=1pLTZl5BKZS8Uy4s9V7_krorUwk9kkr_rVwi3rIJhsU,644
4
+ gitux/domain/__init__.py,sha256=bGqvXT1enNUc4x55VP0c_mfOGZyhm52oe5uTAnfz_XU,747
5
+ gitux/domain/models.py,sha256=ZkIhxXE57CEtOhcPFzsrR-rvHC1iof6swvc045-7bTA,4786
6
+ gitux/git/__init__.py,sha256=33wGQqeWR0gvFkEOyBwcDSMCiNTglqrCVs2tUCMgTos,1151
7
+ gitux/git/commands.py,sha256=OllqC0fm7Jd_xyW85C-qeLaW8IBSi29d92YwuWgvM7Y,12114
8
+ gitux/git/exceptions.py,sha256=N7VAPdnLp-SX3QEaTZx0RhCLhzgdZsB-CWU9SkAKZGQ,230
9
+ gitux/git/parser.py,sha256=AAOKhBfhXMkUVDPVFezRwr1ls5tuA_SsiFlGAxfxTtA,2444
10
+ gitux/presenter/__init__.py,sha256=El3zvXJUr-YdwttEJVGUsS2U3aL-q-KVmKrBfhCOba8,223
11
+ gitux/presenter/commit_presenter.py,sha256=_848SmJpI9VbIhmUaSNR9UyZUnma0cYrOK0k5Rk6g4k,3719
12
+ gitux/presenter/repo_presenter.py,sha256=rMMNQfOEiObDhxUHGkXa6jJBAP3iivX6gnuzi4DSEjM,3889
13
+ gitux/ui/__init__.py,sha256=80z8YKUTs8g7W6DueSYCDUFQsyonCJfwTFoxyzpQAKc,30
14
+ gitux/ui/app.py,sha256=7rPaI3Pm1bIiR387W18daMNUyTFpSo85p4S8yl6fxgE,16680
15
+ gitux/ui/gitux.tcss,sha256=v30PtisCxD3-EijS7bon7pw_97w11oz-BKoee0QsGE8,4941
16
+ gitux/ui/widgets/__init__.py,sha256=vfXQoRgr5dDXUXBwh2BzUmHifDOlDV6mKOWKwCKdV0c,827
17
+ gitux/ui/widgets/block_container.py,sha256=ligGTYr3V9vupZQs191muwh6SGWbE8Z9Xm3b5aeinpo,2210
18
+ gitux/ui/widgets/branch_screen.py,sha256=YhwnJt1kbGbHo0UbT6WoSiVvJ5EdvmtvflMc62trjvo,2069
19
+ gitux/ui/widgets/changed_files.py,sha256=ADTC0H9Rmf0ONOYCZvQyS6DOhcBwGGMqJXWzLuw5AU4,6833
20
+ gitux/ui/widgets/commit_log.py,sha256=FvKhFFoje_eC18yYo0EN2GOXd25ybkkTMYUoiMtrn7o,6704
21
+ gitux/ui/widgets/commit_screen.py,sha256=sLbnQlfEwRisK7E1u__6NDVjXS1pwQIrGNnsDQKUVGU,4120
22
+ gitux/ui/widgets/diff_viewer.py,sha256=WxR9PQNgghHcTxXvwLToLhZrlvf9IJ-GEBiJhP7UoTg,4559
23
+ gitux/ui/widgets/gitux_footer.py,sha256=XsoAOj9Lc17AGQ5j9InfiwWi39Lg690YH051LvcwY-Q,1513
24
+ gitux/ui/widgets/header.py,sha256=ftwQCexmlp2aT-MS2vFPthVQc62dVwZehMA4B_A_CDI,2736
25
+ gitux/ui/widgets/help_screen.py,sha256=fnuyMqKOAVED93LRi_BSn15OZg58pfwX99svRzPMdn8,2699
26
+ gitux/ui/widgets/repo_stats_bar.py,sha256=SlPzgHqZho75HZDGNXOi1V6FpsW1oDwBI1Bi2uC3vCg,7464
27
+ gitux/utils/__init__.py,sha256=PLILUN4DpXkq9aaU8KyZoL5z_UAZw8qGYMMcHXf8qUs,49
28
+ gitux-0.1.0.dist-info/METADATA,sha256=fT13Xgn-qvwmCbrqfkv-Nthn36ieXCHubKcnnSXpwQ4,4834
29
+ gitux-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
30
+ gitux-0.1.0.dist-info/entry_points.txt,sha256=pGB4oUGZz7AieLAwGrMUtFAWXm2gag2IcCi_wUho19k,41
31
+ gitux-0.1.0.dist-info/licenses/LICENSE,sha256=pj_7arY6WRVNI7xLBUOxewGZGxorLPu6GpF-kLO0SSM,1068
32
+ gitux-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gitux = gitux.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hector-2710
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.