dnfseek 0.1.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.
- dnfseek-0.1.0/.github/workflows/publish.yml +19 -0
- dnfseek-0.1.0/.gitignore +121 -0
- dnfseek-0.1.0/PKG-INFO +92 -0
- dnfseek-0.1.0/README.md +84 -0
- dnfseek-0.1.0/main.py +648 -0
- dnfseek-0.1.0/main.tcss +78 -0
- dnfseek-0.1.0/pyproject.toml +22 -0
- dnfseek-0.1.0/uv.lock +130 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v9.0.0
|
|
16
|
+
- name: Build package
|
|
17
|
+
run: uv build
|
|
18
|
+
- name: Publish to PyPI
|
|
19
|
+
run: uv publish
|
dnfseek-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
.hypothesis/
|
|
50
|
+
.pytest_cache/
|
|
51
|
+
|
|
52
|
+
# Translations
|
|
53
|
+
*.mo
|
|
54
|
+
*.pot
|
|
55
|
+
|
|
56
|
+
# Django stuff:
|
|
57
|
+
*.log
|
|
58
|
+
local_settings.py
|
|
59
|
+
db.sqlite3
|
|
60
|
+
|
|
61
|
+
# Flask stuff:
|
|
62
|
+
instance/
|
|
63
|
+
.webassets-cache
|
|
64
|
+
|
|
65
|
+
# Scrapy stuff:
|
|
66
|
+
.scrapy
|
|
67
|
+
|
|
68
|
+
# Sphinx documentation
|
|
69
|
+
docs/_build/
|
|
70
|
+
|
|
71
|
+
# PyBuilder
|
|
72
|
+
target/
|
|
73
|
+
|
|
74
|
+
# Jupyter Notebook
|
|
75
|
+
.ipynb_checkpoints
|
|
76
|
+
|
|
77
|
+
# IPython
|
|
78
|
+
profile_default/
|
|
79
|
+
ipython_config.py
|
|
80
|
+
|
|
81
|
+
# pyenv
|
|
82
|
+
.python-version
|
|
83
|
+
|
|
84
|
+
# celery beat schedule file
|
|
85
|
+
celerybeat-schedule
|
|
86
|
+
|
|
87
|
+
# SageMath parsed files
|
|
88
|
+
*.sage.py
|
|
89
|
+
|
|
90
|
+
# Environments
|
|
91
|
+
.env
|
|
92
|
+
.venv
|
|
93
|
+
env/
|
|
94
|
+
venv/
|
|
95
|
+
ENV/
|
|
96
|
+
env.bak/
|
|
97
|
+
venv.bak/
|
|
98
|
+
|
|
99
|
+
# Spyder project settings
|
|
100
|
+
.spyderproject
|
|
101
|
+
.spyproject
|
|
102
|
+
|
|
103
|
+
# Rope project settings
|
|
104
|
+
.ropeproject
|
|
105
|
+
|
|
106
|
+
# mkdocs documentation
|
|
107
|
+
/site
|
|
108
|
+
|
|
109
|
+
# mypy
|
|
110
|
+
.mypy_cache/
|
|
111
|
+
.dmypy.json
|
|
112
|
+
dmypy.json
|
|
113
|
+
|
|
114
|
+
# Pyre type checker
|
|
115
|
+
.pyre/
|
|
116
|
+
|
|
117
|
+
# VSCODE IDE
|
|
118
|
+
.vscode
|
|
119
|
+
|
|
120
|
+
#Pycharm IDE
|
|
121
|
+
.idea
|
dnfseek-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dnfseek
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A TUI package browser for Fedora, built with Textual
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: textual>=8.2.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# dnfseek
|
|
10
|
+
|
|
11
|
+
A TUI package browser for Fedora, built with [Textual](https://github.com/Textualize/textual).
|
|
12
|
+
A Python port of my forked bash + fzf [`dnfseek`](https://github.com/Sam-Horry/dnfseek) script.
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
- Fedora (or another dnf-based distro)
|
|
19
|
+
- [`uv`](https://docs.astral.sh/uv/) — `curl -LsSf https://astral.sh/uv/install.sh | sh` (recommended)
|
|
20
|
+
|
|
21
|
+
## Test it out
|
|
22
|
+
|
|
23
|
+
You can test out dnfseek without having to install it, using uvx:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uvx --from git+https://github.com/Sam-Horry/dnfseek-tui dnfseek
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
### From git (recommended)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv tool install git+https://github.com/Sam-Horry/dnfseek-tui
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### From PyPI
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv tool install dnfseek
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Either way you get a `dnfseek` command on your PATH. On first run the app
|
|
44
|
+
asks for your sudo password (via `sudo -v`) before the interface starts.
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
dnfseek
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Key | Action |
|
|
53
|
+
| ----- | -------- |
|
|
54
|
+
| `tab` | Switch between search and package list |
|
|
55
|
+
| `u` | Upgrade all packages |
|
|
56
|
+
| `r` | Refresh package cache |
|
|
57
|
+
| `i` / `x` / `e` / `g` | Install / remove / reinstall / update selected package |
|
|
58
|
+
| `d` | Show dependencies |
|
|
59
|
+
| `space` | Show package info |
|
|
60
|
+
|
|
61
|
+
Package lists are cached in `~/.cache/dnfseek` and refreshed if older than
|
|
62
|
+
24 hours.
|
|
63
|
+
|
|
64
|
+
Using the built-in command palette (ctrl+p), you can change dnfseek's behaviour by searching all
|
|
65
|
+
available packages, or only search packages currently installed on your system.
|
|
66
|
+
All the above actions can be performed from the command palette, as well as:
|
|
67
|
+
|
|
68
|
+
| Command | Action |
|
|
69
|
+
| --------- | -------- |
|
|
70
|
+
| Search all | Search all available packages |
|
|
71
|
+
| Search installed | Search installed packages |
|
|
72
|
+
| Theme | Change the app theme |
|
|
73
|
+
| Keys | Shows a help widget with a summary of available keys |
|
|
74
|
+
|
|
75
|
+
## Updating
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
uv tool upgrade dnfseek
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Notes
|
|
82
|
+
|
|
83
|
+
- This tool is basically a wrapper for dnf, with only a few commands at the moment.
|
|
84
|
+
- This tool is provided as-is, use at your own risk. I am not liable if something goes
|
|
85
|
+
wrong - though in practice, dnf makes it pretty hard to brick your system.
|
|
86
|
+
Most mistakes are recoverable with a rollback or `dnf history undo`
|
|
87
|
+
- Installed as a tool, the binary lives in `~/.local/bin/dnfseek`, which
|
|
88
|
+
takes precedence over a `dnfseek` script elsewhere on your PATH.
|
|
89
|
+
- Sudo authentication happens once, before the TUI starts. If your sudo
|
|
90
|
+
session expires mid-session, restart the app to re-authenticate.
|
|
91
|
+
- Requires Python 3.12+; `uv` downloads a compatible interpreter if your
|
|
92
|
+
system Python is older.
|
dnfseek-0.1.0/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# dnfseek
|
|
2
|
+
|
|
3
|
+
A TUI package browser for Fedora, built with [Textual](https://github.com/Textualize/textual).
|
|
4
|
+
A Python port of my forked bash + fzf [`dnfseek`](https://github.com/Sam-Horry/dnfseek) script.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Prerequisites
|
|
9
|
+
|
|
10
|
+
- Fedora (or another dnf-based distro)
|
|
11
|
+
- [`uv`](https://docs.astral.sh/uv/) — `curl -LsSf https://astral.sh/uv/install.sh | sh` (recommended)
|
|
12
|
+
|
|
13
|
+
## Test it out
|
|
14
|
+
|
|
15
|
+
You can test out dnfseek without having to install it, using uvx:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uvx --from git+https://github.com/Sam-Horry/dnfseek-tui dnfseek
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### From git (recommended)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv tool install git+https://github.com/Sam-Horry/dnfseek-tui
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### From PyPI
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv tool install dnfseek
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Either way you get a `dnfseek` command on your PATH. On first run the app
|
|
36
|
+
asks for your sudo password (via `sudo -v`) before the interface starts.
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
dnfseek
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| Key | Action |
|
|
45
|
+
| ----- | -------- |
|
|
46
|
+
| `tab` | Switch between search and package list |
|
|
47
|
+
| `u` | Upgrade all packages |
|
|
48
|
+
| `r` | Refresh package cache |
|
|
49
|
+
| `i` / `x` / `e` / `g` | Install / remove / reinstall / update selected package |
|
|
50
|
+
| `d` | Show dependencies |
|
|
51
|
+
| `space` | Show package info |
|
|
52
|
+
|
|
53
|
+
Package lists are cached in `~/.cache/dnfseek` and refreshed if older than
|
|
54
|
+
24 hours.
|
|
55
|
+
|
|
56
|
+
Using the built-in command palette (ctrl+p), you can change dnfseek's behaviour by searching all
|
|
57
|
+
available packages, or only search packages currently installed on your system.
|
|
58
|
+
All the above actions can be performed from the command palette, as well as:
|
|
59
|
+
|
|
60
|
+
| Command | Action |
|
|
61
|
+
| --------- | -------- |
|
|
62
|
+
| Search all | Search all available packages |
|
|
63
|
+
| Search installed | Search installed packages |
|
|
64
|
+
| Theme | Change the app theme |
|
|
65
|
+
| Keys | Shows a help widget with a summary of available keys |
|
|
66
|
+
|
|
67
|
+
## Updating
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv tool upgrade dnfseek
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Notes
|
|
74
|
+
|
|
75
|
+
- This tool is basically a wrapper for dnf, with only a few commands at the moment.
|
|
76
|
+
- This tool is provided as-is, use at your own risk. I am not liable if something goes
|
|
77
|
+
wrong - though in practice, dnf makes it pretty hard to brick your system.
|
|
78
|
+
Most mistakes are recoverable with a rollback or `dnf history undo`
|
|
79
|
+
- Installed as a tool, the binary lives in `~/.local/bin/dnfseek`, which
|
|
80
|
+
takes precedence over a `dnfseek` script elsewhere on your PATH.
|
|
81
|
+
- Sudo authentication happens once, before the TUI starts. If your sudo
|
|
82
|
+
session expires mid-session, restart the app to re-authenticate.
|
|
83
|
+
- Requires Python 3.12+; `uv` downloads a compatible interpreter if your
|
|
84
|
+
system Python is older.
|
dnfseek-0.1.0/main.py
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
|
|
9
|
+
from textual.app import App, ComposeResult, SystemCommand
|
|
10
|
+
from textual.binding import Binding
|
|
11
|
+
from textual.containers import Horizontal, Vertical
|
|
12
|
+
from textual.screen import Screen
|
|
13
|
+
from textual.theme import Theme
|
|
14
|
+
from textual.widgets import (
|
|
15
|
+
Input,
|
|
16
|
+
Static,
|
|
17
|
+
Header,
|
|
18
|
+
Footer,
|
|
19
|
+
OptionList,
|
|
20
|
+
LoadingIndicator,
|
|
21
|
+
)
|
|
22
|
+
from textual.widgets._option_list import Option
|
|
23
|
+
|
|
24
|
+
CACHE_DIR = Path.home() / ".cache" / "dnfseek"
|
|
25
|
+
INSTALLED_CACHE = CACHE_DIR / "installed"
|
|
26
|
+
AVAILABLE_CACHE = CACHE_DIR / "available"
|
|
27
|
+
CACHE_MAX_AGE = 24 * 60 * 60
|
|
28
|
+
CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "dnfseek"
|
|
29
|
+
THEME_FILE = CONFIG_DIR / "theme"
|
|
30
|
+
DEFAULT_HINT = "Search for a package, and press TAB to switch focus"
|
|
31
|
+
HIDDEN_SYSTEM_COMMANDS = {"Screenshot", "Maximize", "Minimize"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PackageList(OptionList):
|
|
35
|
+
BINDINGS = [
|
|
36
|
+
Binding("space", "select", "Select", show=False),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DnfseekApp(App):
|
|
41
|
+
CSS_PATH = "main.tcss"
|
|
42
|
+
TITLE = "dnfseek"
|
|
43
|
+
SUB_TITLE = "a TUI dnf wrapper"
|
|
44
|
+
|
|
45
|
+
BINDINGS = [
|
|
46
|
+
("u", "upgrade_all", "Upgrade all packages"),
|
|
47
|
+
("r", "refresh_cache", "Refresh cache"),
|
|
48
|
+
("i", "install", "Install package"),
|
|
49
|
+
("x", "remove", "Remove package"),
|
|
50
|
+
("e", "reinstall", "Reinstall package"),
|
|
51
|
+
("g", "update_package", "Update package"),
|
|
52
|
+
("d", "deps", "Show dependencies"),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
super().__init__()
|
|
57
|
+
if (saved_theme := self._load_saved_theme()) is not None:
|
|
58
|
+
self.theme = saved_theme
|
|
59
|
+
self._installed: set[str] = set()
|
|
60
|
+
self._available: set[str] = set()
|
|
61
|
+
self._info_cache: dict[str, str] = {}
|
|
62
|
+
self._deps_cache: dict[str, str] = {}
|
|
63
|
+
self._pending_fetches: set[str] = set()
|
|
64
|
+
self._active_package: str | None = None
|
|
65
|
+
self._view_names: list[str] = []
|
|
66
|
+
self._filter = ""
|
|
67
|
+
|
|
68
|
+
def compose(self) -> ComposeResult:
|
|
69
|
+
yield Header()
|
|
70
|
+
with Horizontal(id="options"):
|
|
71
|
+
yield LoadingIndicator(id="spinner", classes="hidden")
|
|
72
|
+
yield Static(DEFAULT_HINT, id="options_text")
|
|
73
|
+
|
|
74
|
+
with Horizontal():
|
|
75
|
+
with Vertical():
|
|
76
|
+
yield Input(placeholder="Type Package Name", id="input")
|
|
77
|
+
yield PackageList(id="left_panel")
|
|
78
|
+
yield Static("Select a package (enter/space) to view its information", id="right_panel", markup=False)
|
|
79
|
+
yield Footer()
|
|
80
|
+
|
|
81
|
+
def on_mount(self) -> None:
|
|
82
|
+
self.theme_changed_signal.subscribe(self, self._save_theme)
|
|
83
|
+
self.styles.scrollbar_visibility = "hidden"
|
|
84
|
+
self.run_worker(
|
|
85
|
+
self._show_packages(installed_only=False),
|
|
86
|
+
name="search-all",
|
|
87
|
+
group="search",
|
|
88
|
+
exclusive=True,
|
|
89
|
+
exit_on_error=False,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _load_saved_theme(self) -> str | None:
|
|
93
|
+
if "TEXTUAL_THEME" in os.environ:
|
|
94
|
+
return None
|
|
95
|
+
try:
|
|
96
|
+
saved = THEME_FILE.read_text().strip()
|
|
97
|
+
except OSError:
|
|
98
|
+
return None
|
|
99
|
+
if not saved or self.get_theme(saved) is None:
|
|
100
|
+
return None
|
|
101
|
+
return saved
|
|
102
|
+
|
|
103
|
+
def _save_theme(self, theme: Theme) -> None:
|
|
104
|
+
try:
|
|
105
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
THEME_FILE.write_text(theme.name)
|
|
107
|
+
except OSError:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:
|
|
111
|
+
# keep built-in commands, minus the ones we don't need
|
|
112
|
+
for command in super().get_system_commands(screen):
|
|
113
|
+
if command.title not in HIDDEN_SYSTEM_COMMANDS:
|
|
114
|
+
yield command
|
|
115
|
+
# custom commands
|
|
116
|
+
yield SystemCommand(
|
|
117
|
+
"Upgrade all", "Upgrade all packages with dnf", self.upgrade_all
|
|
118
|
+
)
|
|
119
|
+
yield SystemCommand(
|
|
120
|
+
"Refresh cache", "Refresh the dnf package cache", self.refresh_cache
|
|
121
|
+
)
|
|
122
|
+
yield SystemCommand(
|
|
123
|
+
"Search all", "Search all available packages", self.search_all
|
|
124
|
+
)
|
|
125
|
+
yield SystemCommand(
|
|
126
|
+
"Search installed", "Search installed packages", self.search_installed
|
|
127
|
+
)
|
|
128
|
+
yield SystemCommand(
|
|
129
|
+
"Install package", "Install the highlighted package with dnf",
|
|
130
|
+
self.action_install,
|
|
131
|
+
)
|
|
132
|
+
yield SystemCommand(
|
|
133
|
+
"Remove package", "Remove the highlighted package with dnf",
|
|
134
|
+
self.action_remove,
|
|
135
|
+
)
|
|
136
|
+
yield SystemCommand(
|
|
137
|
+
"Reinstall package", "Reinstall the highlighted package with dnf",
|
|
138
|
+
self.action_reinstall,
|
|
139
|
+
)
|
|
140
|
+
yield SystemCommand(
|
|
141
|
+
"Update package", "Update the highlighted package with dnf",
|
|
142
|
+
self.action_update_package,
|
|
143
|
+
)
|
|
144
|
+
yield SystemCommand(
|
|
145
|
+
"Show dependencies", "Show dependencies of the highlighted package",
|
|
146
|
+
self.action_deps,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def _dnf_list(self, args: list[str]) -> str | None:
|
|
150
|
+
process = await asyncio.create_subprocess_exec(
|
|
151
|
+
"dnf", "-q", *args,
|
|
152
|
+
stdout=asyncio.subprocess.PIPE,
|
|
153
|
+
stderr=asyncio.subprocess.PIPE,
|
|
154
|
+
)
|
|
155
|
+
stdout, _ = await process.communicate()
|
|
156
|
+
if process.returncode != 0:
|
|
157
|
+
return None
|
|
158
|
+
return stdout.decode()
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _parse_dnf_list(output: str) -> list[str]:
|
|
162
|
+
names = []
|
|
163
|
+
for line in output.splitlines():
|
|
164
|
+
parts = line.split()
|
|
165
|
+
if len(parts) >= 3 and re.search(r"[0-9.-]", parts[1]):
|
|
166
|
+
names.append(parts[0])
|
|
167
|
+
return names
|
|
168
|
+
|
|
169
|
+
async def _write_cache(self, path: Path, names: list[str]) -> None:
|
|
170
|
+
fd, tmp = tempfile.mkstemp(dir=CACHE_DIR)
|
|
171
|
+
try:
|
|
172
|
+
with os.fdopen(fd, "w") as f:
|
|
173
|
+
f.write("\n".join(names))
|
|
174
|
+
f.write("\n")
|
|
175
|
+
os.replace(tmp, path)
|
|
176
|
+
except OSError:
|
|
177
|
+
os.unlink(tmp)
|
|
178
|
+
|
|
179
|
+
def _load_cache(self) -> None:
|
|
180
|
+
if INSTALLED_CACHE.exists():
|
|
181
|
+
self._installed = set(INSTALLED_CACHE.read_text().split())
|
|
182
|
+
if AVAILABLE_CACHE.exists():
|
|
183
|
+
self._available = set(AVAILABLE_CACHE.read_text().split())
|
|
184
|
+
|
|
185
|
+
def _cache_is_stale(self) -> bool:
|
|
186
|
+
for path in (INSTALLED_CACHE, AVAILABLE_CACHE):
|
|
187
|
+
if not path.exists():
|
|
188
|
+
return True
|
|
189
|
+
if time.time() - path.stat().st_mtime > CACHE_MAX_AGE:
|
|
190
|
+
return True
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
async def update_cache(self) -> None:
|
|
194
|
+
self.notify("Refreshing package lists...", timeout=2)
|
|
195
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
installed, available = await asyncio.gather(
|
|
197
|
+
self._dnf_list(["list", "--installed"]),
|
|
198
|
+
self._dnf_list(
|
|
199
|
+
["-y", "--setopt=gpgcheck=1", "--setopt=repo_gpgcheck=1",
|
|
200
|
+
"list", "--available"]
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
if installed is None or available is None:
|
|
204
|
+
self.notify("Failed to refresh package lists", severity="error")
|
|
205
|
+
return
|
|
206
|
+
installed_names = self._parse_dnf_list(installed)
|
|
207
|
+
available_names = self._parse_dnf_list(available)
|
|
208
|
+
await asyncio.gather(
|
|
209
|
+
self._write_cache(INSTALLED_CACHE, installed_names),
|
|
210
|
+
self._write_cache(AVAILABLE_CACHE, available_names),
|
|
211
|
+
)
|
|
212
|
+
self._installed = set(installed_names)
|
|
213
|
+
self._available = set(available_names)
|
|
214
|
+
self.notify("Package lists refreshed", severity="information")
|
|
215
|
+
|
|
216
|
+
async def _ensure_cache(self) -> None:
|
|
217
|
+
if not self._installed and not self._available:
|
|
218
|
+
self._load_cache()
|
|
219
|
+
if self._cache_is_stale():
|
|
220
|
+
await self.update_cache()
|
|
221
|
+
|
|
222
|
+
async def refresh_cache(self) -> None:
|
|
223
|
+
self.run_worker(
|
|
224
|
+
self.update_cache(),
|
|
225
|
+
name="refresh-cache",
|
|
226
|
+
group="cache",
|
|
227
|
+
exclusive=True,
|
|
228
|
+
exit_on_error=False,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def action_refresh_cache(self) -> None:
|
|
232
|
+
self.run_worker(
|
|
233
|
+
self.update_cache(),
|
|
234
|
+
name="refresh-cache",
|
|
235
|
+
group="cache",
|
|
236
|
+
exclusive=True,
|
|
237
|
+
exit_on_error=False,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _populate_options(self) -> None:
|
|
241
|
+
names = self._view_names
|
|
242
|
+
if self._filter:
|
|
243
|
+
needle = self._filter.casefold()
|
|
244
|
+
names = [name for name in names if needle in name.casefold()]
|
|
245
|
+
options = [
|
|
246
|
+
Option(f"✅ {name}" if name in self._installed else name, id=name)
|
|
247
|
+
for name in names
|
|
248
|
+
]
|
|
249
|
+
left = self.query_one("#left_panel", OptionList)
|
|
250
|
+
left.clear_options()
|
|
251
|
+
left.add_options(options)
|
|
252
|
+
if options:
|
|
253
|
+
left.highlighted = 0
|
|
254
|
+
|
|
255
|
+
async def search_all(self) -> None:
|
|
256
|
+
self.run_worker(
|
|
257
|
+
self._show_packages(installed_only=False),
|
|
258
|
+
name="search-all",
|
|
259
|
+
group="search",
|
|
260
|
+
exclusive=True,
|
|
261
|
+
exit_on_error=False,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
async def search_installed(self) -> None:
|
|
265
|
+
self.run_worker(
|
|
266
|
+
self._show_packages(installed_only=True),
|
|
267
|
+
name="show-installed",
|
|
268
|
+
group="search",
|
|
269
|
+
exclusive=True,
|
|
270
|
+
exit_on_error=False,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
async def _show_packages(self, installed_only: bool) -> None:
|
|
274
|
+
await self._ensure_cache()
|
|
275
|
+
if installed_only:
|
|
276
|
+
names = sorted(self._installed)
|
|
277
|
+
else:
|
|
278
|
+
names = sorted(self._installed | self._available)
|
|
279
|
+
self._view_names = names
|
|
280
|
+
self._filter = ""
|
|
281
|
+
self.query_one("#input", Input).value = ""
|
|
282
|
+
self._populate_options()
|
|
283
|
+
|
|
284
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
285
|
+
if event.input.id != "input":
|
|
286
|
+
return
|
|
287
|
+
if event.value == self._filter:
|
|
288
|
+
return
|
|
289
|
+
self._filter = event.value
|
|
290
|
+
self._populate_options()
|
|
291
|
+
|
|
292
|
+
def _selected_package(self) -> str | None:
|
|
293
|
+
option = self.query_one("#left_panel", OptionList).highlighted_option
|
|
294
|
+
return option.id if option is not None else None
|
|
295
|
+
|
|
296
|
+
def _format_info(self, name: str, content: str) -> str:
|
|
297
|
+
if name in self._installed:
|
|
298
|
+
actions = "x Remove | e Reinstall | g Update | d Dependencies"
|
|
299
|
+
else:
|
|
300
|
+
actions = "i Install | d Dependencies"
|
|
301
|
+
return f"{content}\n\n── Actions ──\n{actions}"
|
|
302
|
+
|
|
303
|
+
def on_option_list_option_selected(
|
|
304
|
+
self, event: OptionList.OptionSelected
|
|
305
|
+
) -> None:
|
|
306
|
+
name = event.option_id
|
|
307
|
+
if name is None:
|
|
308
|
+
return
|
|
309
|
+
self._active_package = name
|
|
310
|
+
right_panel = self.query_one("#right_panel", Static)
|
|
311
|
+
if name in self._info_cache:
|
|
312
|
+
right_panel.update(self._format_info(name, self._info_cache[name]))
|
|
313
|
+
return
|
|
314
|
+
if name in self._pending_fetches:
|
|
315
|
+
return
|
|
316
|
+
self._pending_fetches.add(name)
|
|
317
|
+
right_panel.update(f"Fetching info for {name}...")
|
|
318
|
+
self.run_worker(
|
|
319
|
+
self._fetch_package_info(name),
|
|
320
|
+
name=f"info-{name}",
|
|
321
|
+
group="info",
|
|
322
|
+
exit_on_error=False,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
async def _fetch_package_info(self, name: str) -> None:
|
|
326
|
+
process = await asyncio.create_subprocess_exec(
|
|
327
|
+
"dnf", "info", name,
|
|
328
|
+
stdout=asyncio.subprocess.PIPE,
|
|
329
|
+
stderr=asyncio.subprocess.PIPE,
|
|
330
|
+
)
|
|
331
|
+
stdout, _ = await process.communicate()
|
|
332
|
+
self._pending_fetches.discard(name)
|
|
333
|
+
if process.returncode != 0:
|
|
334
|
+
return
|
|
335
|
+
lines = [
|
|
336
|
+
line.strip()
|
|
337
|
+
for line in stdout.decode().splitlines()
|
|
338
|
+
if re.match(r"^(Name|Summary|Version|Release|Size|URL|Description)", line.strip())
|
|
339
|
+
]
|
|
340
|
+
info = "\n".join(lines)
|
|
341
|
+
self._info_cache[name] = info
|
|
342
|
+
if name == self._active_package:
|
|
343
|
+
self.query_one("#right_panel", Static).update(
|
|
344
|
+
self._format_info(name, info)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def action_install(self) -> None:
|
|
348
|
+
name = self._selected_package()
|
|
349
|
+
if name is None:
|
|
350
|
+
self.notify("No package selected", severity="warning")
|
|
351
|
+
return
|
|
352
|
+
if name in self._installed:
|
|
353
|
+
self.notify(f"{name} is already installed", severity="warning")
|
|
354
|
+
return
|
|
355
|
+
self.notify(f"Installing {name}...", timeout=2)
|
|
356
|
+
self.run_worker(
|
|
357
|
+
self._run_dnf(
|
|
358
|
+
["dnf", "install", "-y", name],
|
|
359
|
+
success_msg=f"Installed: {name}",
|
|
360
|
+
name=name,
|
|
361
|
+
installed="add",
|
|
362
|
+
),
|
|
363
|
+
name=f"install-{name}",
|
|
364
|
+
group="dnf",
|
|
365
|
+
exclusive=True,
|
|
366
|
+
exit_on_error=False,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def action_remove(self) -> None:
|
|
370
|
+
name = self._selected_package()
|
|
371
|
+
if name is None:
|
|
372
|
+
self.notify("No package selected", severity="warning")
|
|
373
|
+
return
|
|
374
|
+
if name not in self._installed:
|
|
375
|
+
self.notify(f"{name} is not installed", severity="warning")
|
|
376
|
+
return
|
|
377
|
+
self.notify(f"Removing {name}...", timeout=2)
|
|
378
|
+
self.run_worker(
|
|
379
|
+
self._run_dnf(
|
|
380
|
+
["dnf", "remove", "-y", name],
|
|
381
|
+
success_msg=f"Removed: {name}",
|
|
382
|
+
name=name,
|
|
383
|
+
installed="remove",
|
|
384
|
+
),
|
|
385
|
+
name=f"remove-{name}",
|
|
386
|
+
group="dnf",
|
|
387
|
+
exclusive=True,
|
|
388
|
+
exit_on_error=False,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def action_reinstall(self) -> None:
|
|
392
|
+
name = self._selected_package()
|
|
393
|
+
if name is None:
|
|
394
|
+
self.notify("No package selected", severity="warning")
|
|
395
|
+
return
|
|
396
|
+
if name not in self._installed:
|
|
397
|
+
self.notify(f"{name} is not installed", severity="warning")
|
|
398
|
+
return
|
|
399
|
+
self.notify(f"Reinstalling {name}...", timeout=2)
|
|
400
|
+
self.run_worker(
|
|
401
|
+
self._run_dnf(
|
|
402
|
+
["dnf", "reinstall", "-y", name],
|
|
403
|
+
success_msg=f"Reinstalled: {name}",
|
|
404
|
+
name=name,
|
|
405
|
+
),
|
|
406
|
+
name=f"reinstall-{name}",
|
|
407
|
+
group="dnf",
|
|
408
|
+
exclusive=True,
|
|
409
|
+
exit_on_error=False,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
def action_update_package(self) -> None:
|
|
413
|
+
name = self._selected_package()
|
|
414
|
+
if name is None:
|
|
415
|
+
self.notify("No package selected", severity="warning")
|
|
416
|
+
return
|
|
417
|
+
if name not in self._installed:
|
|
418
|
+
self.notify(f"{name} is not installed", severity="warning")
|
|
419
|
+
return
|
|
420
|
+
self.notify(f"Updating {name}...", timeout=2)
|
|
421
|
+
self.run_worker(
|
|
422
|
+
self._run_dnf(
|
|
423
|
+
["dnf", "upgrade", "-y", name],
|
|
424
|
+
success_msg=f"Updated: {name}",
|
|
425
|
+
name=name,
|
|
426
|
+
),
|
|
427
|
+
name=f"update-{name}",
|
|
428
|
+
group="dnf",
|
|
429
|
+
exclusive=True,
|
|
430
|
+
exit_on_error=False,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
def action_deps(self) -> None:
|
|
434
|
+
name = self._selected_package()
|
|
435
|
+
if name is None:
|
|
436
|
+
self.notify("No package selected", severity="warning")
|
|
437
|
+
return
|
|
438
|
+
right_panel = self.query_one("#right_panel", Static)
|
|
439
|
+
if name in self._deps_cache:
|
|
440
|
+
right_panel.update(self._format_info(name, self._deps_cache[name]))
|
|
441
|
+
return
|
|
442
|
+
right_panel.update(f"Fetching dependencies for {name}...")
|
|
443
|
+
self.run_worker(
|
|
444
|
+
self._fetch_deps(name),
|
|
445
|
+
name=f"deps-{name}",
|
|
446
|
+
group="info",
|
|
447
|
+
exit_on_error=False,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
async def _fetch_deps(self, name: str) -> None:
|
|
451
|
+
process = await asyncio.create_subprocess_exec(
|
|
452
|
+
"dnf", "repoquery", "--requires", name,
|
|
453
|
+
stdout=asyncio.subprocess.PIPE,
|
|
454
|
+
stderr=asyncio.subprocess.PIPE,
|
|
455
|
+
)
|
|
456
|
+
stdout, _ = await process.communicate()
|
|
457
|
+
right_panel = self.query_one("#right_panel", Static)
|
|
458
|
+
if process.returncode != 0:
|
|
459
|
+
right_panel.update(f"Could not fetch dependencies for {name}")
|
|
460
|
+
self.notify(f"Could not fetch dependencies for {name}", severity="error")
|
|
461
|
+
return
|
|
462
|
+
deps = [line.strip() for line in stdout.decode().splitlines() if line.strip()]
|
|
463
|
+
text = "\n".join(deps) if deps else "No dependencies"
|
|
464
|
+
self._deps_cache[name] = text
|
|
465
|
+
right_panel.update(self._format_info(name, text))
|
|
466
|
+
|
|
467
|
+
def _restore_package_info(self, name: str) -> None:
|
|
468
|
+
self._active_package = name
|
|
469
|
+
if name in self._info_cache:
|
|
470
|
+
self.query_one("#right_panel", Static).update(
|
|
471
|
+
self._format_info(name, self._info_cache[name])
|
|
472
|
+
)
|
|
473
|
+
else:
|
|
474
|
+
self.run_worker(
|
|
475
|
+
self._fetch_package_info(name),
|
|
476
|
+
name=f"info-{name}",
|
|
477
|
+
group="info",
|
|
478
|
+
exit_on_error=False,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
def _mark_installed(self, name: str) -> None:
|
|
482
|
+
self._installed.add(name)
|
|
483
|
+
self._available.discard(name)
|
|
484
|
+
self._info_cache.pop(name, None)
|
|
485
|
+
self._deps_cache.pop(name, None)
|
|
486
|
+
self._populate_options()
|
|
487
|
+
self.run_worker(
|
|
488
|
+
self._sync_cache_files(),
|
|
489
|
+
name="cache-sync",
|
|
490
|
+
group="cache",
|
|
491
|
+
exclusive=True,
|
|
492
|
+
exit_on_error=False,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
def _mark_removed(self, name: str) -> None:
|
|
496
|
+
self._installed.discard(name)
|
|
497
|
+
self._available.add(name)
|
|
498
|
+
self._info_cache.pop(name, None)
|
|
499
|
+
self._deps_cache.pop(name, None)
|
|
500
|
+
self._populate_options()
|
|
501
|
+
self.run_worker(
|
|
502
|
+
self._sync_cache_files(),
|
|
503
|
+
name="cache-sync",
|
|
504
|
+
group="cache",
|
|
505
|
+
exclusive=True,
|
|
506
|
+
exit_on_error=False,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
async def _sync_cache_files(self) -> None:
|
|
510
|
+
await asyncio.gather(
|
|
511
|
+
self._write_cache(INSTALLED_CACHE, sorted(self._installed)),
|
|
512
|
+
self._write_cache(AVAILABLE_CACHE, sorted(self._available)),
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
def _start_upgrade(self) -> None:
|
|
516
|
+
self.notify("Upgrading all packages...", timeout=2)
|
|
517
|
+
self.run_worker(
|
|
518
|
+
self._run_dnf(
|
|
519
|
+
["dnf", "upgrade", "-y"],
|
|
520
|
+
success_msg="All packages upgraded!",
|
|
521
|
+
),
|
|
522
|
+
name="upgrade_all",
|
|
523
|
+
group="dnf",
|
|
524
|
+
exclusive=True,
|
|
525
|
+
exit_on_error=False,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
async def upgrade_all(self) -> None:
|
|
529
|
+
self._start_upgrade()
|
|
530
|
+
|
|
531
|
+
def action_upgrade_all(self) -> None:
|
|
532
|
+
self._start_upgrade()
|
|
533
|
+
|
|
534
|
+
async def _run_dnf(
|
|
535
|
+
self,
|
|
536
|
+
args: list[str],
|
|
537
|
+
success_msg: str = "Command completed successfully",
|
|
538
|
+
name: str | None = None,
|
|
539
|
+
installed: str | None = None,
|
|
540
|
+
) -> None:
|
|
541
|
+
right_panel = self.query_one("#right_panel", Static)
|
|
542
|
+
self._show_status(self._action_status(args, name))
|
|
543
|
+
try:
|
|
544
|
+
right_panel.update(f"Running: {' '.join(args)}")
|
|
545
|
+
returncode, stderr = await self._sudo_once(args)
|
|
546
|
+
if returncode != 0 and (
|
|
547
|
+
b"a password is required" in stderr.lower()
|
|
548
|
+
or b"no password was provided" in stderr.lower()
|
|
549
|
+
):
|
|
550
|
+
self.notify(
|
|
551
|
+
"Sudo session expired — please restart dnfseek",
|
|
552
|
+
severity="error",
|
|
553
|
+
)
|
|
554
|
+
self.exit(
|
|
555
|
+
result="dnfseek: sudo session expired. "
|
|
556
|
+
"Re-run the app to reauthenticate."
|
|
557
|
+
)
|
|
558
|
+
return
|
|
559
|
+
if returncode == 0:
|
|
560
|
+
self.notify(success_msg, timeout=2, severity="information")
|
|
561
|
+
if installed == "add" and name is not None:
|
|
562
|
+
self._mark_installed(name)
|
|
563
|
+
self._restore_package_info(name)
|
|
564
|
+
elif installed == "remove" and name is not None:
|
|
565
|
+
self._mark_removed(name)
|
|
566
|
+
self._restore_package_info(name)
|
|
567
|
+
elif name is not None:
|
|
568
|
+
self._restore_package_info(name)
|
|
569
|
+
else:
|
|
570
|
+
stderr_text = stderr.decode(errors="replace")
|
|
571
|
+
error_text = stderr_text.lower()
|
|
572
|
+
if stderr:
|
|
573
|
+
error_lines = stderr_text.splitlines()[-10:]
|
|
574
|
+
right_panel.update("\n".join(error_lines))
|
|
575
|
+
if "already installed" in error_text:
|
|
576
|
+
self.notify(
|
|
577
|
+
f"{name or 'Package'} is already installed", severity="warning"
|
|
578
|
+
)
|
|
579
|
+
elif "already the latest" in error_text or "nothing to do" in error_text:
|
|
580
|
+
self.notify(
|
|
581
|
+
f"{name or 'Packages'} are already up to date",
|
|
582
|
+
severity="warning",
|
|
583
|
+
)
|
|
584
|
+
else:
|
|
585
|
+
self.notify(
|
|
586
|
+
f"Command failed (error code {returncode})",
|
|
587
|
+
severity="error",
|
|
588
|
+
)
|
|
589
|
+
finally:
|
|
590
|
+
self._hide_status()
|
|
591
|
+
|
|
592
|
+
async def _sudo_once(self, args: list[str]) -> tuple[int, bytes]:
|
|
593
|
+
"""Run `sudo -n <cmd> ...` once, streaming stdout to the right panel."""
|
|
594
|
+
right_panel = self.query_one("#right_panel", Static)
|
|
595
|
+
process = await asyncio.create_subprocess_exec(
|
|
596
|
+
"sudo", "-n", *args,
|
|
597
|
+
stdout=asyncio.subprocess.PIPE,
|
|
598
|
+
stderr=asyncio.subprocess.PIPE,
|
|
599
|
+
)
|
|
600
|
+
stderr_task = asyncio.create_task(process.stderr.read())
|
|
601
|
+
lines: list[str] = []
|
|
602
|
+
async for chunk in process.stdout:
|
|
603
|
+
for line in chunk.decode(errors="replace").replace("\r", "\n").splitlines():
|
|
604
|
+
if line.strip():
|
|
605
|
+
lines.append(line.strip())
|
|
606
|
+
right_panel.update("\n".join(lines[-15:]))
|
|
607
|
+
stderr = await stderr_task
|
|
608
|
+
await process.wait()
|
|
609
|
+
return process.returncode or 0, stderr
|
|
610
|
+
|
|
611
|
+
@staticmethod
|
|
612
|
+
def _action_status(args: list[str], name: str | None) -> str:
|
|
613
|
+
action = args[1]
|
|
614
|
+
if action == "install":
|
|
615
|
+
return f"Installing {name}..."
|
|
616
|
+
if action == "remove":
|
|
617
|
+
return f"Removing {name}..."
|
|
618
|
+
if action == "reinstall":
|
|
619
|
+
return f"Reinstalling {name}..."
|
|
620
|
+
if action == "upgrade":
|
|
621
|
+
if name is not None:
|
|
622
|
+
return f"Upgrading {name}..."
|
|
623
|
+
return "Upgrading all packages..."
|
|
624
|
+
return f"Running: {' '.join(args)}"
|
|
625
|
+
|
|
626
|
+
def _show_status(self, message: str) -> None:
|
|
627
|
+
self.query_one("#spinner", LoadingIndicator).remove_class("hidden")
|
|
628
|
+
self.query_one("#options_text", Static).update(message)
|
|
629
|
+
|
|
630
|
+
def _hide_status(self) -> None:
|
|
631
|
+
self.query_one("#spinner", LoadingIndicator).add_class("hidden")
|
|
632
|
+
self.query_one("#options_text", Static).update(DEFAULT_HINT)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def main() -> None:
|
|
636
|
+
import subprocess
|
|
637
|
+
import sys
|
|
638
|
+
|
|
639
|
+
if subprocess.run(["sudo", "-v"]).returncode != 0:
|
|
640
|
+
raise SystemExit("Authentication failed - dnfseek requires sudo access")
|
|
641
|
+
app = DnfseekApp()
|
|
642
|
+
result = app.run()
|
|
643
|
+
if result is not None:
|
|
644
|
+
print(result, file=sys.stderr)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
if __name__ == "__main__":
|
|
648
|
+
main()
|
dnfseek-0.1.0/main.tcss
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Screen {
|
|
2
|
+
background: $background;
|
|
3
|
+
color: $foreground;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
Horizontal {
|
|
7
|
+
width: 100%;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
#options {
|
|
11
|
+
background: $panel;
|
|
12
|
+
padding: 0 1;
|
|
13
|
+
border: solid $border-blurred;
|
|
14
|
+
height: 3;
|
|
15
|
+
align: left middle;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#options_text {
|
|
19
|
+
width: 1fr;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#spinner {
|
|
23
|
+
width: 10;
|
|
24
|
+
height: 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.hidden {
|
|
28
|
+
display: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#input {
|
|
32
|
+
background: $panel;
|
|
33
|
+
border: solid $border-blurred;
|
|
34
|
+
padding: 0 1;
|
|
35
|
+
/*margin: 0 0 1 0;*/
|
|
36
|
+
}
|
|
37
|
+
#input:focus {
|
|
38
|
+
border: solid $border;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#left_panel {
|
|
42
|
+
width: 1fr;
|
|
43
|
+
height: 1fr;
|
|
44
|
+
background: $panel;
|
|
45
|
+
border: solid $border-blurred;
|
|
46
|
+
padding: 0 1;
|
|
47
|
+
/*margin: 0 0 1 0;*/
|
|
48
|
+
}
|
|
49
|
+
#left_panel:focus-within {
|
|
50
|
+
border: solid $border;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#right_panel {
|
|
54
|
+
width: 66%;
|
|
55
|
+
height: 1fr;
|
|
56
|
+
background: $panel;
|
|
57
|
+
border: solid $border-blurred;
|
|
58
|
+
padding: 1 2;
|
|
59
|
+
/*margin: 0 0 1 0;*/
|
|
60
|
+
}
|
|
61
|
+
#right_panel:focus-within {
|
|
62
|
+
border: solid $border;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
PasswordScreen {
|
|
66
|
+
align: center middle;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
PasswordScreen #password {
|
|
70
|
+
width: 40;
|
|
71
|
+
background: $panel;
|
|
72
|
+
border: solid $border-blurred;
|
|
73
|
+
padding: 0 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
PasswordScreen #password:focus {
|
|
77
|
+
border: solid $border;
|
|
78
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dnfseek"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A TUI package browser for Fedora, built with Textual"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"textual>=8.2.8",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[project.scripts]
|
|
12
|
+
dnfseek = "main:main"
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["hatchling"]
|
|
16
|
+
build-backend = "hatchling.build"
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build.targets.wheel]
|
|
19
|
+
include = [
|
|
20
|
+
"main.py",
|
|
21
|
+
"main.tcss",
|
|
22
|
+
]
|
dnfseek-0.1.0/uv.lock
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.12"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "dnfseek"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
source = { editable = "." }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "textual" },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[package.metadata]
|
|
14
|
+
requires-dist = [{ name = "textual", specifier = ">=8.2.8" }]
|
|
15
|
+
|
|
16
|
+
[[package]]
|
|
17
|
+
name = "linkify-it-py"
|
|
18
|
+
version = "2.1.0"
|
|
19
|
+
source = { registry = "https://pypi.org/simple" }
|
|
20
|
+
dependencies = [
|
|
21
|
+
{ name = "uc-micro-py" },
|
|
22
|
+
]
|
|
23
|
+
sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" }
|
|
24
|
+
wheels = [
|
|
25
|
+
{ url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[[package]]
|
|
29
|
+
name = "markdown-it-py"
|
|
30
|
+
version = "4.2.0"
|
|
31
|
+
source = { registry = "https://pypi.org/simple" }
|
|
32
|
+
dependencies = [
|
|
33
|
+
{ name = "mdurl" },
|
|
34
|
+
]
|
|
35
|
+
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
|
36
|
+
wheels = [
|
|
37
|
+
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[package.optional-dependencies]
|
|
41
|
+
linkify = [
|
|
42
|
+
{ name = "linkify-it-py" },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[[package]]
|
|
46
|
+
name = "mdit-py-plugins"
|
|
47
|
+
version = "0.6.1"
|
|
48
|
+
source = { registry = "https://pypi.org/simple" }
|
|
49
|
+
dependencies = [
|
|
50
|
+
{ name = "markdown-it-py" },
|
|
51
|
+
]
|
|
52
|
+
sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" }
|
|
53
|
+
wheels = [
|
|
54
|
+
{ url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" },
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[[package]]
|
|
58
|
+
name = "mdurl"
|
|
59
|
+
version = "0.1.2"
|
|
60
|
+
source = { registry = "https://pypi.org/simple" }
|
|
61
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
|
62
|
+
wheels = [
|
|
63
|
+
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[[package]]
|
|
67
|
+
name = "platformdirs"
|
|
68
|
+
version = "4.11.0"
|
|
69
|
+
source = { registry = "https://pypi.org/simple" }
|
|
70
|
+
sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" }
|
|
71
|
+
wheels = [
|
|
72
|
+
{ url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" },
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[[package]]
|
|
76
|
+
name = "pygments"
|
|
77
|
+
version = "2.20.0"
|
|
78
|
+
source = { registry = "https://pypi.org/simple" }
|
|
79
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
80
|
+
wheels = [
|
|
81
|
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
[[package]]
|
|
85
|
+
name = "rich"
|
|
86
|
+
version = "15.0.0"
|
|
87
|
+
source = { registry = "https://pypi.org/simple" }
|
|
88
|
+
dependencies = [
|
|
89
|
+
{ name = "markdown-it-py" },
|
|
90
|
+
{ name = "pygments" },
|
|
91
|
+
]
|
|
92
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
|
93
|
+
wheels = [
|
|
94
|
+
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[[package]]
|
|
98
|
+
name = "textual"
|
|
99
|
+
version = "8.2.8"
|
|
100
|
+
source = { registry = "https://pypi.org/simple" }
|
|
101
|
+
dependencies = [
|
|
102
|
+
{ name = "markdown-it-py", extra = ["linkify"] },
|
|
103
|
+
{ name = "mdit-py-plugins" },
|
|
104
|
+
{ name = "platformdirs" },
|
|
105
|
+
{ name = "pygments" },
|
|
106
|
+
{ name = "rich" },
|
|
107
|
+
{ name = "typing-extensions" },
|
|
108
|
+
]
|
|
109
|
+
sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" }
|
|
110
|
+
wheels = [
|
|
111
|
+
{ url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" },
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
[[package]]
|
|
115
|
+
name = "typing-extensions"
|
|
116
|
+
version = "4.16.0"
|
|
117
|
+
source = { registry = "https://pypi.org/simple" }
|
|
118
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
|
119
|
+
wheels = [
|
|
120
|
+
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
[[package]]
|
|
124
|
+
name = "uc-micro-py"
|
|
125
|
+
version = "2.0.0"
|
|
126
|
+
source = { registry = "https://pypi.org/simple" }
|
|
127
|
+
sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" }
|
|
128
|
+
wheels = [
|
|
129
|
+
{ url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" },
|
|
130
|
+
]
|