diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..a2c0ee0 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include README.md +include LICENSE +include requirements.txt +recursive-include share/themes *.json \ No newline at end of file diff --git a/docs/colors.md b/docs/colors.md index 0ff8be0..c2a3aed 100644 --- a/docs/colors.md +++ b/docs/colors.md @@ -4,8 +4,71 @@ title: Colors By default, *tnz* will emulate a 3270 terminal that has 8 colors and `zti` will assume a host terminal capable of true color is being used and use a *tnz\-defined color palette* to display those 8 terminal colors. +## TNZ_COLORS Environment Variable + If you do not want (or cannot use, in the case of `Terminal.app`) this default color behavior, you can use the `TNZ_COLORS` environment variable. Use `export TNZ_COLORS=256` to direct `zti` to use the 256\-color palette instead of true color. Set to an integer less than 16 and `zti` will assume only the *standard* ansi colors can be used (for example `export TNZ_COLORS=8`). This can be helpful if your terminal doesn't support true color or if you want to change the colors to your liking \- terminal emulators typically allow you to set the ansi colors. Set to an integer less than 8 and tnz will emulate a 3270 terminal that has no color capability and `zti` will not use any color capability. +## TNZ_THEME Environment Variable + +For more precise control over the color palette, you can use the `TNZ_THEME` environment variable to specify a JSON file containing custom color definitions. This allows you to define the exact hex values for each of the 8 standard terminal colors. + +To use a custom theme: + +1. Create a JSON file with color definitions (see example below) +2. Set the environment variable: `export TNZ_THEME=/path/to/theme.json` +3. Start `zti` or `ztd` + +### Theme File Format + +The theme JSON file must contain hex color values for all 8 standard colors: + +```json +{ + "black": "#000000", + "red": "#f01818", + "green": "#24d830", + "yellow": "#ffff00", + "blue": "#7890f0", + "magenta": "#ff00ff", + "cyan": "#58f0f0", + "white": "#ffffff" +} +``` + +Each color value must be a hex string in the format `#RRGGBB`. The theme will only be applied if the terminal supports true color (colors >= 264). If any color is missing or invalid, the default IBM PCOMM color palette will be used instead. + +### Example Themes + +**Solarized Dark Theme:** +```json +{ + "black": "#002b36", + "red": "#dc322f", + "green": "#859900", + "yellow": "#b58900", + "blue": "#268bd2", + "magenta": "#d33682", + "cyan": "#2aa198", + "white": "#fdf6e3" +} +``` + +**Gruvbox Theme:** +```json +{ + "black": "#282828", + "red": "#cc241d", + "green": "#98971a", + "yellow": "#d79921", + "blue": "#458588", + "magenta": "#b16286", + "cyan": "#689d6a", + "white": "#ebdbb2" +} +``` + +## Color Palette Reference + The following table describes, for each color mode, the host terminal colors used for the 8 different 3270 colors. If different colors are desired, check the zti\-hosting terminal for the capability to change the color palette. You may need to use TNZ\_COLORS=8 to get zti to use the customized color palette. -**The actual terminal 8\-bit color palette may not match the table above. It is common for terminal emulators to customize the palette.** \ No newline at end of file +**The actual terminal 8\-bit color palette may not match the table above. It is common for terminal emulators to customize the palette.** \ No newline at end of file diff --git a/examples/theme-default.json b/examples/theme-default.json new file mode 100644 index 0000000..70f6fa2 --- /dev/null +++ b/examples/theme-default.json @@ -0,0 +1,10 @@ +{ + "black": "#000000", + "red": "#f01818", + "green": "#24d830", + "yellow": "#ffff00", + "blue": "#7890f0", + "magenta": "#ff00ff", + "cyan": "#58f0f0", + "white": "#ffffff" +} \ No newline at end of file diff --git a/examples/theme-gruvbox.json b/examples/theme-gruvbox.json new file mode 100644 index 0000000..5f84efa --- /dev/null +++ b/examples/theme-gruvbox.json @@ -0,0 +1,10 @@ +{ + "black": "#282828", + "red": "#cc241d", + "green": "#98971a", + "yellow": "#d79921", + "blue": "#458588", + "magenta": "#b16286", + "cyan": "#689d6a", + "white": "#ebdbb2" +} \ No newline at end of file diff --git a/examples/theme-solarized-dark.json b/examples/theme-solarized-dark.json new file mode 100644 index 0000000..8aac1d1 --- /dev/null +++ b/examples/theme-solarized-dark.json @@ -0,0 +1,10 @@ +{ + "black": "#002b36", + "red": "#dc322f", + "green": "#859900", + "yellow": "#b58900", + "blue": "#268bd2", + "magenta": "#d33682", + "cyan": "#2aa198", + "white": "#fdf6e3" +} \ No newline at end of file diff --git a/examples/theme-synthwave84.json b/examples/theme-synthwave84.json new file mode 100644 index 0000000..c19c310 --- /dev/null +++ b/examples/theme-synthwave84.json @@ -0,0 +1,10 @@ +{ + "black": "#241b2f", + "red": "#fe4450", + "green": "#72f1b8", + "yellow": "#fede5d", + "blue": "#03edf9", + "magenta": "#ff7edb", + "cyan": "#36f9f6", + "white": "#fc5aae" +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 48af661..48b64d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,14 @@ source = "https://github.com/IBM/tnz" packages = ["tnz"] [tool.setuptools.package-data] -tnz = ["logging.json"] +tnz = ["logging.json", "../share/themes/*.json"] + +"share/tnz/themes" = [ + "share/themes/theme-default.json", + "share/themes/theme-gruvbox.json", + "share/themes/theme-solarized-dark.json", + "share/themes/theme-synthwave84.json" +] [tool.versioningit.format] distance = "{next_version}-dev.{distance}+{vcs}{rev}" @@ -50,3 +57,4 @@ method = "smallest" [tool.versioningit.write] file = "tnz/_version.py" + \ No newline at end of file diff --git a/share/themes/theme-default.json b/share/themes/theme-default.json new file mode 100644 index 0000000..70f6fa2 --- /dev/null +++ b/share/themes/theme-default.json @@ -0,0 +1,10 @@ +{ + "black": "#000000", + "red": "#f01818", + "green": "#24d830", + "yellow": "#ffff00", + "blue": "#7890f0", + "magenta": "#ff00ff", + "cyan": "#58f0f0", + "white": "#ffffff" +} \ No newline at end of file diff --git a/share/themes/theme-gruvbox.json b/share/themes/theme-gruvbox.json new file mode 100644 index 0000000..5f84efa --- /dev/null +++ b/share/themes/theme-gruvbox.json @@ -0,0 +1,10 @@ +{ + "black": "#282828", + "red": "#cc241d", + "green": "#98971a", + "yellow": "#d79921", + "blue": "#458588", + "magenta": "#b16286", + "cyan": "#689d6a", + "white": "#ebdbb2" +} \ No newline at end of file diff --git a/share/themes/theme-solarized-dark.json b/share/themes/theme-solarized-dark.json new file mode 100644 index 0000000..8aac1d1 --- /dev/null +++ b/share/themes/theme-solarized-dark.json @@ -0,0 +1,10 @@ +{ + "black": "#002b36", + "red": "#dc322f", + "green": "#859900", + "yellow": "#b58900", + "blue": "#268bd2", + "magenta": "#d33682", + "cyan": "#2aa198", + "white": "#fdf6e3" +} \ No newline at end of file diff --git a/share/themes/theme-synthwave84.json b/share/themes/theme-synthwave84.json new file mode 100644 index 0000000..c19c310 --- /dev/null +++ b/share/themes/theme-synthwave84.json @@ -0,0 +1,10 @@ +{ + "black": "#241b2f", + "red": "#fe4450", + "green": "#72f1b8", + "yellow": "#fede5d", + "blue": "#03edf9", + "magenta": "#ff7edb", + "cyan": "#36f9f6", + "white": "#fc5aae" +} \ No newline at end of file diff --git a/tnz/_util.py b/tnz/_util.py index 0676d12..03ed4ad 100644 --- a/tnz/_util.py +++ b/tnz/_util.py @@ -7,8 +7,14 @@ SPDX-License-Identifier: Apache-2.0 """ +import json +import logging +import os + __author__ = "Neil Johnson" +_logger = logging.getLogger(__name__) + _SESSION_PS_SIZES = { "2": (24, 80), "3": (32, 80), @@ -69,3 +75,76 @@ def session_ps_14bit(max_h, max_w): return max_h, max_w return 16383 // max_w, max_w + + + +def load_theme(): + """Load color theme from TNZ_THEME environment variable. + + Returns a dictionary mapping color names to RGB tuples (0-1000 scale), + or None if TNZ_THEME is not set or the file cannot be loaded. + + Expected JSON format: + { + "black": "#000000", + "red": "#f01818", + "green": "#24d830", + "yellow": "#ffff00", + "blue": "#7890f0", + "magenta": "#ff00ff", + "cyan": "#58f0f0", + "white": "#ffffff" + } + + Hex values should be in format #RRGGBB. + """ + theme_path = os.environ.get('TNZ_THEME') + if not theme_path: + return None + + try: + with open(theme_path, 'r') as f: + theme_data = json.load(f) + + # Validate and convert hex colors to RGB tuples (0-1000 scale) + color_names = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'] + theme = {} + + for color_name in color_names: + if color_name not in theme_data: + _logger.warning(f"TNZ_THEME: Missing color '{color_name}' in theme file") + return None + + hex_color = theme_data[color_name] + if not isinstance(hex_color, str) or not hex_color.startswith('#') or len(hex_color) != 7: + _logger.warning(f"TNZ_THEME: Invalid hex color for '{color_name}': {hex_color}") + return None + + try: + # Convert hex to RGB (0-255) then to curses scale (0-1000) + r = int(hex_color[1:3], 16) + g = int(hex_color[3:5], 16) + b = int(hex_color[5:7], 16) + + # Convert from 0-255 to 0-1000 scale + r_1000 = int(round(r * 1000 / 255)) + g_1000 = int(round(g * 1000 / 255)) + b_1000 = int(round(b * 1000 / 255)) + + theme[color_name] = (r_1000, g_1000, b_1000) + except ValueError as e: + _logger.warning(f"TNZ_THEME: Failed to parse hex color for '{color_name}': {hex_color} - {e}") + return None + + _logger.info(f"TNZ_THEME: Loaded theme from {theme_path}") + return theme + + except FileNotFoundError: + _logger.warning(f"TNZ_THEME: Theme file not found: {theme_path}") + return None + except json.JSONDecodeError as e: + _logger.warning(f"TNZ_THEME: Invalid JSON in theme file: {theme_path} - {e}") + return None + except Exception as e: + _logger.warning(f"TNZ_THEME: Error loading theme file: {theme_path} - {e}") + return None \ No newline at end of file diff --git a/tnz/zti.py b/tnz/zti.py index 383cf6d..2d84304 100644 --- a/tnz/zti.py +++ b/tnz/zti.py @@ -25,6 +25,7 @@ TERM_PROGRAM (see _termlib.py) TNZ_COLORS (see tnz.py) TNZ_LOGGING (see tnz.py) + TNZ_THEME (path to JSON file with custom color theme) ZTI_AIDBUFSIZE (9 is default) ZTI_AUTOSIZE ZTI_CURSOR_INSERT @@ -2751,27 +2752,49 @@ def __set_colors(self): _logger.debug("colors = %d", colors) _logger.debug("color_pairs = %d", color_pairs) - # IBM PCOMM default colors: - # name r, g, b - # black 0, 0, 0 000000 - # red 240, 24, 24 f01818 - # green 36, 216, 48 24d830 - # yellow 255, 255, 0 ffff00 - # blue 120, 144, 240 7890f0 - # pink 255, 0, 255 ff00ff - # turquoise 88, 240, 240 58f0f0 - # white 255, 255, 255 ffffff - # - # Converting from 256 to 1000: - # name r, g, b - # black 0, 0, 0 - # red 941, 94, 94 - # green 141, 847, 188 - # yellow 1000, 1000, 0 - # blue 471, 565, 941 - # pink 1000, 0, 1000 - # turquoise 345, 941, 941 - # white 1000, 1000, 1000 + # Try to load custom theme from TNZ_THEME environment variable + theme = _util.load_theme() + + if theme: + # Use custom theme colors + black_rgb = theme['black'] + red_rgb = theme['red'] + green_rgb = theme['green'] + yellow_rgb = theme['yellow'] + blue_rgb = theme['blue'] + pink_rgb = theme['magenta'] + turquoise_rgb = theme['cyan'] + white_rgb = theme['white'] + else: + # IBM PCOMM default colors: + # name r, g, b + # black 0, 0, 0 000000 + # red 240, 24, 24 f01818 + # green 36, 216, 48 24d830 + # yellow 255, 255, 0 ffff00 + # blue 120, 144, 240 7890f0 + # pink 255, 0, 255 ff00ff + # turquoise 88, 240, 240 58f0f0 + # white 255, 255, 255 ffffff + # + # Converting from 256 to 1000: + # name r, g, b + # black 0, 0, 0 + # red 941, 94, 94 + # green 141, 847, 188 + # yellow 1000, 1000, 0 + # blue 471, 565, 941 + # pink 1000, 0, 1000 + # turquoise 345, 941, 941 + # white 1000, 1000, 1000 + black_rgb = (0, 0, 0) + red_rgb = (941, 94, 94) + green_rgb = (141, 847, 188) + yellow_rgb = (1000, 1000, 0) + blue_rgb = (471, 565, 941) + pink_rgb = (1000, 0, 1000) + turquoise_rgb = (345, 941, 941) + white_rgb = (1000, 1000, 1000) # # curses defines the following: # COLOR_BLACK @@ -2808,15 +2831,6 @@ def __set_colors(self): # ** Using ROUND function if colors >= 264: - black_rgb = (0, 0, 0) - red_rgb = (941, 94, 94) - green_rgb = (141, 847, 188) - yellow_rgb = (1000, 1000, 0) - blue_rgb = (471, 565, 941) - pink_rgb = (1000, 0, 1000) - turquoise_rgb = (345, 941, 941) - white_rgb = (1000, 1000, 1000) - color_start = 256 curses.init_color(color_start, *black_rgb) curses.init_color(color_start + 1, *blue_rgb)