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.
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -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
@@ -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,4 @@
1
+ """KAppMan - KDE AppImage Manager package."""
2
+
3
+ __version__ = "0.1.0"
4
+ __app_name__ = "KAppMan"
@@ -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