curses-themes is a lightweight theme support library for Python curses applications. It provides professional theming capabilities with zero external dependencies, inspired by the FlossWare curses-java library.
Version: 0.1.0
License: GPL-3.0
Author: FlossWare
pip install curses-themesimport curses
from curses_themes import ThemeManager
def main(stdscr):
# Load and apply a theme
theme = ThemeManager.load('dark')
theme.apply(stdscr)
# Use semantic colors
stdscr.addstr(0, 0, "Success!", curses.color_pair(theme.colors.success))
stdscr.addstr(1, 0, "Error!", curses.color_pair(theme.colors.error))
# Draw themed boxes
theme.draw_box(stdscr, 3, 2, 10, 40, title="My Panel")
stdscr.refresh()
stdscr.getch()
if __name__ == "__main__":
curses.wrapper(main)Abstract base class for creating curses themes. All custom themes must inherit from this class and implement the required methods.
Theme(name: str, description: str = "", author: str = "")Parameters:
name(str): Human-readable theme namedescription(str, optional): Brief description of the theme's appearance or purposeauthor(str, optional): Name of the theme creator
Example:
class MyTheme(Theme):
def __init__(self):
super().__init__(
name="My Theme",
description="A custom theme with blue accents",
author="Your Name"
)def get_color_map(self) -> Dict[str, Tuple[int, int, int]]Required implementation. Returns RGB color definitions for the theme.
Returns:
Dict[str, Tuple[int, int, int]]: Dictionary mapping semantic color names to RGB tuples (0-255)
Required Keys:
background: Default background colorforeground: Default text colorprimary: Main UI color for highlights and focussuccess: Positive feedback and successful operationserror: Error messages and critical warningswarning: Caution messages and non-critical warningsinfo: Informational messages and help textaccent: Secondary highlight color
Example:
def get_color_map(self):
return {
'background': (0, 0, 0), # Black
'foreground': (255, 255, 255), # White
'primary': (0, 120, 215), # Blue
'success': (16, 124, 16), # Green
'error': (232, 17, 35), # Red
'warning': (193, 156, 0), # Yellow
'info': (0, 120, 212), # Blue
'accent': (142, 68, 173), # Purple
}These methods define color pairs for UI components. All methods return ColorPair objects or None if not implemented.
def get_background(self) -> Optional[ColorPair]Returns the background color pair for normal components.
Returns:
ColorPairorNone: Foreground and background colors for normal background
Example:
def get_background(self) -> ColorPair:
return ColorPair((255, 255, 255), (0, 0, 0)) # White on Blackdef get_button(self) -> Optional[ColorPair]Returns the color pair for buttons in normal state.
Returns:
ColorPairorNone: Colors for button rendering
Example:
def get_button(self) -> ColorPair:
return ColorPair((0, 255, 255), (0, 0, 0)) # Cyan on Blackdef get_button_focused(self) -> Optional[ColorPair]Returns the color pair for buttons when focused.
Returns:
ColorPairorNone: Colors for focused button rendering
Example:
def get_button_focused(self) -> ColorPair:
return ColorPair((0, 0, 0), (0, 255, 255)) # Black on Cyan (inverted)def get_text_input(self) -> Optional[ColorPair]Returns the color pair for text input fields.
Returns:
ColorPairorNone: Colors for text input rendering
def get_border(self) -> Optional[ColorPair]Returns the color pair for borders and frames.
Returns:
ColorPairorNone: Colors for border rendering
def get_selection(self) -> Optional[ColorPair]Returns the color pair for selected/highlighted items.
Returns:
ColorPairorNone: Colors for selection rendering
def get_disabled(self) -> Optional[ColorPair]Returns the color pair for disabled components.
Returns:
ColorPairorNone: Colors for disabled component rendering
def get_border_chars(self) -> strReturns border characters for drawing boxes. Override to provide custom border styles.
Returns:
str: String with 8 characters representing: top-left, top, top-right, left, right, bottom-left, bottom, bottom-right
Default: "+-+||+-+" (ASCII box)
Unicode Example: "┌─┐││└─┘" (Unicode box-drawing)
Example:
def get_border_chars(self) -> str:
return "┌─┐││└─┘" # Unicode box charactersdef apply(self, stdscr) -> NoneApplies the theme to a curses screen. Must be called before using theme.colors, theme.components, or theme.draw_box().
Parameters:
stdscr: Curses window object (typically fromcurses.wrapper)
Raises:
RuntimeError: If color initialization fails
Example:
def main(stdscr):
theme = ThemeManager.load('dark')
theme.apply(stdscr)
# Now theme.colors and theme.components are available@property
def colors(self) -> SemanticColorsReturns semantic color pairs for the theme. This is the legacy API maintained for backward compatibility.
Returns:
SemanticColors: Instance with initialized color pairs
Raises:
RuntimeError: Ifapply()has not been called
Example:
theme.apply(stdscr)
stdscr.addstr(0, 0, "Success!", curses.color_pair(theme.colors.success))
stdscr.addstr(1, 0, "Error!", curses.color_pair(theme.colors.error))@property
def components(self) -> ComponentColorsReturns component-based color pairs for the theme. This is the primary API matching curses-java Theme interface.
Returns:
ComponentColors: Instance with initialized color pairs
Raises:
RuntimeError: Ifapply()has not been called
Example:
theme.apply(stdscr)
stdscr.addstr(0, 0, "Button", curses.color_pair(theme.components.button))
stdscr.addstr(1, 0, "Input: ", curses.color_pair(theme.components.text_input))def draw_box(
self,
window,
y: int,
x: int,
height: int,
width: int,
title: str = "",
color_pair: Optional[int] = None
) -> NoneDraws a themed border box on the given window.
Parameters:
window: Curses window to draw ony(int): Top-left Y coordinatex(int): Top-left X coordinateheight(int): Box height in characterswidth(int): Box width in characterstitle(str, optional): Title to display centered in top bordercolor_pair(int, optional): Color pair number to use (defaults to border color)
Raises:
ValueError: If box dimensions are too small (minimum 2x2)
Example:
# Draw a box with title
theme.draw_box(stdscr, 2, 5, 10, 40, title="Settings")
# Draw a box with custom color
theme.draw_box(stdscr, 15, 5, 5, 40,
color_pair=theme.components.button_focused)Singleton manager for theme registration and loading. Provides a central registry for all available themes.
Note: This is a singleton class. All methods are classmethods. Do not instantiate this class.
@classmethod
def register(cls, theme_class: Type[Theme], name: Optional[str] = None) -> NoneRegisters a theme class for use.
Parameters:
theme_class(Type[Theme]): Theme class (subclass of Theme) to registername(str, optional): Custom name. If not provided, uses theme.name from a temporary instance
Raises:
TypeError: If theme_class is not a Theme subclassValueError: If a theme with this name is already registered
Example:
class MyTheme(Theme):
def __init__(self):
super().__init__("My Theme", "A custom theme")
def get_color_map(self):
return {...}
# Register with automatic name (derived from theme.name)
ThemeManager.register(MyTheme)
# Register with custom name
ThemeManager.register(MyTheme, 'my-custom')@classmethod
def load(cls, name: str) -> ThemeLoads a theme by name. Creates a new instance of the requested theme.
Parameters:
name(str): Theme name (case-insensitive, spaces/underscores converted to hyphens)
Returns:
Theme: New Theme instance
Raises:
KeyError: If theme is not registered
Example:
# Load built-in theme
theme = ThemeManager.load('dark')
# Names are normalized (all equivalent)
theme = ThemeManager.load('Dark')
theme = ThemeManager.load('dark')
theme = ThemeManager.load('DARK')
# Spaces and underscores normalized to hyphens
theme = ThemeManager.load('TI 99/4A') # Same as 'ti-99-4a'
theme = ThemeManager.load('my_custom_theme') # Same as 'my-custom-theme'@classmethod
def list_themes(cls) -> Dict[str, Dict[str, str]]Lists all registered themes with metadata.
Returns:
Dict[str, Dict[str, str]]: Dictionary mapping theme names to metadata dictionaries with keys: 'name', 'description', 'author'
Example:
themes = ThemeManager.list_themes()
for name, info in themes.items():
print(f"{name}:")
print(f" Name: {info['name']}")
print(f" Description: {info['description']}")
print(f" Author: {info['author']}")
# Output:
# dark:
# Name: Dark
# Description: Dark theme with light text on dark background
# Author: FlossWare@classmethod
def get_current(cls) -> Optional[Theme]Returns the currently active theme.
Returns:
ThemeorNone: Current Theme instance, or None if no theme has been loaded
Example:
current = ThemeManager.get_current()
if current:
print(f"Current theme: {current.name}")@classmethod
def unregister(cls, name: str) -> NoneUnregisters a theme by name.
Parameters:
name(str): Name of theme to unregister
Raises:
KeyError: If theme is not registered
Example:
ThemeManager.unregister('my-custom-theme')@classmethod
def reset(cls) -> NoneResets the theme manager state. Clears all registered themes and current theme. This is primarily for testing purposes.
Warning: This will unregister all themes, including built-in themes. They will be re-registered on next load() or list_themes() call.
Manages color initialization and terminal capability detection. Handles the complexities of working with different terminal color capabilities (8, 16, or 256 colors).
ColorManager(stdscr)Parameters:
stdscr: Curses window object (typically fromcurses.wrapper)
Raises:
RuntimeError: If the terminal doesn't support colors
Example:
def main(stdscr):
manager = ColorManager(stdscr)
colors, components = manager.initialize_theme(my_theme)
stdscr.addstr(0, 0, "Hello", curses.color_pair(colors.primary))stdscr: The curses window objectcolor_count(int): Number of colors supported (8, 16, or 256)
Important: ColorManager uses class-level state that persists across all instances.
The _next_pair counter and _pair_cache dictionary are class variables, not instance variables. This means:
- Creating multiple ColorManager instances does NOT reset color pairs
- All instances share the same color pair allocation pool
- Color pairs persist until the Python process exits
- The cache ensures identical color combinations reuse the same pair number
This design prevents duplicate color pair allocation and helps avoid exceeding the terminal's COLOR_PAIRS limit.
Example:
def main(stdscr):
# First manager allocates pairs 1-10
mgr1 = ColorManager(stdscr)
semantic1, component1 = mgr1.initialize_theme(theme1)
# Second manager continues from pair 11
# (or reuses cached pairs if colors match)
mgr2 = ColorManager(stdscr)
semantic2, component2 = mgr2.initialize_theme(theme2)
# Both managers share the same pair cache
# Same RGB combinations return the same pair numbersTesting Note: The reset() method exists primarily for test isolation. In production code, you should rarely need to call it.
def initialize_theme(self, theme) -> Tuple[SemanticColors, ComponentColors]Initializes all color pairs for a theme. Converts the theme's RGB color map and component colors to curses color pairs appropriate for the terminal's capabilities.
Parameters:
theme: Theme instance withget_color_map()and component methods
Returns:
Tuple[SemanticColors, ComponentColors]: Initialized color pair numbers
Raises:
ValueError: If color map is missing required keysRuntimeError: If color initialization fails
Example:
manager = ColorManager(stdscr)
semantic_colors, component_colors = manager.initialize_theme(my_theme)
# Use the colors
stdscr.addstr(0, 0, "Text", curses.color_pair(semantic_colors.primary))
stdscr.addstr(1, 0, "Button", curses.color_pair(component_colors.button))When you access theme.colors.primary or theme.components.button, you get an integer (the color pair number), not a curses attribute. This number is an internal identifier that curses uses to track foreground/background color combinations.
theme = ThemeManager.load('dark')
theme.apply(stdscr)
print(type(theme.colors.primary)) # <class 'int'>
print(theme.colors.primary) # 1 (or some other integer)To use a color pair number with curses display functions, you must wrap it with curses.color_pair():
# CORRECT - wrap the number
stdscr.addstr(0, 0, "Text", curses.color_pair(theme.colors.primary))
# WRONG - using raw number
stdscr.addstr(0, 0, "Text", theme.colors.primary) # May display incorrectly or crashAlways use curses.color_pair() when passing colors to curses display functions:
window.addstr(y, x, text, curses.color_pair(num))window.addch(y, x, ch, curses.color_pair(num))window.bkgd(' ', curses.color_pair(num))window.chgat(y, x, n, curses.color_pair(num))window.attrset(curses.color_pair(num))
# All correct usages
stdscr.addstr(0, 0, "Success", curses.color_pair(theme.colors.success))
stdscr.bkgd(' ', curses.color_pair(theme.components.background))
stdscr.chgat(0, 0, 10, curses.color_pair(theme.colors.error))Use the raw integer when passing to library methods that expect pair numbers:
theme.draw_box(window, y, x, h, w, color_pair=num)- No wrapper needed- Storing in data structures for later use
- Comparing color pair values
# Correct - draw_box expects the raw number
theme.draw_box(stdscr, 0, 0, 10, 40, color_pair=theme.components.border)
# Wrong - don't wrap when passing to library methods
theme.draw_box(stdscr, 0, 0, 10, 40,
color_pair=curses.color_pair(theme.components.border)) # WRONG!# WRONG - missing curses.color_pair()
stdscr.addstr(0, 0, "Error", theme.colors.error)
# CORRECT
stdscr.addstr(0, 0, "Error", curses.color_pair(theme.colors.error))# WRONG - draw_box internally applies curses.color_pair()
theme.draw_box(stdscr, 0, 0, 10, 40,
color_pair=curses.color_pair(theme.components.border))
# CORRECT - pass raw number
theme.draw_box(stdscr, 0, 0, 10, 40,
color_pair=theme.components.border)# WRONG - hardcoded, not theme-aware
stdscr.addstr(0, 0, "Text", curses.color_pair(1))
# CORRECT - use theme color names
stdscr.addstr(0, 0, "Text", curses.color_pair(theme.colors.primary))import curses
from curses_themes import ThemeManager
def main(stdscr):
theme = ThemeManager.load('dark')
theme.apply(stdscr)
# Display functions - use curses.color_pair()
stdscr.addstr(0, 0, "Title",
curses.color_pair(theme.colors.primary) | curses.A_BOLD)
stdscr.addstr(2, 0, "Success message",
curses.color_pair(theme.colors.success))
stdscr.addstr(3, 0, "[Button]",
curses.color_pair(theme.components.button))
# Background - use curses.color_pair()
stdscr.bkgd(' ', curses.color_pair(theme.components.background))
# Library methods - use raw number
theme.draw_box(stdscr, 5, 0, 10, 40,
title="Panel",
color_pair=theme.components.border)
stdscr.refresh()
stdscr.getch()
if __name__ == "__main__":
curses.wrapper(main)This API design matches the standard curses model:
- Separation of concerns: Color pair numbers are identifiers,
curses.color_pair()converts them to display attributes - Flexibility: You can combine color pairs with other attributes using bitwise OR:
curses.color_pair(theme.colors.primary) | curses.A_BOLD | curses.A_UNDERLINE
- Performance: Raw numbers can be stored and passed around efficiently
- Compatibility: Works with standard curses API conventions
| Context | Use | Example |
|---|---|---|
addstr(), addch() |
curses.color_pair(num) |
stdscr.addstr(0, 0, "Text", curses.color_pair(theme.colors.primary)) |
bkgd(), chgat() |
curses.color_pair(num) |
stdscr.bkgd(' ', curses.color_pair(theme.components.background)) |
draw_box() |
Raw number | theme.draw_box(stdscr, 0, 0, 10, 40, color_pair=theme.components.border) |
| Storing in variables | Raw number | border_color = theme.components.border |
| Combining with attributes | curses.color_pair(num) |
`curses.color_pair(theme.colors.error) |
def reset(self) -> NoneResets the class-level color pair counter and cache. This affects ALL ColorManager instances.
Warning: This is a class-level operation. Calling reset() on any ColorManager instance will invalidate color pairs created by ALL instances. Previously allocated pair numbers will become invalid.
Use Cases:
- Test isolation (pytest fixtures)
- Never use in production code
Example:
# In pytest conftest.py
@pytest.fixture(autouse=True)
def reset_color_manager():
from curses_themes.colors import ColorManager
yield
ColorManager._next_pair = 1
ColorManager._pair_cache.clear()The following methods handle RGB-to-palette conversion and are used internally by initialize_theme().
def _rgb_to_curses_color(self, r: int, g: int, b: int) -> intConverts RGB values to a curses color number. Adapts to terminal capabilities automatically.
Parameters:
r(int): Red component (0-255)g(int): Green component (0-255)b(int): Blue component (0-255)
Returns:
int: Curses color number appropriate for terminal capability
Container for semantic color pairs used by themes. Provides named access to curses color pair numbers for common UI elements.
SemanticColors(
primary: int,
success: int,
error: int,
warning: int,
info: int,
background: int,
foreground: int,
accent: int
)Parameters:
All parameters are curses color pair numbers (integers) that can be used with curses.color_pair().
primary(int): Main UI color for highlights and focussuccess(int): Positive feedback and successful operationserror(int): Error messages and critical warningswarning(int): Caution messages and non-critical warningsinfo(int): Informational messages and help textbackground(int): Default background colorforeground(int): Default text coloraccent(int): Secondary highlight color
All constructor parameters are available as instance attributes.
theme = ThemeManager.load('dark')
theme.apply(stdscr)
# Access semantic colors
stdscr.addstr(0, 0, "Success!", curses.color_pair(theme.colors.success))
stdscr.addstr(1, 0, "Error!", curses.color_pair(theme.colors.error))
stdscr.addstr(2, 0, "Warning!", curses.color_pair(theme.colors.warning))
stdscr.addstr(3, 0, "Info", curses.color_pair(theme.colors.info))
stdscr.addstr(4, 0, "Primary", curses.color_pair(theme.colors.primary))
stdscr.addstr(5, 0, "Accent", curses.color_pair(theme.colors.accent))Container for component-based color pairs used by themes. This is the primary API matching curses-java Theme interface.
ComponentColors(
background: int,
button: int,
button_focused: int,
text_input: int,
border: int,
selection: int,
disabled: int
)Parameters:
All parameters are curses color pair numbers (integers) that can be used with curses.color_pair().
background(int): Normal background color pairbutton(int): Button in normal statebutton_focused(int): Button when focusedtext_input(int): Text input fieldsborder(int): Borders and framesselection(int): Selected/highlighted itemsdisabled(int): Disabled components
All constructor parameters are available as instance attributes.
theme = ThemeManager.load('dark')
theme.apply(stdscr)
# Access component colors
stdscr.addstr(0, 0, "Background", curses.color_pair(theme.components.background))
stdscr.addstr(1, 0, "[Button]", curses.color_pair(theme.components.button))
stdscr.addstr(2, 0, "[Focused]", curses.color_pair(theme.components.button_focused))
stdscr.addstr(3, 0, "Input: ", curses.color_pair(theme.components.text_input))
stdscr.addstr(4, 0, "Border", curses.color_pair(theme.components.border))
stdscr.addstr(5, 0, "Selected", curses.color_pair(theme.components.selection))
stdscr.addstr(6, 0, "Disabled", curses.color_pair(theme.components.disabled))Represents a foreground/background color pair with RGB values.
ColorPair(foreground: Tuple[int, int, int], background: Tuple[int, int, int])Parameters:
foreground(Tuple[int, int, int]): RGB tuple for foreground color (0-255)background(Tuple[int, int, int]): RGB tuple for background color (0-255)
foreground(Tuple[int, int, int]): Foreground RGB valuesbackground(Tuple[int, int, int]): Background RGB values
# Define a color pair
pair = ColorPair((255, 255, 255), (0, 0, 0)) # White on Black
# Use in theme component methods
def get_background(self) -> ColorPair:
return ColorPair((255, 255, 255), (0, 0, 0))The library includes 8 built-in themes:
Classic terminal appearance with white text on black background.
Load with: ThemeManager.load('default')
Color Scheme:
- Background: White on Black
- Button: Cyan on Black
- Button Focused: Black on Cyan
- Text Input: Green on Black
- Border: White on Black
- Selection: Black on White
- Disabled: White on Black
Modern dark theme with light text on dark background.
Load with: ThemeManager.load('dark')
Clean light theme with dark text on light background.
Load with: ThemeManager.load('light')
Vintage TI-99/4A computer theme with characteristic color scheme.
Load with: ThemeManager.load('ti-99-4a')
Classic TRS-80 computer theme with green on black appearance.
Load with: ThemeManager.load('trs-80')
MS-DOS inspired theme with blue background.
Load with: ThemeManager.load('dos')
Retro dBASE III database application theme.
Load with: ThemeManager.load('dbase-iii')
Classic dBASE IV database application theme.
Load with: ThemeManager.load('dbase-iv')
from curses_themes import Theme, ThemeManager, ColorPair
class CyberpunkTheme(Theme):
"""Custom cyberpunk-inspired theme."""
def __init__(self):
super().__init__(
name="Cyberpunk",
description="Neon cyberpunk theme with electric colors",
author="Your Name"
)
def get_color_map(self):
return {
'background': (10, 10, 30), # Dark blue
'foreground': (0, 255, 255), # Cyan
'primary': (255, 0, 255), # Magenta
'success': (0, 255, 128), # Bright green
'error': (255, 0, 128), # Hot pink
'warning': (255, 255, 0), # Yellow
'info': (0, 200, 255), # Light blue
'accent': (255, 128, 0), # Orange
}
def get_background(self) -> ColorPair:
return ColorPair((0, 255, 255), (10, 10, 30))
def get_button(self) -> ColorPair:
return ColorPair((255, 0, 255), (10, 10, 30))
def get_button_focused(self) -> ColorPair:
return ColorPair((10, 10, 30), (255, 0, 255))
def get_text_input(self) -> ColorPair:
return ColorPair((0, 255, 128), (10, 10, 30))
def get_border(self) -> ColorPair:
return ColorPair((0, 255, 255), (10, 10, 30))
def get_selection(self) -> ColorPair:
return ColorPair((10, 10, 30), (255, 0, 255))
def get_disabled(self) -> ColorPair:
return ColorPair((100, 100, 120), (10, 10, 30))
def get_border_chars(self) -> str:
# Use Unicode box-drawing characters
return "┌─┐││└─┘"
# Register and use
ThemeManager.register(CyberpunkTheme)
theme = ThemeManager.load('cyberpunk')import curses
from curses_themes import ThemeManager
def show_theme_menu(stdscr):
"""Display a menu of available themes."""
themes = ThemeManager.list_themes()
stdscr.clear()
stdscr.addstr(0, 0, "Available Themes:", curses.A_BOLD)
row = 2
for name, info in themes.items():
stdscr.addstr(row, 2, f"- {info['name']}")
stdscr.addstr(row + 1, 4, f"{info['description']}")
row += 3
stdscr.refresh()
stdscr.getch()
if __name__ == "__main__":
curses.wrapper(show_theme_menu)import curses
from curses_themes import ThemeManager
def main(stdscr):
theme_names = ['default', 'dark', 'light', 'ti-99-4a']
current_theme_idx = 0
while True:
# Load and apply current theme
theme = ThemeManager.load(theme_names[current_theme_idx])
theme.apply(stdscr)
# Clear and redraw
stdscr.clear()
stdscr.addstr(0, 0, f"Current Theme: {theme.name}",
curses.color_pair(theme.components.border))
stdscr.addstr(2, 0, "Press 'n' for next theme, 'q' to quit",
curses.color_pair(theme.components.text_input))
# Draw a sample box
theme.draw_box(stdscr, 4, 2, 8, 40, title="Sample Box")
stdscr.refresh()
# Handle input
key = stdscr.getch()
if key == ord('q'):
break
elif key == ord('n'):
current_theme_idx = (current_theme_idx + 1) % len(theme_names)
if __name__ == "__main__":
curses.wrapper(main)import curses
from curses_themes import ThemeManager
def draw_ui(stdscr):
theme = ThemeManager.load('dark')
theme.apply(stdscr)
# Draw main window
height, width = stdscr.getmaxyx()
# Title bar
stdscr.addstr(0, 0, " My Application ".center(width),
curses.color_pair(theme.components.border) | curses.A_BOLD)
# Menu panel
theme.draw_box(stdscr, 2, 2, 10, 20, title="Menu")
menu_items = ["File", "Edit", "View", "Help"]
for idx, item in enumerate(menu_items):
if idx == 0: # Highlight first item
attr = curses.color_pair(theme.components.selection)
else:
attr = curses.color_pair(theme.components.button)
stdscr.addstr(4 + idx, 4, f" {item} ", attr)
# Content panel
theme.draw_box(stdscr, 2, 24, 10, width - 26, title="Content")
stdscr.addstr(4, 26, "Welcome to the application!",
curses.color_pair(theme.components.background))
stdscr.addstr(5, 26, "Status: OK",
curses.color_pair(theme.colors.success))
# Status bar
stdscr.addstr(height - 1, 0, " Press 'q' to quit ".center(width),
curses.color_pair(theme.components.border))
stdscr.refresh()
stdscr.getch()
if __name__ == "__main__":
curses.wrapper(draw_ui)The library automatically adapts to terminal capabilities:
- 256-color terminals: Full RGB color support with 6x6x6 color cube
- 16-color terminals: Maps to extended ANSI colors
- 8-color terminals: Maps to basic ANSI colors
Color conversion is handled automatically by ColorManager using Euclidean distance in RGB space for the best visual match.
Raised when:
- Terminal doesn't support colors
theme.colorsortheme.componentsaccessed beforeapply()- Color initialization fails
- Maximum color pairs exceeded
try:
theme = ThemeManager.load('dark')
theme.apply(stdscr)
except RuntimeError as e:
print(f"Failed to apply theme: {e}")Raised when:
- Loading a non-existent theme
- Unregistering a non-existent theme
try:
theme = ThemeManager.load('nonexistent')
except KeyError as e:
print(f"Theme not found: {e}")
# Fall back to default
theme = ThemeManager.load('default')Raised when:
- Theme color map is missing required keys
- Box dimensions are too small in
draw_box() - Theme already registered with same name
try:
theme.draw_box(stdscr, 0, 0, 1, 1) # Too small
except ValueError as e:
print(f"Invalid dimensions: {e}")Raised when:
- Attempting to instantiate
ThemeManager - Registering a non-Theme class
# Good
theme = ThemeManager.load('dark')
theme.apply(stdscr)
stdscr.addstr(0, 0, "Text", curses.color_pair(theme.colors.primary))
# Bad - will raise RuntimeError
theme = ThemeManager.load('dark')
stdscr.addstr(0, 0, "Text", curses.color_pair(theme.colors.primary))Component colors provide better semantic meaning for UI elements:
# Good - clear intent
stdscr.addstr(0, 0, "[OK]", curses.color_pair(theme.components.button))
stdscr.addstr(1, 0, "Name: ", curses.color_pair(theme.components.text_input))
# Less clear
stdscr.addstr(0, 0, "[OK]", curses.color_pair(theme.colors.primary))import curses
from curses_themes import ThemeManager, ColorManager
def main(stdscr):
try:
manager = ColorManager(stdscr)
theme = ThemeManager.load('dark')
theme.apply(stdscr)
# Check capabilities
if manager.color_count < 256:
stdscr.addstr(0, 0, "Limited colors - display may vary")
except RuntimeError as e:
stdscr.addstr(0, 0, f"Color support error: {e}")
returnTheme names are automatically normalized (lowercase, hyphens), but it's good practice to use consistent naming:
# All equivalent, but be consistent
ThemeManager.load('dark') # Preferred
ThemeManager.load('Dark')
ThemeManager.load('DARK')This library is licensed under the GNU General Public License v3.0.
Copyright (C) 2024 FlossWare
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
See https://www.gnu.org/licenses/ for details.