kappman 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.
- kappman-0.1.0/.gitignore +10 -0
- kappman-0.1.0/.python-version +1 -0
- kappman-0.1.0/LICENSE +21 -0
- kappman-0.1.0/PKG-INFO +102 -0
- kappman-0.1.0/README.md +87 -0
- kappman-0.1.0/kappman/__init__.py +4 -0
- kappman-0.1.0/kappman/autostart/kappman-watcher.desktop +9 -0
- kappman-0.1.0/kappman/config.py +110 -0
- kappman-0.1.0/kappman/core.py +239 -0
- kappman-0.1.0/kappman/gui.py +463 -0
- kappman-0.1.0/kappman/main.py +133 -0
- kappman-0.1.0/kappman/qml/main.qml +46 -0
- kappman-0.1.0/kappman/themes/breeze_dark.qss +78 -0
- kappman-0.1.0/kappman/themes/catppuccin_latte.qss +82 -0
- kappman-0.1.0/kappman/themes/catppuccin_macchiato.qss +78 -0
- kappman-0.1.0/kappman/themes/catppuccin_mocha.qss +95 -0
- kappman-0.1.0/kappman/watcher.py +131 -0
- kappman-0.1.0/pyproject.toml +28 -0
- kappman-0.1.0/tests/test_config.py +43 -0
- kappman-0.1.0/tests/test_core.py +87 -0
- kappman-0.1.0/uv.lock +197 -0
kappman-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
kappman-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KAppMan Contributors
|
|
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.
|
kappman-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kappman
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A KDE-native AppImage manager — watch, integrate, and manage AppImages from your desktop.
|
|
5
|
+
Author: KAppMan Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: pyqt6>=6.6.0
|
|
10
|
+
Requires-Dist: watchdog>=4.0.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest-mock>=3.12; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# KAppMan – KDE AppImage Manager
|
|
17
|
+
|
|
18
|
+
> A lightweight, KDE-native AppImage manager built with Python + PyQt6. Managed by [uv](https://github.com/astral-sh/uv).
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Auto-integrate** AppImages from any directory you choose
|
|
23
|
+
- **Desktop entries** auto-generated in `~/.local/share/applications/` (XDG compliant)
|
|
24
|
+
- **Folder watcher** — drop an AppImage and it's integrated instantly
|
|
25
|
+
- **Configurable watch directory** — set it from the GUI or CLI
|
|
26
|
+
- **Live theme switching** — pick any `.qss` file from a directory; ships with four themes:
|
|
27
|
+
- Catppuccin Mocha (default)
|
|
28
|
+
- Catppuccin Macchiato
|
|
29
|
+
- Catppuccin Latte
|
|
30
|
+
- Breeze Dark
|
|
31
|
+
- **Custom themes** — drop your own `.qss` into the themes directory and it appears in the selector instantly
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Requires uv — https://docs.astral.sh/uv/
|
|
39
|
+
git clone https://github.com/you/KAppMan
|
|
40
|
+
cd KAppMan
|
|
41
|
+
uv sync
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Launch the GUI (default)
|
|
50
|
+
uv run kappman
|
|
51
|
+
|
|
52
|
+
# Headless folder watcher (default dir: ~/AppImages)
|
|
53
|
+
uv run kappman --watch
|
|
54
|
+
|
|
55
|
+
# Watch a custom directory
|
|
56
|
+
uv run kappman --watch /mnt/storage/Apps
|
|
57
|
+
|
|
58
|
+
# One-shot: integrate a single AppImage
|
|
59
|
+
uv run kappman --integrate ~/Downloads/MyApp.AppImage
|
|
60
|
+
|
|
61
|
+
# Remove a desktop entry
|
|
62
|
+
uv run kappman --remove ~/AppImages/MyApp.AppImage
|
|
63
|
+
|
|
64
|
+
# List all KAppMan-integrated apps
|
|
65
|
+
uv run kappman --list
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Adding Themes
|
|
71
|
+
|
|
72
|
+
1. Create a `.qss` file (standard Qt stylesheet syntax)
|
|
73
|
+
2. Drop it into `kappman/themes/` **or** any custom directory
|
|
74
|
+
3. In the GUI, point the **Themes Directory** field to your folder
|
|
75
|
+
4. Select your theme from the dropdown — applied instantly, no restart needed
|
|
76
|
+
|
|
77
|
+
Your theme choice and directory are persisted to `~/.config/kappman/config.ini`.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## KDE Autostart
|
|
82
|
+
|
|
83
|
+
To auto-run the watcher on every login:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cp kappman/autostart/kappman-watcher.desktop ~/.config/autostart/
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Development
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
uv sync
|
|
95
|
+
uv run pytest tests/ -v
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
kappman-0.1.0/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# KAppMan – KDE AppImage Manager
|
|
2
|
+
|
|
3
|
+
> A lightweight, KDE-native AppImage manager built with Python + PyQt6. Managed by [uv](https://github.com/astral-sh/uv).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Auto-integrate** AppImages from any directory you choose
|
|
8
|
+
- **Desktop entries** auto-generated in `~/.local/share/applications/` (XDG compliant)
|
|
9
|
+
- **Folder watcher** — drop an AppImage and it's integrated instantly
|
|
10
|
+
- **Configurable watch directory** — set it from the GUI or CLI
|
|
11
|
+
- **Live theme switching** — pick any `.qss` file from a directory; ships with four themes:
|
|
12
|
+
- Catppuccin Mocha (default)
|
|
13
|
+
- Catppuccin Macchiato
|
|
14
|
+
- Catppuccin Latte
|
|
15
|
+
- Breeze Dark
|
|
16
|
+
- **Custom themes** — drop your own `.qss` into the themes directory and it appears in the selector instantly
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Requires uv — https://docs.astral.sh/uv/
|
|
24
|
+
git clone https://github.com/you/KAppMan
|
|
25
|
+
cd KAppMan
|
|
26
|
+
uv sync
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Launch the GUI (default)
|
|
35
|
+
uv run kappman
|
|
36
|
+
|
|
37
|
+
# Headless folder watcher (default dir: ~/AppImages)
|
|
38
|
+
uv run kappman --watch
|
|
39
|
+
|
|
40
|
+
# Watch a custom directory
|
|
41
|
+
uv run kappman --watch /mnt/storage/Apps
|
|
42
|
+
|
|
43
|
+
# One-shot: integrate a single AppImage
|
|
44
|
+
uv run kappman --integrate ~/Downloads/MyApp.AppImage
|
|
45
|
+
|
|
46
|
+
# Remove a desktop entry
|
|
47
|
+
uv run kappman --remove ~/AppImages/MyApp.AppImage
|
|
48
|
+
|
|
49
|
+
# List all KAppMan-integrated apps
|
|
50
|
+
uv run kappman --list
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Adding Themes
|
|
56
|
+
|
|
57
|
+
1. Create a `.qss` file (standard Qt stylesheet syntax)
|
|
58
|
+
2. Drop it into `kappman/themes/` **or** any custom directory
|
|
59
|
+
3. In the GUI, point the **Themes Directory** field to your folder
|
|
60
|
+
4. Select your theme from the dropdown — applied instantly, no restart needed
|
|
61
|
+
|
|
62
|
+
Your theme choice and directory are persisted to `~/.config/kappman/config.ini`.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## KDE Autostart
|
|
67
|
+
|
|
68
|
+
To auto-run the watcher on every login:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
cp kappman/autostart/kappman-watcher.desktop ~/.config/autostart/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
uv sync
|
|
80
|
+
uv run pytest tests/ -v
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
[Desktop Entry]
|
|
2
|
+
Name=KAppMan Watcher
|
|
3
|
+
Comment=Automatically integrates AppImages from your watch directory into the KDE menu
|
|
4
|
+
Exec=kappman --watch
|
|
5
|
+
Icon=application-x-executable
|
|
6
|
+
Type=Application
|
|
7
|
+
X-KDE-autostart-condition=
|
|
8
|
+
X-KDE-AutostartScript=true
|
|
9
|
+
Hidden=false
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
kappman.config
|
|
3
|
+
==============
|
|
4
|
+
Persistent user configuration stored at ~/.config/kappman/config.ini.
|
|
5
|
+
|
|
6
|
+
Stores:
|
|
7
|
+
- watch_dir : directory to monitor for new AppImages (default: ~/AppImages)
|
|
8
|
+
- theme : name of the active .qss theme file without extension
|
|
9
|
+
(default: catppuccin_mocha)
|
|
10
|
+
- themes_dir : directory scanned for .qss theme files
|
|
11
|
+
(default: the built-in kappman/themes/ package directory)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import configparser
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_CONFIG_DIR = Path.home() / ".config" / "kappman"
|
|
20
|
+
_CONFIG_FILE = _CONFIG_DIR / "config.ini"
|
|
21
|
+
|
|
22
|
+
# Built-in themes bundled with the package
|
|
23
|
+
_BUILTIN_THEMES_DIR = Path(__file__).parent / "themes"
|
|
24
|
+
|
|
25
|
+
_SECTION = "kappman"
|
|
26
|
+
_DEFAULTS: dict[str, str] = {
|
|
27
|
+
"watch_dir": str(Path.home() / "AppImages"),
|
|
28
|
+
"theme": "catppuccin_mocha",
|
|
29
|
+
"themes_dir": str(_BUILTIN_THEMES_DIR),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _load() -> configparser.ConfigParser:
|
|
34
|
+
cfg = configparser.ConfigParser()
|
|
35
|
+
cfg[_SECTION] = _DEFAULTS.copy()
|
|
36
|
+
if _CONFIG_FILE.exists():
|
|
37
|
+
cfg.read(_CONFIG_FILE, encoding="utf-8")
|
|
38
|
+
return cfg
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _save(cfg: configparser.ConfigParser) -> None:
|
|
42
|
+
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
with open(_CONFIG_FILE, "w", encoding="utf-8") as fh:
|
|
44
|
+
cfg.write(fh)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ── Watch directory ────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
def get_watch_dir() -> Path:
|
|
50
|
+
return Path(_load()[_SECTION]["watch_dir"]).expanduser()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def set_watch_dir(directory: str | Path) -> None:
|
|
54
|
+
cfg = _load()
|
|
55
|
+
cfg[_SECTION]["watch_dir"] = str(Path(directory).expanduser().resolve())
|
|
56
|
+
_save(cfg)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ── Themes directory ───────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def get_themes_dir() -> Path:
|
|
62
|
+
return Path(_load()[_SECTION]["themes_dir"]).expanduser()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def set_themes_dir(directory: str | Path) -> None:
|
|
66
|
+
cfg = _load()
|
|
67
|
+
cfg[_SECTION]["themes_dir"] = str(Path(directory).expanduser().resolve())
|
|
68
|
+
_save(cfg)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def list_themes(themes_dir: Path | None = None) -> list[str]:
|
|
72
|
+
"""
|
|
73
|
+
Return a sorted list of theme names (filename stems) found in *themes_dir*.
|
|
74
|
+
Falls back to the built-in themes directory if none given.
|
|
75
|
+
"""
|
|
76
|
+
d = themes_dir or get_themes_dir()
|
|
77
|
+
if not d.exists():
|
|
78
|
+
return []
|
|
79
|
+
return sorted(p.stem for p in d.glob("*.qss"))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_theme_stylesheet(theme_name: str, themes_dir: Path | None = None) -> str:
|
|
83
|
+
"""
|
|
84
|
+
Read and return the QSS content for *theme_name*.
|
|
85
|
+
Looks in *themes_dir* first, then in the built-in themes directory.
|
|
86
|
+
Returns an empty string if the file is not found.
|
|
87
|
+
"""
|
|
88
|
+
dirs = []
|
|
89
|
+
if themes_dir:
|
|
90
|
+
dirs.append(Path(themes_dir))
|
|
91
|
+
dirs.append(get_themes_dir())
|
|
92
|
+
dirs.append(_BUILTIN_THEMES_DIR)
|
|
93
|
+
|
|
94
|
+
for d in dirs:
|
|
95
|
+
qss_file = d / f"{theme_name}.qss"
|
|
96
|
+
if qss_file.exists():
|
|
97
|
+
return qss_file.read_text(encoding="utf-8")
|
|
98
|
+
return ""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ── Active theme ───────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
def get_theme() -> str:
|
|
104
|
+
return _load()[_SECTION]["theme"]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def set_theme(theme_name: str) -> None:
|
|
108
|
+
cfg = _load()
|
|
109
|
+
cfg[_SECTION]["theme"] = theme_name
|
|
110
|
+
_save(cfg)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
kappman.core
|
|
3
|
+
============
|
|
4
|
+
Core logic for AppImage integration and removal.
|
|
5
|
+
|
|
6
|
+
Responsibilities
|
|
7
|
+
----------------
|
|
8
|
+
- Make an AppImage executable (``chmod +x``).
|
|
9
|
+
- Write an XDG-compliant ``.desktop`` entry to
|
|
10
|
+
``~/.local/share/applications/`` so the application appears in the KDE
|
|
11
|
+
(and any other XDG-conformant) application menu.
|
|
12
|
+
- Optionally extract an icon from the AppImage via ``unsquashfs``.
|
|
13
|
+
- Remove a previously created ``.desktop`` entry.
|
|
14
|
+
- List all AppImages currently managed by KAppMan (identified by the
|
|
15
|
+
``X-KAppMan-Source`` key injected into every entry we create).
|
|
16
|
+
|
|
17
|
+
All public functions raise :class:`FileNotFoundError` when given a path that
|
|
18
|
+
does not exist and return plain dicts or booleans so that callers (GUI,
|
|
19
|
+
watcher, CLI) can present output in whatever way is appropriate for their
|
|
20
|
+
context.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
import os
|
|
27
|
+
import shutil
|
|
28
|
+
import stat
|
|
29
|
+
import subprocess
|
|
30
|
+
import tempfile
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
APPLICATIONS_DIR = Path.home() / ".local" / "share" / "applications"
|
|
36
|
+
ICONS_DIR = Path.home() / ".local" / "share" / "icons" / "kappman"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ensure_dirs() -> None:
|
|
40
|
+
APPLICATIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
ICONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _desktop_path(app_name: str) -> Path:
|
|
45
|
+
return APPLICATIONS_DIR / f"{app_name}.desktop"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _sanitize_name(file_path: Path) -> str:
|
|
49
|
+
"""Return a display name by stripping the ``.AppImage`` / ``.appimage`` suffix."""
|
|
50
|
+
name = file_path.name
|
|
51
|
+
for suffix in (".AppImage", ".appimage"):
|
|
52
|
+
if name.endswith(suffix):
|
|
53
|
+
return name[: -len(suffix)]
|
|
54
|
+
return name
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _extract_icon(appimage_path: Path, app_name: str) -> Path | None:
|
|
58
|
+
"""Attempt to extract an icon from *appimage_path* using ``unsquashfs``.
|
|
59
|
+
|
|
60
|
+
The extraction is best-effort. If ``unsquashfs`` is not installed, or if
|
|
61
|
+
the AppImage does not contain a recognisable icon, ``None`` is returned and
|
|
62
|
+
the caller falls back to a generic system icon.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
appimage_path:
|
|
67
|
+
Absolute path to the AppImage file.
|
|
68
|
+
app_name:
|
|
69
|
+
Sanitised application name used to name the extracted icon file.
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
Path | None
|
|
74
|
+
Absolute path to the extracted icon, or ``None`` on failure.
|
|
75
|
+
"""
|
|
76
|
+
if not shutil.which("unsquashfs"):
|
|
77
|
+
logger.debug("unsquashfs not found — skipping icon extraction")
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
with tempfile.TemporaryDirectory(prefix="kappman_") as tmpdir:
|
|
82
|
+
subprocess.run(
|
|
83
|
+
[
|
|
84
|
+
"unsquashfs", "-n", "-i",
|
|
85
|
+
"-d", f"{tmpdir}/squash",
|
|
86
|
+
str(appimage_path),
|
|
87
|
+
"*.png", "*.svg", "*.DirIcon",
|
|
88
|
+
],
|
|
89
|
+
capture_output=True,
|
|
90
|
+
timeout=15,
|
|
91
|
+
)
|
|
92
|
+
squash_root = Path(tmpdir) / "squash"
|
|
93
|
+
for pattern in ("*.png", "*.svg", ".DirIcon"):
|
|
94
|
+
candidates = sorted(squash_root.rglob(pattern))
|
|
95
|
+
if candidates:
|
|
96
|
+
src = candidates[0]
|
|
97
|
+
dest = ICONS_DIR / f"{app_name}{src.suffix}"
|
|
98
|
+
shutil.copy2(src, dest)
|
|
99
|
+
logger.info("Extracted icon: %s", dest)
|
|
100
|
+
return dest
|
|
101
|
+
except Exception:
|
|
102
|
+
logger.debug("Icon extraction failed for %s", appimage_path, exc_info=True)
|
|
103
|
+
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def integrate_appimage(file_path: str | Path) -> dict:
|
|
108
|
+
"""Make *file_path* executable and register it in the application menu.
|
|
109
|
+
|
|
110
|
+
Steps performed:
|
|
111
|
+
|
|
112
|
+
1. Resolve the absolute path and verify the file exists.
|
|
113
|
+
2. Set the executable bit (equivalent to ``chmod +x``).
|
|
114
|
+
3. Attempt icon extraction via :func:`_extract_icon`.
|
|
115
|
+
4. Write a ``.desktop`` entry to :data:`APPLICATIONS_DIR`.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
file_path:
|
|
120
|
+
Path to the AppImage, accepts ``~`` expansion.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
dict
|
|
125
|
+
A dict with keys ``app_name``, ``exec_path``, ``desktop_path``, and
|
|
126
|
+
``icon_path`` (``None`` if no icon could be extracted).
|
|
127
|
+
|
|
128
|
+
Raises
|
|
129
|
+
------
|
|
130
|
+
FileNotFoundError
|
|
131
|
+
If *file_path* does not exist.
|
|
132
|
+
"""
|
|
133
|
+
_ensure_dirs()
|
|
134
|
+
|
|
135
|
+
path = Path(os.path.abspath(os.path.expanduser(file_path)))
|
|
136
|
+
if not path.exists():
|
|
137
|
+
raise FileNotFoundError(f"AppImage not found: {path}")
|
|
138
|
+
|
|
139
|
+
current_mode = path.stat().st_mode
|
|
140
|
+
path.chmod(current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
141
|
+
logger.info("Made executable: %s", path)
|
|
142
|
+
|
|
143
|
+
app_name = _sanitize_name(path)
|
|
144
|
+
icon_path = _extract_icon(path, app_name)
|
|
145
|
+
icon_value = str(icon_path) if icon_path else "application-x-executable"
|
|
146
|
+
|
|
147
|
+
desktop_content = (
|
|
148
|
+
"[Desktop Entry]\n"
|
|
149
|
+
f"Name={app_name}\n"
|
|
150
|
+
f"Exec={path}\n"
|
|
151
|
+
f"Icon={icon_value}\n"
|
|
152
|
+
"Type=Application\n"
|
|
153
|
+
"Categories=Utility;\n"
|
|
154
|
+
"Terminal=false\n"
|
|
155
|
+
"StartupNotify=true\n"
|
|
156
|
+
f"Comment=AppImage managed by KAppMan\n"
|
|
157
|
+
f"X-KAppMan-Source={path}\n"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
dp = _desktop_path(app_name)
|
|
161
|
+
dp.write_text(desktop_content, encoding="utf-8")
|
|
162
|
+
logger.info("Desktop entry written: %s", dp)
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
"app_name": app_name,
|
|
166
|
+
"exec_path": str(path),
|
|
167
|
+
"desktop_path": str(dp),
|
|
168
|
+
"icon_path": str(icon_path) if icon_path else None,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def remove_appimage(file_path: str | Path) -> bool:
|
|
173
|
+
"""Remove the ``.desktop`` entry associated with *file_path*.
|
|
174
|
+
|
|
175
|
+
The AppImage file itself is **not** deleted.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
file_path:
|
|
180
|
+
Path to the AppImage whose desktop entry should be removed.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
bool
|
|
185
|
+
``True`` if a desktop entry was found and deleted; ``False`` if no
|
|
186
|
+
matching entry existed.
|
|
187
|
+
"""
|
|
188
|
+
path = Path(os.path.abspath(os.path.expanduser(file_path)))
|
|
189
|
+
app_name = _sanitize_name(path)
|
|
190
|
+
dp = _desktop_path(app_name)
|
|
191
|
+
|
|
192
|
+
if not dp.exists():
|
|
193
|
+
logger.warning("Desktop entry not found for %s", app_name)
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
dp.unlink()
|
|
197
|
+
logger.info("Removed desktop entry: %s", dp)
|
|
198
|
+
|
|
199
|
+
for suffix in (".png", ".svg", ""):
|
|
200
|
+
icon = ICONS_DIR / f"{app_name}{suffix}"
|
|
201
|
+
if icon.exists():
|
|
202
|
+
icon.unlink()
|
|
203
|
+
logger.info("Removed icon: %s", icon)
|
|
204
|
+
break
|
|
205
|
+
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def list_integrated() -> list[dict]:
|
|
210
|
+
"""Return a list of all AppImages currently managed by KAppMan.
|
|
211
|
+
|
|
212
|
+
Only ``.desktop`` files that contain the ``X-KAppMan-Source`` marker key
|
|
213
|
+
are included. This prevents KAppMan from claiming ownership of desktop
|
|
214
|
+
entries created by other tools.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
list[dict]
|
|
219
|
+
Each item has keys ``app_name``, ``exec_path``, and ``desktop_path``.
|
|
220
|
+
"""
|
|
221
|
+
if not APPLICATIONS_DIR.exists():
|
|
222
|
+
return []
|
|
223
|
+
|
|
224
|
+
results = []
|
|
225
|
+
for desktop_file in sorted(APPLICATIONS_DIR.glob("*.desktop")):
|
|
226
|
+
content = desktop_file.read_text(encoding="utf-8", errors="ignore")
|
|
227
|
+
if "X-KAppMan-Source=" not in content:
|
|
228
|
+
continue
|
|
229
|
+
exec_path = ""
|
|
230
|
+
for line in content.splitlines():
|
|
231
|
+
if line.startswith("X-KAppMan-Source="):
|
|
232
|
+
exec_path = line.split("=", 1)[1]
|
|
233
|
+
break
|
|
234
|
+
results.append({
|
|
235
|
+
"app_name": desktop_file.stem,
|
|
236
|
+
"exec_path": exec_path,
|
|
237
|
+
"desktop_path": str(desktop_file),
|
|
238
|
+
})
|
|
239
|
+
return results
|