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.
- {vappman-2.2.1 → vappman-2.2.4}/PKG-INFO +8 -6
- {vappman-2.2.1 → vappman-2.2.4}/README.md +4 -4
- {vappman-2.2.1 → vappman-2.2.4}/pyproject.toml +28 -2
- vappman-2.2.4/vappman/Appearance.py +93 -0
- {vappman-2.2.1 → vappman-2.2.4}/vappman/AppimageDoctor.py +15 -4
- {vappman-2.2.1 → vappman-2.2.4}/vappman/Prerequisites.py +20 -7
- {vappman-2.2.1 → vappman-2.2.4}/vappman/__init__.py +1 -1
- {vappman-2.2.1 → vappman-2.2.4}/vappman/main.py +98 -50
- vappman-2.2.1/vappman/Appearance.py +0 -119
- {vappman-2.2.1 → vappman-2.2.4}/LICENSE +0 -0
- {vappman-2.2.1 → vappman-2.2.4}/vappman/AppmanLauncher.py +0 -0
- {vappman-2.2.1 → vappman-2.2.4}/vappman/AppmanVars.py +0 -0
- {vappman-2.2.1 → vappman-2.2.4}/vappman/CommandRunner.py +0 -0
- {vappman-2.2.1 → vappman-2.2.4}/vappman/PersistentState.py +0 -0
- {vappman-2.2.1 → vappman-2.2.4}/vappman/VappmanListCache.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vappman
|
|
3
|
-
Version: 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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
252
|
-
print('\n
|
|
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('
|
|
268
|
+
print(f'{warn_mark()} Core prerequisites satisfied '
|
|
269
|
+
'(optional dependencies had issues).')
|
|
257
270
|
else:
|
|
258
271
|
print('✅ All prerequisites satisfied.')
|
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
f'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
#
|
|
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=
|
|
511
|
-
note=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
817
|
-
|
|
818
|
-
|
|
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
|
|
1042
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|