Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include README.md
include LICENSE
include requirements.txt
recursive-include share/themes *.json
65 changes: 64 additions & 1 deletion docs/colors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
**The actual terminal 8\-bit color palette may not match the table above. It is common for terminal emulators to customize the palette.**
10 changes: 10 additions & 0 deletions examples/theme-default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"black": "#000000",
"red": "#f01818",
"green": "#24d830",
"yellow": "#ffff00",
"blue": "#7890f0",
"magenta": "#ff00ff",
"cyan": "#58f0f0",
"white": "#ffffff"
}
10 changes: 10 additions & 0 deletions examples/theme-gruvbox.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"black": "#282828",
"red": "#cc241d",
"green": "#98971a",
"yellow": "#d79921",
"blue": "#458588",
"magenta": "#b16286",
"cyan": "#689d6a",
"white": "#ebdbb2"
}
10 changes: 10 additions & 0 deletions examples/theme-solarized-dark.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"black": "#002b36",
"red": "#dc322f",
"green": "#859900",
"yellow": "#b58900",
"blue": "#268bd2",
"magenta": "#d33682",
"cyan": "#2aa198",
"white": "#fdf6e3"
}
10 changes: 10 additions & 0 deletions examples/theme-synthwave84.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"black": "#241b2f",
"red": "#fe4450",
"green": "#72f1b8",
"yellow": "#fede5d",
"blue": "#03edf9",
"magenta": "#ff7edb",
"cyan": "#36f9f6",
"white": "#fc5aae"
}
10 changes: 9 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -50,3 +57,4 @@ method = "smallest"

[tool.versioningit.write]
file = "tnz/_version.py"

10 changes: 10 additions & 0 deletions share/themes/theme-default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"black": "#000000",
"red": "#f01818",
"green": "#24d830",
"yellow": "#ffff00",
"blue": "#7890f0",
"magenta": "#ff00ff",
"cyan": "#58f0f0",
"white": "#ffffff"
}
10 changes: 10 additions & 0 deletions share/themes/theme-gruvbox.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"black": "#282828",
"red": "#cc241d",
"green": "#98971a",
"yellow": "#d79921",
"blue": "#458588",
"magenta": "#b16286",
"cyan": "#689d6a",
"white": "#ebdbb2"
}
10 changes: 10 additions & 0 deletions share/themes/theme-solarized-dark.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"black": "#002b36",
"red": "#dc322f",
"green": "#859900",
"yellow": "#b58900",
"blue": "#268bd2",
"magenta": "#d33682",
"cyan": "#2aa198",
"white": "#fdf6e3"
}
10 changes: 10 additions & 0 deletions share/themes/theme-synthwave84.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"black": "#241b2f",
"red": "#fe4450",
"green": "#72f1b8",
"yellow": "#fede5d",
"blue": "#03edf9",
"magenta": "#ff7edb",
"cyan": "#36f9f6",
"white": "#fc5aae"
}
79 changes: 79 additions & 0 deletions tnz/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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
74 changes: 44 additions & 30 deletions tnz/zti.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading