vappman 2.2.1__tar.gz → 2.2.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vappman
3
- Version: 2.2.1
3
+ Version: 2.2.4
4
4
  Summary: A visual wrapper for appman
5
5
  Keywords: app,installer,manager,appimages
6
6
  Author-email: Joe Defen <joedef@google.com>
@@ -15,14 +15,16 @@ Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
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 ~= 3.0.0
19
19
  Requires-Dist: pytest>=7 ; extra == "dev"
20
20
  Requires-Dist: pytest-cov>=5 ; extra == "dev"
21
21
  Requires-Dist: ruff>=0.6 ; extra == "dev"
22
22
  Requires-Dist: build>=1.2 ; extra == "dev"
23
23
  Requires-Dist: twine>=7 ; extra == "dev"
24
24
  Requires-Dist: packaging>=26 ; extra == "dev"
25
+ Requires-Dist: tomli>=2 ; extra == "dev" and ( python_version < '3.11')
25
26
  Requires-Dist: pytest>=7.0 ; extra == "test"
27
+ Requires-Dist: tomli>=2 ; extra == "test" and ( python_version < '3.11')
26
28
  Project-URL: Bug Tracker, https://github.com/joedefen/vappman/issues
27
29
  Project-URL: Homepage, https://github.com/joedefen/vappman
28
30
  Provides-Extra: dev
@@ -196,7 +198,7 @@ Press `/` to start filtering. The filter supports two modes:
196
198
  * `i` or `ENTER` - Install the app. A prompt appears *below* the entry's rows
197
199
  (so the synopsis stays readable), followed by the install-type menu:
198
200
  ```
199
- ⚠️ Install <app>?
201
+ 🌟 Install <app>?
200
202
  *n:none s:sandbox c:icons b:both y:last ESC
201
203
  ```
202
204
  * `n` - plain install
@@ -221,7 +223,7 @@ Press `/` to start filtering. The filter supports two modes:
221
223
  * `t` - Test the app by running it in a terminal
222
224
 
223
225
  For `r`, `C`, `S` and `b`, a prompt appears below the entry's rows (synopsis and
224
- version/type remain visible), e.g. `⚠️ Remove <app>? Press y or ESC`; `y`
226
+ version/type remain visible), e.g. `🌟 Remove <app>? Press y or ESC`; `y`
225
227
  confirms, `ESC` cancels, and all other keys are ignored until you answer. The
226
228
  confirmed command is run with `-y`, so `am/appman` does not ask again.
227
229
 
@@ -289,7 +291,7 @@ AppImage Compatibility Check (Ubuntu 24.04 LTS)
289
291
  ============================================================
290
292
  ✗ libfuse2: libfuse2 is not installed (required for most AppImages)
291
293
  Fix: sudo apt install libfuse2
292
- ⚠ user namespaces: Unprivileged user namespaces are disabled
294
+ 🌟 user namespaces: Unprivileged user namespaces are disabled
293
295
  ✓ /tmp noexec: OK
294
296
  ✓ AppArmor: OK
295
297
 
@@ -300,7 +302,7 @@ Detailed Information:
300
302
  libfuse2 is not installed (required for most AppImages)
301
303
  Fix: sudo apt install libfuse2
302
304
 
303
- ⚠ user_namespaces:
305
+ 🌟 user_namespaces:
304
306
  Unprivileged user namespaces are disabled (sandboxed AppImages will fail)
305
307
  Suggestion: sudo sysctl -w kernel.unprivileged_userns_clone=1
306
308
  To make permanent: echo "kernel.unprivileged_userns_clone=1" | sudo tee /etc/sysctl.d/00-local-userns.conf
@@ -165,7 +165,7 @@ Press `/` to start filtering. The filter supports two modes:
165
165
  * `i` or `ENTER` - Install the app. A prompt appears *below* the entry's rows
166
166
  (so the synopsis stays readable), followed by the install-type menu:
167
167
  ```
168
- ⚠️ Install <app>?
168
+ 🌟 Install <app>?
169
169
  *n:none s:sandbox c:icons b:both y:last ESC
170
170
  ```
171
171
  * `n` - plain install
@@ -190,7 +190,7 @@ Press `/` to start filtering. The filter supports two modes:
190
190
  * `t` - Test the app by running it in a terminal
191
191
 
192
192
  For `r`, `C`, `S` and `b`, a prompt appears below the entry's rows (synopsis and
193
- version/type remain visible), e.g. `⚠️ Remove <app>? Press y or ESC`; `y`
193
+ version/type remain visible), e.g. `🌟 Remove <app>? Press y or ESC`; `y`
194
194
  confirms, `ESC` cancels, and all other keys are ignored until you answer. The
195
195
  confirmed command is run with `-y`, so `am/appman` does not ask again.
196
196
 
@@ -258,7 +258,7 @@ AppImage Compatibility Check (Ubuntu 24.04 LTS)
258
258
  ============================================================
259
259
  ✗ libfuse2: libfuse2 is not installed (required for most AppImages)
260
260
  Fix: sudo apt install libfuse2
261
- ⚠ user namespaces: Unprivileged user namespaces are disabled
261
+ 🌟 user namespaces: Unprivileged user namespaces are disabled
262
262
  ✓ /tmp noexec: OK
263
263
  ✓ AppArmor: OK
264
264
 
@@ -269,7 +269,7 @@ Detailed Information:
269
269
  libfuse2 is not installed (required for most AppImages)
270
270
  Fix: sudo apt install libfuse2
271
271
 
272
- ⚠ user_namespaces:
272
+ 🌟 user_namespaces:
273
273
  Unprivileged user namespaces are disabled (sandboxed AppImages will fail)
274
274
  Suggestion: sudo sysctl -w kernel.unprivileged_userns_clone=1
275
275
  To make permanent: echo "kernel.unprivileged_userns_clone=1" | sudo tee /etc/sysctl.d/00-local-userns.conf
@@ -32,7 +32,12 @@ classifiers = [
32
32
  "Operating System :: POSIX :: Linux"
33
33
  ]
34
34
  dependencies = [
35
- 'console-window == 2.1.3',
35
+ # 3.0 is the first release whose content model lives on Screen (one drawing
36
+ # model) and whose appearance layer is ``console_window.Appearance``. The
37
+ # compatible-3.0.x spelling keeps the app on the 3.0 line: patch releases
38
+ # are fixes, while 3.1 may add (not break) API and would be a deliberate
39
+ # move.
40
+ 'console-window ~= 3.0.0',
36
41
  ]
37
42
 
38
43
  [project.urls]
@@ -43,7 +48,13 @@ dependencies = [
43
48
  vappman = "vappman.main:main"
44
49
 
45
50
  [project.optional-dependencies]
46
- test = ["pytest>=7.0"]
51
+ # `tomli` backfills `tomllib` on 3.10, where `tests/test_bless.py` and
52
+ # `tests/inspect_dist.py` read pyproject.toml. The vendored `bless` already
53
+ # falls back to it, so the gate and its tests carry one requirement.
54
+ test = [
55
+ "pytest>=7.0",
56
+ "tomli>=2; python_version < '3.11'",
57
+ ]
47
58
  # The release gate (`./bless`) runs ruff, compileall and pytest with coverage,
48
59
  # then builds, inspects, `twine check`s and smoke-installs the artifacts. Those
49
60
  # tools must live in the interpreter that runs bless, and twine/packaging must
@@ -55,12 +66,27 @@ dev = [
55
66
  "build>=1.2",
56
67
  "twine>=7",
57
68
  "packaging>=26",
69
+ "tomli>=2; python_version < '3.11'",
58
70
  ]
59
71
 
60
72
  [tool.pytest.ini_options]
61
73
  testpaths = ["tests"]
62
74
  addopts = "-ra"
63
75
 
76
+ [tool.coverage.run]
77
+ source = ["vappman"]
78
+
79
+ [tool.coverage.report]
80
+ # Set just below the measured number when the shared gate was adopted. Raise
81
+ # it as the suite grows; never lower it to make a commit pass.
82
+ fail_under = 35
83
+
84
+ [tool.bless]
85
+ # The default is the pair, written out here so the branch model is visible
86
+ # where the rest of this project's identity lives. `publish` aligns `dev`
87
+ # with `main` before tagging, and pushes both.
88
+ branches = ["main", "dev"]
89
+
64
90
  [tool.ruff]
65
91
  # Keep the lint gate identical on every machine: no global/user config can
66
92
  # change which rules the gate enforces. The selection below is deliberately
@@ -0,0 +1,93 @@
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` (the ``Appearance`` class, whose module is private
17
+ from 3.0 on), and are re-exported here. Keeping one switch matters: the
18
+ library's theme picker paints its swatches through that layer, so a
19
+ vappman-local copy would leave the picker in colour while the rest of the UI
20
+ went mono.
21
+
22
+ ``mono`` is a real console-window theme in 3.0 (it installs the terminal-default
23
+ background and uses the monochrome role attributes), so vappman no longer owns a
24
+ pseudo-theme of its own. What stays here is the preference *pair* the picker
25
+ persists: a palette name plus the colour switch, which can be off for a named
26
+ palette too (``--no-color``, ``NO_COLOR``).
27
+ """
28
+
29
+ import os
30
+
31
+ from console_window import (
32
+ MONO_ROLES, Theme, colors_enabled, semantic_attr, set_colors_enabled,
33
+ )
34
+
35
+ # Explicit re-exports: the appearance API is read from this module by vappman.
36
+ __all__ = [
37
+ 'MONO_ROLES', 'MONO_THEME', 'appearance_choices', 'appearance_label',
38
+ 'colors_enabled', 'resolve_ui_prefs', 'semantic_attr',
39
+ 'set_colors_enabled',
40
+ ]
41
+
42
+ # vappman's no-colour appearance. Since console-window 3.0 this is also a
43
+ # library theme name, which is deliberate: the picker's rotation, the window's
44
+ # recorded theme and a remembered preference all agree on one spelling.
45
+ MONO_THEME = Theme.MONO
46
+
47
+
48
+ def appearance_choices():
49
+ """Every appearance the picker cycles, in order.
50
+
51
+ console-window's themes come first (in ``Theme.THEMES`` order), with
52
+ vappman's no-colour appearance last, so one cycle always reaches ``mono``.
53
+ ``mono`` is a library theme in 3.0 and is already last in the registry; it
54
+ is moved by name rather than trusted to stay there, so a registry reorder
55
+ cannot put it twice or in the middle.
56
+ """
57
+ others = [name for name in Theme.list_all() if name != MONO_THEME]
58
+ return others + [MONO_THEME]
59
+
60
+
61
+ def appearance_label(theme, colors):
62
+ """The name to show for the live appearance."""
63
+ if not colors:
64
+ return MONO_THEME
65
+ return theme or 'default'
66
+
67
+
68
+ def resolve_ui_prefs(cli_theme=None, no_color=False, saved_theme='',
69
+ saved_colors=True, environ=None):
70
+ """Starting ``(theme, colors)`` from the flag, the environment and the prefs.
71
+
72
+ Precedence, highest first: an explicit ``--theme`` / ``--no-color``, the
73
+ ``NO_COLOR`` convention (any non-empty value disables colour), then the
74
+ remembered choice. An explicit ``--theme`` is a statement of intent, so it
75
+ re-enables colour even when the saved choice was ``mono`` (unless
76
+ ``--no-color`` says otherwise); an unknown theme name falls back to
77
+ console-window's ``default``.
78
+
79
+ ``theme`` is ``''`` for the library default. The special value ``mono``
80
+ forces ``colors`` off and is reported as ``theme=''``: vappman remembers the
81
+ pair (palette, colour switch), and ``''`` + ``colors=False`` is the
82
+ canonical spelling of "no palette, no colour".
83
+ """
84
+ env = os.environ if environ is None else environ
85
+ if cli_theme:
86
+ if cli_theme == MONO_THEME:
87
+ return '', False
88
+ return Theme.resolve(cli_theme), not no_color
89
+ theme = '' if saved_theme in (None, MONO_THEME) else Theme.resolve(saved_theme)
90
+ colors = bool(saved_colors) and saved_theme != MONO_THEME
91
+ if env.get('NO_COLOR') or no_color:
92
+ colors = False
93
+ return theme, colors
@@ -7,6 +7,17 @@ import subprocess
7
7
  from pathlib import Path
8
8
  from typing import List, Dict, Tuple, Optional
9
9
 
10
+ from console_window import Glyphs
11
+
12
+
13
+ def warn_mark():
14
+ """The two-cell 'pay attention' badge for console output.
15
+
16
+ Asked of console-window's mark registry rather than pasted as a literal, so
17
+ a terminal that cannot draw the emoji gets the registered ASCII fallback.
18
+ """
19
+ return Glyphs.mark('star_glow')
20
+
10
21
 
11
22
  class AppImageIssue:
12
23
  """Represents a system compatibility issue for AppImages"""
@@ -243,7 +254,7 @@ def format_report(issues: List[AppImageIssue], distro_id: str, distro_name: str,
243
254
  infos = [i for i in issues if i.severity == 'info']
244
255
 
245
256
  if critical:
246
- lines.append("\n⚠ CRITICAL ISSUES (AppImages will likely not work):")
257
+ lines.append(f"\n{warn_mark()} CRITICAL ISSUES (AppImages will likely not work):")
247
258
  for issue in critical:
248
259
  lines.append(f"\n • {issue.description}")
249
260
  if verbose:
@@ -251,7 +262,7 @@ def format_report(issues: List[AppImageIssue], distro_id: str, distro_name: str,
251
262
  lines.append(f" Fix: {fix}")
252
263
 
253
264
  if warnings:
254
- lines.append("\n⚠ WARNINGS (Some AppImages may fail):")
265
+ lines.append(f"\n{warn_mark()} WARNINGS (Some AppImages may fail):")
255
266
  for issue in warnings:
256
267
  lines.append(f"\n • {issue.description}")
257
268
  if verbose:
@@ -298,7 +309,7 @@ def quick_check() -> Tuple[bool, List[str]]:
298
309
  status_lines.append(f" ✗ {check_name}: {issue.description}")
299
310
  status_lines.append(f" Fix: {fix_cmd}")
300
311
  elif issue.severity == 'warning':
301
- status_lines.append(f" ⚠ {check_name}: {issue.description}")
312
+ status_lines.append(f" {warn_mark()} {check_name}: {issue.description}")
302
313
  else:
303
314
  status_lines.append(f" ℹ {check_name}: {issue.description}")
304
315
  else:
@@ -333,7 +344,7 @@ def main():
333
344
  print(f" Fix: {fix}")
334
345
  elif issue.severity == 'warning':
335
346
  fix = issue.fix_commands.get(distro_id) or issue.fix_commands.get('default', 'No fix available')
336
- print(f"\n⚠ {issue.name}:")
347
+ print(f"\n{warn_mark()} {issue.name}:")
337
348
  print(f" {issue.description}")
338
349
  print(f" Suggestion: {fix}")
339
350
  else:
@@ -13,6 +13,18 @@ import sys
13
13
  import shutil
14
14
  import subprocess
15
15
 
16
+ from console_window import Glyphs
17
+
18
+
19
+ def warn_mark():
20
+ """The two-cell 'pay attention' badge for console messages.
21
+
22
+ Asked of console-window's mark registry rather than pasted as a literal, so
23
+ a terminal that cannot draw the emoji gets the registered ASCII fallback.
24
+ """
25
+ return Glyphs.mark('star_glow')
26
+
27
+
16
28
  class Prerequisites:
17
29
  """ Detect / install prereqs """
18
30
  def __init__(self):
@@ -69,7 +81,7 @@ class Prerequisites:
69
81
  packages = sorted(missing)
70
82
 
71
83
  if not skip_prompt:
72
- print(f'\n⚠️ Missing dependencies: {", ".join(packages)}')
84
+ print(f'\n{warn_mark()} Missing dependencies: {", ".join(packages)}')
73
85
  print(f'\nDetected package manager: {pm_name}')
74
86
 
75
87
  response = input('\nInstall missing dependencies? [y/N]: ').strip().lower()
@@ -107,7 +119,7 @@ class Prerequisites:
107
119
  Returns:
108
120
  bool: True if installation succeeded or user declined, False on error
109
121
  """
110
- print('\n⚠️ AM/appman is not installed.')
122
+ print(f'\n{warn_mark()} AM/appman is not installed.')
111
123
  print('\nAM is a powerful AppImage package manager that allows you to:')
112
124
  print(' • Install and manage 2500+ AppImages, Soarpkgs, and AppBundles')
113
125
  print(' • Update apps with a single command')
@@ -183,7 +195,7 @@ class Prerequisites:
183
195
  has_sudo = shutil.which('sudo') is not None
184
196
  has_doas = shutil.which('doas') is not None
185
197
  if not has_sudo and not has_doas:
186
- print('\n⚠️ WARNING: Neither "sudo" nor "doas" found.')
198
+ print(f'\n{warn_mark()} WARNING: Neither "sudo" nor "doas" found.')
187
199
  print(' System-level operations may not work correctly.')
188
200
 
189
201
  # Check core dependencies
@@ -224,7 +236,7 @@ class Prerequisites:
224
236
  for prog in missing_optional:
225
237
  packages_needed.add(optional_deps[prog])
226
238
 
227
- print(f'\n⚠️ Optional dependencies missing: {", ".join(sorted(missing_optional))}')
239
+ print(f'\n{warn_mark()} Optional dependencies missing: {", ".join(sorted(missing_optional))}')
228
240
  print(' Some features may not work without these packages.')
229
241
 
230
242
  response = input('\nInstall optional dependencies? [y/N]: ').strip().lower()
@@ -248,11 +260,12 @@ class Prerequisites:
248
260
 
249
261
  # After successful installation, exit so user can restart vappman
250
262
  print('\n✅ Installation complete!')
251
- print('\n⚠️ Run "appman -h" or "am -h" and answer setup questions.')
252
- print('\n⚠️ ONLY after init, restart vappman to begin using it.')
263
+ print(f'\n{warn_mark()} Run "appman -h" or "am -h" and answer setup questions.')
264
+ print(f'\n{warn_mark()} ONLY after init, restart vappman to begin using it.')
253
265
  sys.exit(0)
254
266
 
255
267
  if optional_install_failed:
256
- print('⚠️ Core prerequisites satisfied (optional dependencies had issues).')
268
+ print(f'{warn_mark()} Core prerequisites satisfied '
269
+ '(optional dependencies had issues).')
257
270
  else:
258
271
  print('✅ All prerequisites satisfied.')
@@ -1,3 +1,3 @@
1
1
  """vappman -- a visual wrapper for appman."""
2
2
 
3
- __version__ = "2.2.1"
3
+ __version__ = "2.2.4"
@@ -26,7 +26,7 @@ import curses as cs
26
26
  from console_window import (
27
27
  ConsoleWindow, OptionSpinner, ConsoleWindowOpts,
28
28
  Screen, ScreenStack, BasicHelpScreen, Context,
29
- IncrementalSearchBar, InlineConfirmation, Theme
29
+ Glyphs, IncrementalSearchBar, InlineConfirmation, Theme
30
30
  )
31
31
  from . import __version__
32
32
  from .PersistentState import PersistentState
@@ -36,8 +36,8 @@ from .Prerequisites import Prerequisites
36
36
  from .VappmanListCache import AppCacheManager
37
37
  from . import AppimageDoctor
38
38
  from .Appearance import (
39
- MONO_THEME, appearance_choices, appearance_label, resolve_ui_prefs,
40
- semantic_attr, set_colors_enabled,
39
+ MONO_THEME, appearance_choices, appearance_label, colors_enabled,
40
+ resolve_ui_prefs, semantic_attr, set_colors_enabled,
41
41
  )
42
42
 
43
43
  # Screen constants
@@ -56,6 +56,17 @@ INSTALL_OPTIONS = (
56
56
  INSTALL_OPTION_KEYS = {ord(key): flags for key, _, flags in INSTALL_OPTIONS}
57
57
 
58
58
 
59
+ def notice_mark():
60
+ """The two-cell 'pay attention' badge used on prompted rows.
61
+
62
+ Asked of console-window's mark registry rather than pasted as a literal, so
63
+ ASCII mode substitutes the registered fallback (``'S '``) and the prompt
64
+ column keeps its two cells. Called at draw time because the glyph mode is
65
+ process state, decided when the first window is created.
66
+ """
67
+ return Glyphs.mark('star_glow')
68
+
69
+
59
70
  def _install_option_key(install_opts):
60
71
  """Return the menu key matching a persisted `install_opts` value."""
61
72
  flags = frozenset(x for x in (install_opts or '').split(',') if x)
@@ -140,18 +151,16 @@ class HomeScreen(VappmanScreen):
140
151
  rv += ' ╰── ' if corner else ' │ '
141
152
  return rv
142
153
 
143
- win = self.win
144
-
145
154
  # Skip first line (already shown on main line), add up to 'max' continuation lines
146
155
  # Continuation lines are already sized correctly from textwrap's subsequent_indent
147
156
  indent = make_indent(2+fold_offset, False)
148
157
  lines = wrapped_lines[1:1+num]
149
158
  for line in lines[:-1]:
150
159
  # Indent to fold_offset position
151
- win.add_body(indent + line, context=Context("TRANSIENT"))
160
+ self.add_body(indent + line, context=Context("TRANSIENT"))
152
161
  indent = make_indent(2+fold_offset, True)
153
162
  if len(lines) > 0:
154
- win.add_body(indent + wrapped_lines[-1], context=Context("TRANSIENT"))
163
+ self.add_body(indent + wrapped_lines[-1], context=Context("TRANSIENT"))
155
164
 
156
165
 
157
166
  def get_folded_synopsis(self, offset1, offset2, text):
@@ -189,14 +198,14 @@ class HomeScreen(VappmanScreen):
189
198
  pending = app.pending_action
190
199
  if label is None or pending is None:
191
200
  return ''
192
- return f' ⚠️ {label} {pending[1].appname}? Press y or ESC'
201
+ return f' {notice_mark()} {label} {pending[1].appname}? Press y or ESC'
193
202
 
194
203
 
195
204
  def draw_screen(self):
196
205
  """Draw the home screen with app list ⮜–⮞
197
206
  """
198
207
  app = self.app
199
- win = self.win
208
+ win = self.win # window-only services: cols, demo mode, picked context
200
209
  all_dbs = bool(app.opts.database == 'ALL')
201
210
  # While the incremental search bar is active, suppress the selected-line
202
211
  # highlight so the pattern being typed is visually unambiguous.
@@ -209,7 +218,10 @@ class HomeScreen(VappmanScreen):
209
218
  """True when the given row should be shown as the selected line."""
210
219
  return not searching and row_idx == win.pick_pos
211
220
 
212
- win.set_pick_mode(not searching)
221
+ # Content and the pick state live on the screen (console-window 3.0), so
222
+ # the writers below are Screen methods; only the demo indicator and the
223
+ # terminal width are the window's.
224
+ self.set_pick_mode(not searching)
213
225
  win.set_demo_mode(app.opts.demo_mode)
214
226
 
215
227
  title = "APPMAN"
@@ -252,7 +264,7 @@ class HomeScreen(VappmanScreen):
252
264
  # Colour is decoration: installed rows are also marked by ✔/🔒
253
265
  # and the S/U flags, so HOTSWAP only reinforces them.
254
266
  attr = semantic_attr(Theme.HOTSWAP) if status == 'installed' else None
255
- win.add_body(line, attr=attr, context=Context(status, info=ns))
267
+ self.add_body(line, attr=attr, context=Context(status, info=ns))
256
268
 
257
269
  if picked(idx):
258
270
  selected_key = (appname, db)
@@ -267,7 +279,7 @@ class HomeScreen(VappmanScreen):
267
279
  # TRANSIENT (non-pickable) so the engine's auto-abut
268
280
  # reserves the row even when the pick sits at the
269
281
  # viewport bottom.
270
- win.add_body(
282
+ self.add_body(
271
283
  prompt, attr=cs.A_BOLD | semantic_attr(Theme.DANGER),
272
284
  context=Context("TRANSIENT", pickable=False))
273
285
  idx += 1
@@ -293,7 +305,7 @@ class HomeScreen(VappmanScreen):
293
305
  else:
294
306
  line += ns.synopsis
295
307
 
296
- win.add_body(line, context=Context("uninstalled", info=ns))
308
+ self.add_body(line, context=Context("uninstalled", info=ns))
297
309
 
298
310
  if picked(idx):
299
311
  selected_key = (appname, db)
@@ -304,11 +316,11 @@ class HomeScreen(VappmanScreen):
304
316
  # TRANSIENT (non-pickable) so the engine's auto-abut
305
317
  # reserves both lines -- a DECOR row would not be
306
318
  # counted, and the prompt could fall off-screen.
307
- win.add_body(
308
- f' ⚠️ Install {appname}?',
319
+ self.add_body(
320
+ f' {notice_mark()} Install {appname}?',
309
321
  attr=cs.A_BOLD | semantic_attr(Theme.DANGER),
310
322
  context=Context("TRANSIENT", pickable=False))
311
- win.add_body(
323
+ self.add_body(
312
324
  ' ' + install_options_legend(app.opts.install_opts),
313
325
  attr=cs.A_DIM,
314
326
  context=Context("TRANSIENT", pickable=False))
@@ -324,7 +336,7 @@ class HomeScreen(VappmanScreen):
324
336
  #############################################
325
337
  header1 = f'{title} {app.get_keys_line()}'
326
338
  # Use fancy header formatting to highlight keys automatically
327
- win.add_fancy_header(header1, app.opts.fancy_header, line_attr=cs.A_DIM)
339
+ self.add_fancy_header(header1, app.opts.fancy_header, line_attr=cs.A_DIM)
328
340
 
329
341
  # Build dynamic action keys (e.g., " [r]mv [u]pd [b]kup")
330
342
  # Get base header line and combine with dynamic actions
@@ -349,7 +361,7 @@ class HomeScreen(VappmanScreen):
349
361
  else:
350
362
  header2 += ' [i]nstall [a]bout'
351
363
 
352
- win.add_fancy_header(header2, app.opts.fancy_header, line_attr=cs.A_DIM)
364
+ self.add_fancy_header(header2, app.opts.fancy_header, line_attr=cs.A_DIM)
353
365
 
354
366
  def appman_on_installed(self, verb):
355
367
  """ TBD """
@@ -501,21 +513,39 @@ class VappmanHelpScreen(BasicHelpScreen):
501
513
  self.app.ss.push(THEMES_ST, self.app.win.pick_pos)
502
514
 
503
515
 
504
- # console-window's generic picker draws the swatches and the hint line; only
505
- # the cycle is overridden so vappman's 'mono' pseudo-theme is part of the
506
- # rotation and the process-wide colour switch follows it. Keeping the generic
507
- # draw means the preview is console-window's role set, not a vappman copy.
516
+ # Key that cycles the appearance; the picker's hint line advertises it.
517
+ THEME_CYCLE_KEY = 't'
518
+
519
+ # One dim line under the swatches. It must not explain mono: a picker entry
520
+ # named 'mono' with a monochrome preview explains itself, and the sentence
521
+ # showed under every colour theme as well.
522
+ THEME_PICKER_NOTE = 'The choice is remembered for the next run.'
523
+
524
+ # console-window paints the swatches through its appearance layer, so they
525
+ # follow the same process-wide colour switch vappman flips for 'mono': with
526
+ # colour off the picker shows the monochrome role hierarchy instead of the
527
+ # previous palette. Only the cycle and the header label are vappman's.
508
528
  _ThemePickerScreen = Theme.create_picker_screen(
509
529
  VappmanScreen,
510
- cycle_key='t',
511
- note='The last entry, mono, uses no colour at all. '
512
- 'The choice is remembered for the next run.',
530
+ cycle_key=THEME_CYCLE_KEY,
531
+ note=THEME_PICKER_NOTE,
513
532
  )
514
533
 
515
534
 
516
535
  class VappmanThemeScreen(_ThemePickerScreen):
517
536
  """Theme picker over console-window's themes plus vappman's 'mono'."""
518
537
 
538
+ def theme_label(self, theme_name):
539
+ """Name 'mono' while colour is off, so header and swatches agree.
540
+
541
+ With colour on the generic label names the installed palette. With
542
+ colour off the swatches are drawn from the monochrome role attributes,
543
+ so naming the palette left underneath would tell a different story.
544
+ """
545
+ if not colors_enabled():
546
+ return MONO_THEME
547
+ return super().theme_label(theme_name)
548
+
519
549
  def spin_theme_ACTION(self):
520
550
  """'t' advances to the next appearance (mono included) and persists it."""
521
551
  self.app.cycle_appearance()
@@ -592,13 +622,13 @@ class Vappman(Prerequisites):
592
622
  win_opts.dialog_abort = True
593
623
  win_opts.ctrl_c_terminates = False
594
624
  win_opts.min_cols_rows = (60, 10)
595
- win_opts.theme = self.appearance_theme or 'default'
625
+ # 'mono' is a real theme in console-window 3.0, so the window is created
626
+ # on the appearance actually in effect instead of being relabelled after
627
+ # construction (the switch alone used to carry the choice).
628
+ win_opts.theme = MONO_THEME if not self.appearance_colors else (
629
+ self.appearance_theme or 'default')
596
630
 
597
631
  self.win = ConsoleWindow(win_opts)
598
- if not self.appearance_colors:
599
- # 'mono' is not a library theme; record it on the window for the
600
- # picker's header while the installed palette stays at the default.
601
- self.win.theme = MONO_THEME
602
632
 
603
633
  # Initialize screens and screen stack
604
634
  self.screens = {
@@ -608,10 +638,13 @@ class Vappman(Prerequisites):
608
638
  }
609
639
  self.ss = ScreenStack(self.win, None, SCREENS, self.screens)
610
640
 
611
- spin = self.spin = OptionSpinner(stack=self.ss)
641
+ # console-window 3.0 registers ``quit`` (q, ^C) and ``escape``
642
+ # (ESC, ^J, ENTER) itself, before anything else, so the quit key set is
643
+ # a constructor argument rather than a second registration of the same
644
+ # name. 'x' stays an alternative quit key; ^C is delivered as a key
645
+ # because ctrl_c_terminates is False above.
646
+ spin = self.spin = OptionSpinner(stack=self.ss, quit_keys='qx\x03')
612
647
  self.opts = spin.default_obj
613
- spin.add_key('quit', 'q,x - quit program (CTL-C disabled)',
614
- genre='action', keys='qx')
615
648
  spin.add_key('help', '? - enter help screen', genre='action')
616
649
  dbs = sorted(list(self.cache_mgr.dbs))
617
650
  spin.add_key('database', 'd - select app database', vals=['ALL'] + dbs)
@@ -631,11 +664,17 @@ class Vappman(Prerequisites):
631
664
  spin.add_key('update_all', 'U - update ALL installed apps', genre='action')
632
665
  spin.add_key('reinstall_all', 'R - reinstall ALL apps w updated install script', genre='action')
633
666
  spin.add_key('slash', '/ - filter apps by keywords or regex', genre='action')
634
- spin.add_key('escape_filter', 'ESC - clear filter and jump to top', genre='action', keys=27)
667
+ # ESC is reserved by the built-in 'escape' action; claiming it on HOME
668
+ # only is an explicit, per-screen override, which is what clears a filter
669
+ # without leaving the screen.
670
+ spin.add_key('escape_filter', 'ESC - clear filter and jump to top',
671
+ genre='action', keys=27, scope=HOME_ST)
635
672
 
636
673
  spin.add_key('install', 'i - install uninstalled app', genre='action')
674
+ # ENTER/^J also belong to the built-in 'escape' action, so the
675
+ # install/uninstall shortcut is an explicit HOME-only override.
637
676
  spin.add_key('default', 'ENTER - install/uninstall app',
638
- genre='action', keys=[cs.KEY_ENTER, 10])
677
+ genre='action', keys=[cs.KEY_ENTER, 10], scope=HOME_ST)
639
678
  spin.add_key('remove', 'r - remove installed app', genre='action')
640
679
 
641
680
  spin.add_key('sandbox', 'S - Sandbox/Unsandbox app', genre='action')
@@ -745,7 +784,10 @@ class Vappman(Prerequisites):
745
784
  result = subprocess.run(command, stdout=subprocess.PIPE,
746
785
  stderr=subprocess.PIPE, check=False)
747
786
  except Exception as exc:
748
- ConsoleWindow.stop_curses()
787
+ # stop_all() is the permanent teardown in every mode; the bare
788
+ # ConsoleWindow.stop_curses() class call only exists for
789
+ # singleton mode and raises when no window was ever created.
790
+ ConsoleWindow.stop_all()
749
791
  if switch_to_system_mode is True:
750
792
  self.appman.set_system_mode_cheat(False)
751
793
  print(f'FAILED: {command}: {exc}')
@@ -813,9 +855,13 @@ class Vappman(Prerequisites):
813
855
  last_database = self.opts.database
814
856
 
815
857
  while True:
816
- # Get and draw current screen
817
- screen_num = self.ss.curr.num
818
- self.screens[screen_num].draw_screen()
858
+ # The 3.0 loop: clear() adopts the screen on the window (and resets
859
+ # its content), draw_screen() appends the rows, render() paints them.
860
+ # Clearing first is what makes the screen current, so the writers in
861
+ # draw_screen() and the pick state read below belong to it.
862
+ screen = self.screens[self.ss.curr.num]
863
+ screen.clear()
864
+ screen.draw_screen()
819
865
 
820
866
  win.render()
821
867
  key = win.prompt(seconds=self.next_prompt_seconds[0])
@@ -864,8 +910,6 @@ class Vappman(Prerequisites):
864
910
  last_database = self.opts.database
865
911
  self.restore_selection(self.pick_identity)
866
912
 
867
- win.clear()
868
-
869
913
  def _handle_confirmation_key(self, key):
870
914
  """Route one key to the active inline install/remove confirmation."""
871
915
  if self.confirmation.action_type == 'install':
@@ -1038,19 +1082,20 @@ class Vappman(Prerequisites):
1038
1082
  """Make *name* live ('mono' turns colour off) and remember it.
1039
1083
 
1040
1084
  The preference is written immediately so an early exit cannot lose it,
1041
- and the window is told so its own ``theme`` (which the generic picker
1042
- reads for the header) matches the colour table that was installed.
1085
+ and the window is told which theme was installed. ``mono`` is a real
1086
+ console-window theme in 3.0, so the window is given that name: a reader
1087
+ of ``win.theme`` sees the no-colour appearance, and ``Theme.set_active``
1088
+ (called by ``set_theme``) turns the semantic-colour switch off with it.
1089
+ The switch is also set here, because vappman resolves the preference
1090
+ pair before any window exists and a host without ``set_theme`` must
1091
+ still end up monochrome.
1043
1092
  """
1044
1093
  mono = name == MONO_THEME
1045
1094
  self.appearance_colors = not mono
1046
1095
  # '' is the library default; keep the saved file canonical.
1047
1096
  self.appearance_theme = '' if mono or name == 'default' else name
1097
+ self.win.set_theme(MONO_THEME if mono else name)
1048
1098
  set_colors_enabled(not mono)
1049
- self.win.set_theme('default' if mono else name)
1050
- if mono:
1051
- # 'mono' is not a library theme: record it for the picker's header
1052
- # while the installed palette stays at the library default.
1053
- self.win.theme = MONO_THEME
1054
1099
  self.disk_state.theme = self.appearance_theme
1055
1100
  self.disk_state.colors = self.appearance_colors
1056
1101
  self.disk_state.save()
@@ -1191,7 +1236,8 @@ def main():
1191
1236
  print(line)
1192
1237
 
1193
1238
  if has_critical:
1194
- print("\n⚠ Critical issues found! Run 'vappman --check-appimage' for detailed fixes")
1239
+ print(f"\n{notice_mark()} Critical issues found! "
1240
+ "Run 'vappman --check-appimage' for detailed fixes")
1195
1241
  print("Starting TUI in 3 seconds...")
1196
1242
  time.sleep(3)
1197
1243
  print()
@@ -1202,7 +1248,9 @@ def main():
1202
1248
  except KeyboardInterrupt:
1203
1249
  pass
1204
1250
  except Exception as exce:
1205
- ConsoleWindow.stop_curses()
1251
+ # Tear down whatever exists (a failure before the window was created is
1252
+ # common here, and stop_all() tolerates it).
1253
+ ConsoleWindow.stop_all()
1206
1254
  print("exception:", str(exce))
1207
1255
  print(traceback.format_exc())
1208
1256
 
@@ -1,119 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """Semantic colour roles and 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
- ``mono`` is vappman's own no-colour pseudo-theme: console-window's
16
- ``dark-mono``/``light-mono`` are still colour themes, so the true monochrome
17
- appearance lives here as the :data:`MONO_ROLES` mapping. It is part of the
18
- picker's rotation but is not a library theme.
19
- """
20
-
21
- import os
22
- import curses
23
-
24
- from console_window import Theme
25
-
26
- # vappman's pseudo-theme: no colour at all, only attributes.
27
- MONO_THEME = 'mono'
28
-
29
- # Role -> attribute used when colour is off. The hierarchy is preserved with
30
- # emphasis instead of hue: destructive prompts are bold+reverse, informational
31
- # rows carry nothing (their ✔/🔒 and S/U markers already distinguish them).
32
- MONO_ROLES = {
33
- Theme.DANGER: curses.A_BOLD | curses.A_REVERSE,
34
- Theme.ERROR: curses.A_BOLD | curses.A_REVERSE,
35
- Theme.WARNING: curses.A_BOLD,
36
- Theme.SUCCESS: curses.A_BOLD,
37
- Theme.EMPHASIS: curses.A_BOLD,
38
- Theme.INFO: curses.A_NORMAL,
39
- Theme.PROGRESS: curses.A_NORMAL,
40
- Theme.OLD_SUCCESS: curses.A_NORMAL,
41
- Theme.HOTSWAP: curses.A_NORMAL,
42
- }
43
-
44
- # Process-wide switch, seeded at startup by --no-color / NO_COLOR / mono. One
45
- # terminal, so one decision.
46
- _COLORS_ENABLED = True
47
-
48
-
49
- def set_colors_enabled(enabled):
50
- """Turn semantic colour on or off for the whole process."""
51
- global _COLORS_ENABLED # pylint: disable=global-statement
52
- _COLORS_ENABLED = bool(enabled)
53
-
54
-
55
- def colors_enabled():
56
- """True while semantic roles resolve to colour pairs rather than attributes."""
57
- return _COLORS_ENABLED
58
-
59
-
60
- def semantic_attr(pair_id):
61
- """Attribute for a console-window ``Theme`` role, or plain when no colour.
62
-
63
- With colour off the role becomes an attribute from :data:`MONO_ROLES`, so
64
- the UI keeps its emphasis hierarchy on a monochrome terminal. Otherwise
65
- both ``curses.has_colors()`` and ``curses.color_pair()`` can raise
66
- ``curses.error`` before ``initscr()`` (and a failed lookup must never abort
67
- a redraw), so any failure degrades to plain text.
68
- """
69
- if not _COLORS_ENABLED:
70
- return MONO_ROLES.get(pair_id, curses.A_NORMAL)
71
- try:
72
- if not curses.has_colors():
73
- return 0
74
- return curses.color_pair(pair_id)
75
- except curses.error:
76
- return 0
77
-
78
-
79
- def appearance_choices():
80
- """Every appearance the picker cycles, in order.
81
-
82
- console-window's themes come first (in ``Theme.THEMES`` order), then
83
- vappman's no-colour pseudo-theme, so one cycle always reaches ``mono``.
84
- """
85
- return list(Theme.list_all()) + [MONO_THEME]
86
-
87
-
88
- def appearance_label(theme, colors):
89
- """The name to show for the live appearance."""
90
- if not colors:
91
- return MONO_THEME
92
- return theme or 'default'
93
-
94
-
95
- def resolve_ui_prefs(cli_theme=None, no_color=False, saved_theme='',
96
- saved_colors=True, environ=None):
97
- """Starting ``(theme, colors)`` from the flag, the environment and the prefs.
98
-
99
- Precedence, highest first: an explicit ``--theme`` / ``--no-color``, the
100
- ``NO_COLOR`` convention (any non-empty value disables colour), then the
101
- remembered choice. An explicit ``--theme`` is a statement of intent, so it
102
- re-enables colour even when the saved choice was ``mono`` (unless
103
- ``--no-color`` says otherwise); an unknown theme name falls back to
104
- console-window's ``default``.
105
-
106
- ``theme`` is ``''`` for the library default. The special value ``mono``
107
- forces ``colors`` off and is reported as ``theme=''``: there is no library
108
- theme to install, because mono is the attribute mapping in this module.
109
- """
110
- env = os.environ if environ is None else environ
111
- if cli_theme:
112
- if cli_theme == MONO_THEME:
113
- return '', False
114
- return Theme.resolve(cli_theme), not no_color
115
- theme = '' if saved_theme in (None, MONO_THEME) else Theme.resolve(saved_theme)
116
- colors = bool(saved_colors) and saved_theme != MONO_THEME
117
- if env.get('NO_COLOR') or no_color:
118
- colors = False
119
- return theme, colors
File without changes
File without changes