vappman 2.2.0__tar.gz → 2.2.2__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.
Files changed (31) hide show
  1. {vappman-2.2.0 → vappman-2.2.2}/PKG-INFO +22 -4
  2. {vappman-2.2.0 → vappman-2.2.2}/README.md +10 -0
  3. vappman-2.2.2/pyproject.toml +88 -0
  4. vappman-2.2.2/vappman/Appearance.py +85 -0
  5. {vappman-2.2.0 → vappman-2.2.2}/vappman/AppmanLauncher.py +0 -1
  6. {vappman-2.2.0 → vappman-2.2.2}/vappman/AppmanVars.py +2 -1
  7. {vappman-2.2.0 → vappman-2.2.2}/vappman/PersistentState.py +30 -12
  8. {vappman-2.2.0 → vappman-2.2.2}/vappman/Prerequisites.py +5 -7
  9. {vappman-2.2.0 → vappman-2.2.2}/vappman/VappmanListCache.py +5 -3
  10. vappman-2.2.2/vappman/__init__.py +3 -0
  11. {vappman-2.2.0 → vappman-2.2.2}/vappman/main.py +135 -11
  12. vappman-2.2.0/.github/workflows/tests.yml +0 -25
  13. vappman-2.2.0/.gitignore +0 -7
  14. vappman-2.2.0/images/screen-2024-06-23-v0.7.mkv +0 -0
  15. vappman-2.2.0/images/vappman-2025-12-28.12-48.gif +0 -0
  16. vappman-2.2.0/images/vappman-with-filter.png +0 -0
  17. vappman-2.2.0/pyproject.toml +0 -42
  18. vappman-2.2.0/runner +0 -18
  19. vappman-2.2.0/tests/conftest.py +0 -13
  20. vappman-2.2.0/tests/example_init_order.py +0 -124
  21. vappman-2.2.0/tests/test_abut.py +0 -132
  22. vappman-2.2.0/tests/test_init_order.py +0 -99
  23. vappman-2.2.0/tests/test_install_confirmation.py +0 -393
  24. vappman-2.2.0/tests/test_scope_subtraction.py +0 -104
  25. vappman-2.2.0/tests/test_scoped_keys.py +0 -204
  26. vappman-2.2.0/tests/test_scoped_keys_unit.py +0 -209
  27. vappman-2.2.0/tests/test_selection_anchor.py +0 -157
  28. vappman-2.2.0/vappman/__init__.py +0 -0
  29. {vappman-2.2.0 → vappman-2.2.2}/LICENSE +0 -0
  30. {vappman-2.2.0 → vappman-2.2.2}/vappman/AppimageDoctor.py +0 -0
  31. {vappman-2.2.0 → vappman-2.2.2}/vappman/CommandRunner.py +0 -0
@@ -1,25 +1,33 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: vappman
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: A visual wrapper for appman
5
5
  Keywords: app,installer,manager,appimages
6
6
  Author-email: Joe Defen <joedef@google.com>
7
7
  Requires-Python: >=3.10
8
8
  Description-Content-Type: text/markdown
9
+ License-Expression: MIT
9
10
  Classifier: Programming Language :: Python :: 3
10
11
  Classifier: Programming Language :: Python :: 3 :: Only
11
12
  Classifier: Programming Language :: Python :: 3.10
12
13
  Classifier: Programming Language :: Python :: 3.11
13
14
  Classifier: Programming Language :: Python :: 3.12
14
15
  Classifier: Programming Language :: Python :: 3.13
15
- Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Operating System :: POSIX :: Linux
17
17
  License-File: LICENSE
18
- Requires-Dist: console-window == 2.1.3
18
+ Requires-Dist: console-window == 2.2.2
19
+ Requires-Dist: pytest>=7 ; extra == "dev"
20
+ Requires-Dist: pytest-cov>=5 ; extra == "dev"
21
+ Requires-Dist: ruff>=0.6 ; extra == "dev"
22
+ Requires-Dist: build>=1.2 ; extra == "dev"
23
+ Requires-Dist: twine>=7 ; extra == "dev"
24
+ Requires-Dist: packaging>=26 ; extra == "dev"
19
25
  Requires-Dist: pytest>=7.0 ; extra == "test"
20
26
  Project-URL: Bug Tracker, https://github.com/joedefen/vappman/issues
21
27
  Project-URL: Homepage, https://github.com/joedefen/vappman
28
+ Provides-Extra: dev
22
29
  Provides-Extra: test
30
+ Import-Name: vappman
23
31
 
24
32
  ## vappman
25
33
  `vappman` presents a "visual" (or TUI) interface to `appman/am`.
@@ -117,6 +125,15 @@ Run `vappman` from the command line. You will see a screen similar to this:
117
125
  * `﹫` - Database indicator (in multi-database view)
118
126
  * `>` - Currently selected app
119
127
 
128
+ Colour reinforces these symbols, never replaces them: installed rows are
129
+ tinted with the `HOTSWAP` role and the inline install/remove prompts with
130
+ `DANGER`. On a terminal without colour the roles fall back to bold/reverse
131
+ attributes, so every state remains readable.
132
+
133
+ Press `T` to open the theme picker; `t` there cycles through the available
134
+ themes and `mono` (no colour at all). The choice is remembered for the next
135
+ run. `--theme NAME` and `--no-color` override it for a single run.
136
+
120
137
  #### Global Keys (Top Line)
121
138
 
122
139
  * `m` - Switch mode if using `am`, toggling between SYSTEM and USER mode
@@ -132,6 +149,7 @@ Run `vappman` from the command line. You will see a screen similar to this:
132
149
  * `_` - Toggle fancy header mode (Underline/Reverse/Off)
133
150
  * `*` - Toggle demo mode
134
151
  * `#` - Change number of backups to keep (2, 1, or -1 for infinite)
152
+ * `T` - Open the colour-theme picker (`t` cycles there; `mono` disables colour)
135
153
 
136
154
  #### Navigation Keys
137
155
 
@@ -94,6 +94,15 @@ Run `vappman` from the command line. You will see a screen similar to this:
94
94
  * `﹫` - Database indicator (in multi-database view)
95
95
  * `>` - Currently selected app
96
96
 
97
+ Colour reinforces these symbols, never replaces them: installed rows are
98
+ tinted with the `HOTSWAP` role and the inline install/remove prompts with
99
+ `DANGER`. On a terminal without colour the roles fall back to bold/reverse
100
+ attributes, so every state remains readable.
101
+
102
+ Press `T` to open the theme picker; `t` there cycles through the available
103
+ themes and `mono` (no colour at all). The choice is remembered for the next
104
+ run. `--theme NAME` and `--no-color` override it for a single run.
105
+
97
106
  #### Global Keys (Top Line)
98
107
 
99
108
  * `m` - Switch mode if using `am`, toggling between SYSTEM and USER mode
@@ -109,6 +118,7 @@ Run `vappman` from the command line. You will see a screen similar to this:
109
118
  * `_` - Toggle fancy header mode (Underline/Reverse/Off)
110
119
  * `*` - Toggle demo mode
111
120
  * `#` - Change number of backups to keep (2, 1, or -1 for infinite)
121
+ * `T` - Open the colour-theme picker (`t` cycles there; `mono` disables colour)
112
122
 
113
123
  #### Navigation Keys
114
124
 
@@ -0,0 +1,88 @@
1
+ [build-system]
2
+ # 3.12 is the first flit_core that understands the PEP 639 license expression
3
+ # and `license-files` declared below; older backends reject them.
4
+ requires = ["flit_core>=3.12"]
5
+ build-backend = "flit_core.buildapi"
6
+
7
+ [project]
8
+ name = "vappman"
9
+ # `vappman/__init__.py:__version__` is the single source of truth. Flit reads
10
+ # it at build time, so `bless` can rewrite a throwaway X.Y.Z.99.N in a copy
11
+ # without touching the tree.
12
+ dynamic = ["version"]
13
+ description = "A visual wrapper for appman"
14
+ authors = [
15
+ { name = "Joe Defen", email = "joedef@google.com" }
16
+ ]
17
+ readme = "README.md"
18
+ # SPDX expression (PEP 639) rather than the deprecated table form: this is what
19
+ # puts `License-Expression: MIT` in the core metadata, which is also why the
20
+ # legacy `License :: ...` classifier below must not come back.
21
+ license = "MIT"
22
+ license-files = ["LICENSE"]
23
+ requires-python = ">=3.10"
24
+ keywords = ["app", "installer", "manager", "appimages"]
25
+ classifiers = [
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3 :: Only",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Operating System :: POSIX :: Linux"
33
+ ]
34
+ dependencies = [
35
+ 'console-window == 2.2.2',
36
+ ]
37
+
38
+ [project.urls]
39
+ "Homepage" = "https://github.com/joedefen/vappman"
40
+ "Bug Tracker" = "https://github.com/joedefen/vappman/issues"
41
+
42
+ [project.scripts]
43
+ vappman = "vappman.main:main"
44
+
45
+ [project.optional-dependencies]
46
+ test = ["pytest>=7.0"]
47
+ # The release gate (`./bless`) runs ruff, compileall and pytest with coverage,
48
+ # then builds, inspects, `twine check`s and smoke-installs the artifacts. Those
49
+ # tools must live in the interpreter that runs bless, and twine/packaging must
50
+ # be new enough to validate Metadata 2.5 from flit_core 3.12.
51
+ dev = [
52
+ "pytest>=7",
53
+ "pytest-cov>=5",
54
+ "ruff>=0.6",
55
+ "build>=1.2",
56
+ "twine>=7",
57
+ "packaging>=26",
58
+ ]
59
+
60
+ [tool.pytest.ini_options]
61
+ testpaths = ["tests"]
62
+ addopts = "-ra"
63
+
64
+ [tool.coverage.run]
65
+ source = ["vappman"]
66
+
67
+ [tool.coverage.report]
68
+ # Set just below the measured number when the shared gate was adopted. Raise
69
+ # it as the suite grows; never lower it to make a commit pass.
70
+ fail_under = 35
71
+
72
+ [tool.bless]
73
+ # The default is the pair, written out here so the branch model is visible
74
+ # where the rest of this project's identity lives. `publish` aligns `dev`
75
+ # with `main` before tagging, and pushes both.
76
+ branches = ["main", "dev"]
77
+
78
+ [tool.ruff]
79
+ # Keep the lint gate identical on every machine: no global/user config can
80
+ # change which rules the gate enforces. The selection below is deliberately
81
+ # the conservative, version-stable core; a stricter ratchet can be adopted
82
+ # later without changing bless.
83
+ target-version = "py310"
84
+ line-length = 100
85
+ extend-exclude = ["build", "dist", ".venv"]
86
+
87
+ [tool.ruff.lint]
88
+ select = ["E4", "E7", "E9", "F"]
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """The appearance preferences for vappman.
4
+
5
+ Colour is decoration, never information. Every role degrades to an attribute
6
+ (or to plain text) when the terminal has no colour, so the installed markers
7
+ (``✔``/``🔒``), the ``S``/``U`` flags and the ``⚠️`` prompts always remain
8
+ readable on a monochrome terminal.
9
+
10
+ The role ids are ``console_window.Theme``'s semantic pairs rather than literal
11
+ colour numbers. console-window installs those pairs at startup and re-installs
12
+ them whenever a theme is applied, so changing the active theme recolours the
13
+ whole UI without an edit to any draw code.
14
+
15
+ The role-to-attribute mapping, the process-wide switch and the forgiving lookup
16
+ live in :mod:`console_window.appearance`, and are re-exported here. Keeping one
17
+ switch matters: the library's theme picker paints its swatches through that
18
+ layer, so a vappman-local copy would leave the picker in colour while the rest
19
+ of the UI went mono.
20
+
21
+ ``mono`` is vappman's own no-colour pseudo-theme: console-window's
22
+ ``dark-mono``/``light-mono`` are still colour themes, so the true monochrome
23
+ appearance is "colour off" rather than a library theme. It is part of the
24
+ picker's rotation but is not a library theme.
25
+ """
26
+
27
+ import os
28
+
29
+ from console_window import Theme
30
+ from console_window.appearance import (
31
+ MONO_ROLES, colors_enabled, semantic_attr, set_colors_enabled,
32
+ )
33
+
34
+ # Explicit re-exports: the appearance API is read from this module by vappman.
35
+ __all__ = [
36
+ 'MONO_ROLES', 'MONO_THEME', 'appearance_choices', 'appearance_label',
37
+ 'colors_enabled', 'resolve_ui_prefs', 'semantic_attr',
38
+ 'set_colors_enabled',
39
+ ]
40
+
41
+ # vappman's pseudo-theme: no colour at all, only attributes.
42
+ MONO_THEME = 'mono'
43
+
44
+
45
+ def appearance_choices():
46
+ """Every appearance the picker cycles, in order.
47
+
48
+ console-window's themes come first (in ``Theme.THEMES`` order), then
49
+ vappman's no-colour pseudo-theme, so one cycle always reaches ``mono``.
50
+ """
51
+ return list(Theme.list_all()) + [MONO_THEME]
52
+
53
+
54
+ def appearance_label(theme, colors):
55
+ """The name to show for the live appearance."""
56
+ if not colors:
57
+ return MONO_THEME
58
+ return theme or 'default'
59
+
60
+
61
+ def resolve_ui_prefs(cli_theme=None, no_color=False, saved_theme='',
62
+ saved_colors=True, environ=None):
63
+ """Starting ``(theme, colors)`` from the flag, the environment and the prefs.
64
+
65
+ Precedence, highest first: an explicit ``--theme`` / ``--no-color``, the
66
+ ``NO_COLOR`` convention (any non-empty value disables colour), then the
67
+ remembered choice. An explicit ``--theme`` is a statement of intent, so it
68
+ re-enables colour even when the saved choice was ``mono`` (unless
69
+ ``--no-color`` says otherwise); an unknown theme name falls back to
70
+ console-window's ``default``.
71
+
72
+ ``theme`` is ``''`` for the library default. The special value ``mono``
73
+ forces ``colors`` off and is reported as ``theme=''``: there is no library
74
+ theme to install, because mono is the attribute mapping in this module.
75
+ """
76
+ env = os.environ if environ is None else environ
77
+ if cli_theme:
78
+ if cli_theme == MONO_THEME:
79
+ return '', False
80
+ return Theme.resolve(cli_theme), not no_color
81
+ theme = '' if saved_theme in (None, MONO_THEME) else Theme.resolve(saved_theme)
82
+ colors = bool(saved_colors) and saved_theme != MONO_THEME
83
+ if env.get('NO_COLOR') or no_color:
84
+ colors = False
85
+ return theme, colors
@@ -14,7 +14,6 @@
14
14
  import os
15
15
  import subprocess
16
16
  import shutil
17
- import glob
18
17
  from pathlib import Path
19
18
  from typing import Optional, List
20
19
 
@@ -76,7 +76,8 @@ class AppmanVars:
76
76
  """Locates the user's appman directory via config file."""
77
77
  try:
78
78
  config_file = self._config_base / "appman" / "appman-config"
79
- if not config_file.exists(): return None
79
+ if not config_file.exists():
80
+ return None
80
81
  raw_path = config_file.read_text(encoding='utf-8').strip()
81
82
  path_obj = Path(raw_path)
82
83
  if not path_obj.is_absolute():
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env python3
2
2
  import json
3
- import os
4
3
  from pathlib import Path
5
4
 
6
5
  class PersistentState:
@@ -20,7 +19,18 @@ class PersistentState:
20
19
  self.load()
21
20
 
22
21
  def load(self):
23
- """Loads state from JSON, validating keys and types."""
22
+ """Loads state from JSON, merging it over the defaults.
23
+
24
+ A file written by an older version is missing keys the software has
25
+ since added, and one written by a newer version may carry keys this
26
+ version does not know. Both are normal, not corruption: known keys are
27
+ honoured after a type check, unknown keys are dropped, and missing keys
28
+ keep their defaults. The merged result is written back, so the next run
29
+ sees a complete file (a one-time, in-place migration).
30
+
31
+ Only unreadable JSON, a document that is not an object, or a type
32
+ mismatch for a known key still resets everything to the defaults.
33
+ """
24
34
  if not self._config_file.exists():
25
35
  self.save()
26
36
  return
@@ -29,24 +39,32 @@ class PersistentState:
29
39
  with open(self._config_file, 'r', encoding='utf-8') as f:
30
40
  data = json.load(f)
31
41
 
32
- # Validation logic
33
- # 1. Check if keys match identically
34
- if set(data.keys()) != set(self._defaults.keys()):
35
- raise ValueError("Key sets do not match.")
42
+ if not isinstance(data, dict):
43
+ raise ValueError("State file is not an object.")
36
44
 
37
- # 2. Check if types match the defaults
45
+ # Start from the defaults so keys an older file did not have keep
46
+ # their value; validate only the keys this version actually knows.
47
+ merged = dict(self._defaults)
38
48
  for key, value in data.items():
39
- if type(value) != type(self._defaults[key]):
49
+ if key not in self._defaults:
50
+ continue # a key from a newer version: drop it on save
51
+ if type(value) is not type(self._defaults[key]):
40
52
  raise TypeError(f"Type mismatch for {key}.")
41
-
42
- # If valid, update the object attributes
43
- for key, value in data.items():
44
- setattr(self, key, value)
53
+ merged[key] = value
45
54
 
46
55
  except (json.JSONDecodeError, ValueError, TypeError) as e:
47
56
  print(f"Invalid state file ({e}). Reverting to defaults.")
48
57
  self._reset_to_defaults()
49
58
  self.save()
59
+ return
60
+
61
+ for key, value in merged.items():
62
+ setattr(self, key, value)
63
+
64
+ # Persist the merge only when the file differed from it (old file
65
+ # missing keys, or a file with keys this version drops).
66
+ if merged != data:
67
+ self.save()
50
68
 
51
69
  def _reset_to_defaults(self):
52
70
  """Internal helper to reset attributes to original defaults."""
@@ -10,10 +10,8 @@
10
10
  # pylint: disable=line-too-long,protected-access,invalid-name
11
11
 
12
12
  import sys
13
- import re
14
13
  import shutil
15
14
  import subprocess
16
- from types import SimpleNamespace
17
15
 
18
16
  class Prerequisites:
19
17
  """ Detect / install prereqs """
@@ -63,7 +61,7 @@ class Prerequisites:
63
61
  pm_name, install_cmd_template = self.detect_package_manager()
64
62
 
65
63
  if not pm_name:
66
- print(f'\nERROR: Cannot find supported package manager.')
64
+ print('\nERROR: Cannot find supported package manager.')
67
65
  print(f'Missing dependencies: {", ".join(sorted(missing))}')
68
66
  print('Please install them manually using your system package manager.')
69
67
  return False
@@ -74,7 +72,7 @@ class Prerequisites:
74
72
  print(f'\n⚠️ Missing dependencies: {", ".join(packages)}')
75
73
  print(f'\nDetected package manager: {pm_name}')
76
74
 
77
- response = input(f'\nInstall missing dependencies? [y/N]: ').strip().lower()
75
+ response = input('\nInstall missing dependencies? [y/N]: ').strip().lower()
78
76
 
79
77
  if response not in ('y', 'yes'):
80
78
  print('Installation cancelled.')
@@ -96,7 +94,7 @@ class Prerequisites:
96
94
  print(' - Check for problematic PPAs in /etc/apt/sources.list.d/')
97
95
  print(' - Remove outdated PPAs that block apt operations')
98
96
  return False
99
- print(f'\n✅ Dependencies installed successfully!')
97
+ print('\n✅ Dependencies installed successfully!')
100
98
  return True
101
99
  except Exception as exc:
102
100
  print(f'\n❌ Installation failed: {exc}')
@@ -143,7 +141,7 @@ class Prerequisites:
143
141
  if result.returncode != 0:
144
142
  print(f'\n❌ AM installation failed with exit code {result.returncode}')
145
143
  return False
146
- print(f'\n✅ AM/appman installed successfully!')
144
+ print('\n✅ AM/appman installed successfully!')
147
145
  return True
148
146
  except Exception as exc:
149
147
  print(f'\n❌ AM installation failed: {exc}')
@@ -229,7 +227,7 @@ class Prerequisites:
229
227
  print(f'\n⚠️ Optional dependencies missing: {", ".join(sorted(missing_optional))}')
230
228
  print(' Some features may not work without these packages.')
231
229
 
232
- response = input(f'\nInstall optional dependencies? [y/N]: ').strip().lower()
230
+ response = input('\nInstall optional dependencies? [y/N]: ').strip().lower()
233
231
  if response in ('y', 'yes'):
234
232
  if not self.install_dependencies(packages_needed, skip_prompt=True):
235
233
  optional_install_failed = True
@@ -168,7 +168,8 @@ class AppCacheManager:
168
168
  self.apps, self.apps_by_key, self.dbs = apps, apps_by_key, databases
169
169
 
170
170
  def refresh_background(self):
171
- if self.runner and self.runner.is_running(): return
171
+ if self.runner and self.runner.is_running():
172
+ return
172
173
  os.makedirs(self.config_dir, exist_ok=True)
173
174
  self.runner = CommandRunner([self.binary, "list", "--all"], self.list_file)
174
175
  self.runner.start()
@@ -177,7 +178,8 @@ class AppCacheManager:
177
178
  if not os.path.exists(self.list_file):
178
179
  # print("[Cache] No file found. Fetching initial list...")
179
180
  self.refresh_background()
180
- while self.runner.is_running(): time.sleep(0.1)
181
+ while self.runner.is_running():
182
+ time.sleep(0.1)
181
183
  print('... got initial app list ... continuing up ...')
182
184
  self._parse_now()
183
185
  else:
@@ -210,7 +212,7 @@ if __name__ == "__main__":
210
212
  dup_count = sum(1 for entries in apps.values() if len(entries) > 1)
211
213
  total_entries = sum(len(entries) for entries in apps.values())
212
214
 
213
- print(f"Initial Load:")
215
+ print("Initial Load:")
214
216
  print(f" - {len(apps)} unique app names")
215
217
  print(f" - {total_entries} total entries")
216
218
  print(f" - {dup_count} app names with duplicates")
@@ -0,0 +1,3 @@
1
+ """vappman -- a visual wrapper for appman."""
2
+
3
+ __version__ = "2.2.2"