Skip to content

Commit 0f91f79

Browse files
committed
Added functions for registering synchronized styles.
1 parent c1ecc62 commit 0f91f79

3 files changed

Lines changed: 104 additions & 38 deletions

File tree

cmd2/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,11 @@
5959
get_theme,
6060
register_pt_mapping,
6161
register_synchronized_prefix,
62+
register_synchronized_style,
6263
set_theme,
6364
unregister_pt_mapping,
6465
unregister_synchronized_prefix,
66+
unregister_synchronized_style,
6567
)
6668
from .utils import (
6769
CustomCompletionSettings,
@@ -120,9 +122,11 @@
120122
"get_theme",
121123
"register_pt_mapping",
122124
"register_synchronized_prefix",
125+
"register_synchronized_style",
123126
"set_theme",
124127
"unregister_pt_mapping",
125128
"unregister_synchronized_prefix",
129+
"unregister_synchronized_style",
126130
# Utilities
127131
"categorize",
128132
"CustomCompletionSettings",

cmd2/theme.py

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@
4646
# Maps style names to internal UI component names used by prompt-toolkit.
4747
# This allows developers to use application-specific style names in set_theme()
4848
# while ensuring the underlying prompt-toolkit UI is styled correctly.
49-
# Use register_pt_mapping() to modify it.
49+
# Use register_pt_mapping() and unregister_pt_mapping() to manage these mappings.
50+
#
51+
# Presence in this mapping, even with an empty set of UI component names, flags the
52+
# style for synchronization to the prompt-toolkit theme. Use register_synchronized_style()
53+
# and unregister_synchronized_style() to manage synchronization for styles that lack a
54+
# registered prefix and do not require specific UI component mappings.
5055
_PT_UI_MAP: dict[str, set[str]] = {
5156
Cmd2Style.COMPLETION_MENU: {"completion-menu"},
5257
Cmd2Style.COMPLETION_MENU_COMPLETION: {"completion-menu.completion"},
@@ -58,8 +63,9 @@
5863
},
5964
}
6065

61-
# Only Rich styles starting with one of these prefixes are synchronized to
62-
# the prompt-toolkit theme. Use register_synchronized_prefix() to modify it.
66+
# Rich styles that start with one of these prefixes are automatically
67+
# synchronized to the prompt-toolkit theme. Use register_synchronized_prefix()
68+
# and unregister_synchronized_prefix() to modify this set.
6369
_SYNCHRONIZED_PREFIXES: set[str] = {"cmd2."}
6470

6571

@@ -135,7 +141,7 @@ def _sync_pt_theme() -> None:
135141
# Register the style name as a prompt-toolkit class (accessible via 'class:name')
136142
style_rules.append((name, pt_style_str))
137143

138-
# Add any prompt-toolkit UI component aliases from the map (e.g., 'completion-menu')
144+
# Add any prompt-toolkit UI component names from the map (e.g., 'completion-menu')
139145
if is_mapped_style:
140146
style_rules.extend((pt_name, pt_style_str) for pt_name in _PT_UI_MAP[name])
141147

@@ -159,22 +165,23 @@ def register_pt_mapping(style_name: str, pt_ui_names: str | Iterable[str]) -> No
159165
This enables styling of prompt-toolkit's internal elements (such as the
160166
completion menu) using styles in the application's Rich theme.
161167
168+
Registering a mapping also flags the style for synchronization to the
169+
prompt-toolkit theme, making it accessible via 'class:style_name'.
170+
162171
:param style_name: The style name used in the Rich theme.
163172
:param pt_ui_names: One or more prompt-toolkit UI component names (e.g., 'completion-menu').
164173
"""
165174
if isinstance(pt_ui_names, str):
166175
pt_ui_names = [pt_ui_names]
167176

168-
# Register the style in the map. Presence in this map, even with an empty set,
169-
# is the trigger that flags this style for synchronization to prompt-toolkit.
170-
# This is helpful for styles which do not begin with a registered prefix.
177+
# Register the style in the map.
171178
if style_name not in _PT_UI_MAP:
172179
_PT_UI_MAP[style_name] = set()
173180
changed = True
174181
else:
175182
changed = False
176183

177-
# Add UI aliases, excluding 'style_name' which the sync handles by default.
184+
# Add UI mappings, excluding 'style_name' which the sync handles by default.
178185
original_size = len(_PT_UI_MAP[style_name])
179186
_PT_UI_MAP[style_name].update(n for n in pt_ui_names if n != style_name)
180187

@@ -186,42 +193,64 @@ def register_pt_mapping(style_name: str, pt_ui_names: str | Iterable[str]) -> No
186193
_sync_pt_theme()
187194

188195

189-
def unregister_pt_mapping(style_name: str, pt_ui_names: str | Iterable[str] | None = None) -> None:
196+
def register_synchronized_style(style_name: str) -> None:
197+
"""Register a Rich theme style for synchronization with prompt-toolkit.
198+
199+
This ensures that the style is synchronized to the prompt-toolkit theme
200+
(accessible via 'class:style_name') even if it does not begin with a
201+
registered prefix.
202+
203+
:param style_name: The style name used in the Rich theme.
204+
"""
205+
register_pt_mapping(style_name, [])
206+
207+
208+
def unregister_pt_mapping(style_name: str, pt_ui_names: str | Iterable[str]) -> None:
190209
"""Remove one or more prompt-toolkit UI component mappings.
191210
192-
If pt_ui_names is None, all mappings for the given style_name are removed.
211+
The style itself remains in the synchronization mapping (even if no
212+
UI component mappings remain), ensuring it continues to be synchronized
213+
to the prompt-toolkit theme.
214+
215+
To completely remove a style from synchronization, use
216+
unregister_synchronized_style().
193217
194218
:param style_name: The style name used in the Rich theme.
195-
:param pt_ui_names: Specific UI component(s) to unmap, or None to clear all.
219+
:param pt_ui_names: Specific UI component(s) to unmap.
196220
"""
197221
if style_name not in _PT_UI_MAP:
198222
return
199223

200-
changed = False
201-
202-
if pt_ui_names is None:
203-
del _PT_UI_MAP[style_name]
204-
changed = True
205-
else:
206-
if isinstance(pt_ui_names, str):
207-
pt_ui_names = [pt_ui_names]
208-
209-
original_size = len(_PT_UI_MAP[style_name])
210-
for name in pt_ui_names:
211-
_PT_UI_MAP[style_name].discard(name)
224+
if isinstance(pt_ui_names, str):
225+
pt_ui_names = [pt_ui_names]
212226

213-
changed = len(_PT_UI_MAP[style_name]) < original_size
227+
original_size = len(_PT_UI_MAP[style_name])
228+
for name in pt_ui_names:
229+
_PT_UI_MAP[style_name].discard(name)
214230

215-
# Clean up the key if no mappings remain
216-
if not _PT_UI_MAP[style_name]:
217-
del _PT_UI_MAP[style_name]
218-
changed = True
231+
changed = len(_PT_UI_MAP[style_name]) < original_size
219232

220233
# Trigger a re-sync if the theme is already initialized
221234
if changed and _PT_THEME is not None:
222235
_sync_pt_theme()
223236

224237

238+
def unregister_synchronized_style(style_name: str) -> None:
239+
"""Stop synchronizing a Rich theme style with prompt-toolkit.
240+
241+
This removes the style and all its mappings entirely from the
242+
prompt-toolkit theme synchronization.
243+
244+
:param style_name: The style name to unregister.
245+
"""
246+
if style_name in _PT_UI_MAP:
247+
del _PT_UI_MAP[style_name]
248+
249+
# Trigger a re-sync if the theme is already initialized
250+
if _PT_THEME is not None:
251+
_sync_pt_theme()
252+
253+
225254
def register_synchronized_prefix(prefix: str) -> None:
226255
"""Register a prefix whose styles will be synchronized to the prompt-toolkit theme.
227256

tests/test_theme.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
get_theme,
1616
register_pt_mapping,
1717
register_synchronized_prefix,
18+
register_synchronized_style,
1819
set_theme,
1920
unregister_pt_mapping,
2021
unregister_synchronized_prefix,
22+
unregister_synchronized_style,
2123
)
2224

2325

@@ -99,7 +101,7 @@ def test_pt_theme_is_none() -> None:
99101

100102

101103
def test_register_pt_mapping() -> None:
102-
"""Test style to UI mapping."""
104+
"""Test style registration with UI mapping."""
103105
style_name = "my_custom_scrollbar"
104106
ui_name = "scrollbar"
105107

@@ -147,7 +149,7 @@ def test_register_pt_mapping_existing_style() -> None:
147149

148150

149151
def test_unregister_pt_mapping() -> None:
150-
"""Test unmapping styles from UI components."""
152+
"""Test unregistering UI mappings from styles."""
151153
from prompt_toolkit.styles import DEFAULT_ATTRS
152154

153155
style_name = "custom_scroll"
@@ -166,28 +168,59 @@ def test_unregister_pt_mapping() -> None:
166168
assert pt_theme.get_attrs_for_style_str("class:scroll1") == DEFAULT_ATTRS
167169
assert pt_theme.get_attrs_for_style_str("class:scroll2").color == "ansired"
168170

169-
# Unregister the entire style mapping
170-
unregister_pt_mapping(style_name)
171+
# Unregister the other UI component
172+
unregister_pt_mapping(style_name, "scroll2")
171173
pt_theme = get_pt_theme()
172174
assert pt_theme.get_attrs_for_style_str("class:scroll2") == DEFAULT_ATTRS
173175

174176

175177
def test_unregister_pt_mapping_nonexistent() -> None:
176-
"""Test unregistering a mapping that doesn't exist."""
177-
unregister_pt_mapping("nonexistent_style")
178+
"""Test unregistering a style that doesn't exist."""
179+
unregister_pt_mapping("nonexistent_style", "some_ui")
178180

179181

180-
def test_unregister_pt_mapping_cleans_up_key() -> None:
181-
"""Test that unregistering the last UI component removes the style key."""
182-
style_name = "cleanup_style"
183-
ui_name = "cleanup_ui"
182+
def test_unregister_pt_mapping_preserves_key() -> None:
183+
"""Test that unregistering UI components preserves the style key for synchronization."""
184+
style_name = "preserved_style"
185+
ui_name = "some_ui"
184186
register_pt_mapping(style_name, ui_name)
185187

186188
from cmd2 import theme
187189

188190
assert style_name in theme._PT_UI_MAP
191+
assert ui_name in theme._PT_UI_MAP[style_name]
189192

193+
# Unregister just the UI component
190194
unregister_pt_mapping(style_name, ui_name)
195+
196+
# The style key should still be in the map to trigger synchronization
197+
assert style_name in theme._PT_UI_MAP
198+
assert not theme._PT_UI_MAP[style_name]
199+
200+
201+
def test_register_synchronized_style() -> None:
202+
"""Test that simple registration (no UI mapping) synchronizes to PT."""
203+
style_name = "simple_style"
204+
register_synchronized_style(style_name)
205+
206+
set_theme({style_name: Style(color=Color.RED)})
207+
208+
# It should be available as a class:name
209+
pt_theme = get_pt_theme()
210+
attrs = pt_theme.get_attrs_for_style_str(f"class:{style_name}")
211+
assert attrs.color == "ansired"
212+
213+
214+
def test_unregister_synchronized_style() -> None:
215+
"""Test that unregistering removes the style entirely."""
216+
style_name = "removal_style"
217+
ui_name = "removal_ui"
218+
register_pt_mapping(style_name, ui_name)
219+
220+
from cmd2 import theme
221+
222+
assert style_name in theme._PT_UI_MAP
223+
unregister_synchronized_style(style_name)
191224
assert style_name not in theme._PT_UI_MAP
192225

193226

0 commit comments

Comments
 (0)