vappman 2.2.2__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.2
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.2.2
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.2.2',
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,6 +66,7 @@ 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]
@@ -4,7 +4,7 @@
4
4
 
5
5
  Colour is decoration, never information. Every role degrades to an attribute
6
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
7
+ (``✔``/``🔒``), the ``S``/``U`` flags and the ``🌟`` prompts always remain
8
8
  readable on a monochrome terminal.
9
9
 
10
10
  The role ids are ``console_window.Theme``'s semantic pairs rather than literal
@@ -13,22 +13,23 @@ them whenever a theme is applied, so changing the active theme recolours the
13
13
  whole UI without an edit to any draw code.
14
14
 
15
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.
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``).
25
27
  """
26
28
 
27
29
  import os
28
30
 
29
- from console_window import Theme
30
- from console_window.appearance import (
31
- MONO_ROLES, colors_enabled, semantic_attr, set_colors_enabled,
31
+ from console_window import (
32
+ MONO_ROLES, Theme, colors_enabled, semantic_attr, set_colors_enabled,
32
33
  )
33
34
 
34
35
  # Explicit re-exports: the appearance API is read from this module by vappman.
@@ -38,17 +39,23 @@ __all__ = [
38
39
  'set_colors_enabled',
39
40
  ]
40
41
 
41
- # vappman's pseudo-theme: no colour at all, only attributes.
42
- MONO_THEME = 'mono'
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
43
46
 
44
47
 
45
48
  def appearance_choices():
46
49
  """Every appearance the picker cycles, in order.
47
50
 
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``.
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.
50
56
  """
51
- return list(Theme.list_all()) + [MONO_THEME]
57
+ others = [name for name in Theme.list_all() if name != MONO_THEME]
58
+ return others + [MONO_THEME]
52
59
 
53
60
 
54
61
  def appearance_label(theme, colors):
@@ -70,8 +77,9 @@ def resolve_ui_prefs(cli_theme=None, no_color=False, saved_theme='',
70
77
  console-window's ``default``.
71
78
 
72
79
  ``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.
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".
75
83
  """
76
84
  env = os.environ if environ is None else environ
77
85
  if cli_theme:
@@ -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.2"
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
@@ -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 """
@@ -610,13 +622,13 @@ class Vappman(Prerequisites):
610
622
  win_opts.dialog_abort = True
611
623
  win_opts.ctrl_c_terminates = False
612
624
  win_opts.min_cols_rows = (60, 10)
613
- 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')
614
630
 
615
631
  self.win = ConsoleWindow(win_opts)
616
- if not self.appearance_colors:
617
- # 'mono' is not a library theme: the installed palette stays at the
618
- # default, but the window names the appearance actually in effect.
619
- self.win.theme = MONO_THEME
620
632
 
621
633
  # Initialize screens and screen stack
622
634
  self.screens = {
@@ -626,10 +638,13 @@ class Vappman(Prerequisites):
626
638
  }
627
639
  self.ss = ScreenStack(self.win, None, SCREENS, self.screens)
628
640
 
629
- 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')
630
647
  self.opts = spin.default_obj
631
- spin.add_key('quit', 'q,x - quit program (CTL-C disabled)',
632
- genre='action', keys='qx')
633
648
  spin.add_key('help', '? - enter help screen', genre='action')
634
649
  dbs = sorted(list(self.cache_mgr.dbs))
635
650
  spin.add_key('database', 'd - select app database', vals=['ALL'] + dbs)
@@ -649,11 +664,17 @@ class Vappman(Prerequisites):
649
664
  spin.add_key('update_all', 'U - update ALL installed apps', genre='action')
650
665
  spin.add_key('reinstall_all', 'R - reinstall ALL apps w updated install script', genre='action')
651
666
  spin.add_key('slash', '/ - filter apps by keywords or regex', genre='action')
652
- 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)
653
672
 
654
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.
655
676
  spin.add_key('default', 'ENTER - install/uninstall app',
656
- genre='action', keys=[cs.KEY_ENTER, 10])
677
+ genre='action', keys=[cs.KEY_ENTER, 10], scope=HOME_ST)
657
678
  spin.add_key('remove', 'r - remove installed app', genre='action')
658
679
 
659
680
  spin.add_key('sandbox', 'S - Sandbox/Unsandbox app', genre='action')
@@ -763,7 +784,10 @@ class Vappman(Prerequisites):
763
784
  result = subprocess.run(command, stdout=subprocess.PIPE,
764
785
  stderr=subprocess.PIPE, check=False)
765
786
  except Exception as exc:
766
- 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()
767
791
  if switch_to_system_mode is True:
768
792
  self.appman.set_system_mode_cheat(False)
769
793
  print(f'FAILED: {command}: {exc}')
@@ -831,9 +855,13 @@ class Vappman(Prerequisites):
831
855
  last_database = self.opts.database
832
856
 
833
857
  while True:
834
- # Get and draw current screen
835
- screen_num = self.ss.curr.num
836
- 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()
837
865
 
838
866
  win.render()
839
867
  key = win.prompt(seconds=self.next_prompt_seconds[0])
@@ -882,8 +910,6 @@ class Vappman(Prerequisites):
882
910
  last_database = self.opts.database
883
911
  self.restore_selection(self.pick_identity)
884
912
 
885
- win.clear()
886
-
887
913
  def _handle_confirmation_key(self, key):
888
914
  """Route one key to the active inline install/remove confirmation."""
889
915
  if self.confirmation.action_type == 'install':
@@ -1056,20 +1082,20 @@ class Vappman(Prerequisites):
1056
1082
  """Make *name* live ('mono' turns colour off) and remember it.
1057
1083
 
1058
1084
  The preference is written immediately so an early exit cannot lose it,
1059
- and the window is told which palette was installed; ``mono`` keeps its
1060
- own name there so a reader of ``win.theme`` sees the no-colour
1061
- appearance rather than the default palette left underneath.
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.
1062
1092
  """
1063
1093
  mono = name == MONO_THEME
1064
1094
  self.appearance_colors = not mono
1065
1095
  # '' is the library default; keep the saved file canonical.
1066
1096
  self.appearance_theme = '' if mono or name == 'default' else name
1097
+ self.win.set_theme(MONO_THEME if mono else name)
1067
1098
  set_colors_enabled(not mono)
1068
- self.win.set_theme('default' if mono else name)
1069
- if mono:
1070
- # 'mono' is not a library theme: the installed palette stays at the
1071
- # library default, but the window names the appearance in effect.
1072
- self.win.theme = MONO_THEME
1073
1099
  self.disk_state.theme = self.appearance_theme
1074
1100
  self.disk_state.colors = self.appearance_colors
1075
1101
  self.disk_state.save()
@@ -1210,7 +1236,8 @@ def main():
1210
1236
  print(line)
1211
1237
 
1212
1238
  if has_critical:
1213
- 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")
1214
1241
  print("Starting TUI in 3 seconds...")
1215
1242
  time.sleep(3)
1216
1243
  print()
@@ -1221,7 +1248,9 @@ def main():
1221
1248
  except KeyboardInterrupt:
1222
1249
  pass
1223
1250
  except Exception as exce:
1224
- 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()
1225
1254
  print("exception:", str(exce))
1226
1255
  print(traceback.format_exc())
1227
1256
 
File without changes
File without changes