Skip to content
Merged
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
11 changes: 7 additions & 4 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ jobs:
steps:
- name: Get PR info
id: pr-info
env:
PR_JSON: ${{ needs.release-please.outputs.pr }}
run: |
PR_JSON='${{ needs.release-please.outputs.pr }}'
echo "branch=$(echo "$PR_JSON" | jq -r '.headBranchName')" >> $GITHUB_OUTPUT
- uses: actions/checkout@v5
with:
Expand All @@ -51,15 +52,17 @@ jobs:
uses: astral-sh/setup-uv@v5
- name: Update uv.lock
run: uv sync
- name: Update JSON schemas
run: uv run python scripts/generate_schemas.py
- name: Commit and push if changed
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add uv.lock
git add uv.lock ctec.schema.json iterm2-color-scheme.schema.json
if git diff --staged --quiet; then
echo "No changes to uv.lock"
echo "No changes to commit"
else
git commit -m "chore: update uv.lock for release"
git commit -m "chore: update uv.lock and schemas for release"
git push
fi

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ jobs:
- name: Run tests with coverage
run: mise run test
- name: Coverage comment
if: github.event_name == 'pull_request'
uses: py-cov-action/python-coverage-comment-action@v3
with:
GITHUB_TOKEN: ${{ github.token }}
Expand Down
71 changes: 71 additions & 0 deletions console_cowboy/ctec/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,52 @@ class TabBarStyle(Enum):
SEPARATOR = "separator" # Kitty separator


class BackgroundImageScale(Enum):
"""
How to scale/fit the background image.

Common across terminals:
- Ghostty: background-image-fit (contain, cover, stretch, none)
- Kitty: background_image_layout (scaled, cscaled, tiled, centered, clamped)
- WezTerm: via background config
- iTerm2: Mode (Stretch, Tile, Scale to Fill, Scale to Fit)
"""

# Scale to fit entirely within window, preserving aspect ratio (letterbox/pillarbox)
CONTAIN = "contain"
# Scale to cover entire window, preserving aspect ratio (may crop)
COVER = "cover"
# Stretch to fill window exactly, ignoring aspect ratio
STRETCH = "stretch"
# Tile/repeat the image without scaling
TILE = "tile"
# No scaling, display at original size
NONE = "none"
# Centered without scaling (Kitty-specific but useful)
CENTERED = "centered"


class BackgroundImagePosition(Enum):
"""
Position of the background image within the window.

Common across terminals:
- Ghostty: background-image-position (9 positions)
- Kitty: via centered/clamped modes
- WezTerm: via advanced background config
"""

TOP_LEFT = "top-left"
TOP_CENTER = "top-center"
TOP_RIGHT = "top-right"
CENTER_LEFT = "center-left"
CENTER = "center"
CENTER_RIGHT = "center-right"
BOTTOM_LEFT = "bottom-left"
BOTTOM_CENTER = "bottom-center"
BOTTOM_RIGHT = "bottom-right"


class NewTabPosition(Enum):
"""
Where new tabs are created.
Expand Down Expand Up @@ -650,6 +696,10 @@ class WindowConfig:
decorations: Whether to show window decorations (title bar, etc.)
startup_mode: Initial window mode ('windowed', 'maximized', 'fullscreen')
dynamic_title: Whether to update window title from shell
background_image: Path to background image file
background_image_opacity: Background image opacity (0.0-1.0, 1.0 = fully visible)
background_image_scale: How to scale/fit the background image
background_image_position: Position of the background image
"""

columns: int | None = None
Expand All @@ -661,6 +711,10 @@ class WindowConfig:
decorations: bool | None = None
startup_mode: str | None = None
dynamic_title: bool | None = None
background_image: str | None = None
background_image_opacity: float | None = None
background_image_scale: BackgroundImageScale | None = None
background_image_position: BackgroundImagePosition | None = None

def to_dict(self) -> dict:
"""Convert to dictionary representation."""
Expand All @@ -675,15 +729,28 @@ def to_dict(self) -> dict:
"decorations",
"startup_mode",
"dynamic_title",
"background_image",
"background_image_opacity",
]:
value = getattr(self, field_name)
if value is not None:
result[field_name] = value
# Handle enum fields
if self.background_image_scale is not None:
result["background_image_scale"] = self.background_image_scale.value
if self.background_image_position is not None:
result["background_image_position"] = self.background_image_position.value
return result

@classmethod
def from_dict(cls, data: dict) -> "WindowConfig":
"""Create a WindowConfig from a dictionary."""
bg_scale = None
if "background_image_scale" in data:
bg_scale = BackgroundImageScale(data["background_image_scale"])
bg_position = None
if "background_image_position" in data:
bg_position = BackgroundImagePosition(data["background_image_position"])
return cls(
columns=data.get("columns"),
rows=data.get("rows"),
Expand All @@ -694,6 +761,10 @@ def from_dict(cls, data: dict) -> "WindowConfig":
decorations=data.get("decorations"),
startup_mode=data.get("startup_mode"),
dynamic_title=data.get("dynamic_title"),
background_image=data.get("background_image"),
background_image_opacity=data.get("background_image_opacity"),
background_image_scale=bg_scale,
background_image_position=bg_position,
)


Expand Down
3 changes: 3 additions & 0 deletions console_cowboy/terminals/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .alacritty import AlacrittyAdapter
from .base import TerminalAdapter, TerminalRegistry
from .ghostty import GhosttyAdapter
from .hyper import HyperAdapter
from .iterm2 import ITerm2Adapter
from .kitty import KittyAdapter
from .terminal_app import TerminalAppAdapter
Expand All @@ -22,6 +23,7 @@
TerminalRegistry.register(WeztermAdapter)
TerminalRegistry.register(VSCodeAdapter)
TerminalRegistry.register(TerminalAppAdapter)
TerminalRegistry.register(HyperAdapter)

__all__ = [
"TerminalAdapter",
Expand All @@ -33,4 +35,5 @@
"WeztermAdapter",
"VSCodeAdapter",
"TerminalAppAdapter",
"HyperAdapter",
]
95 changes: 94 additions & 1 deletion console_cowboy/terminals/ghostty.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from console_cowboy.ctec.schema import (
CTEC,
BackgroundImagePosition,
BackgroundImageScale,
BehaviorConfig,
ColorScheme,
CursorConfig,
Expand Down Expand Up @@ -116,6 +118,37 @@ class GhosttyAdapter(TerminalAdapter, CursorStyleMixin, ColorMapMixin, ParsingMi
"window-padding-y": ("padding_vertical", int),
"window-decoration": ("decorations", lambda v: v.lower() != "none"),
"window-title-show-all": ("dynamic_title", lambda v: v.lower() == "true"),
"background-image": ("background_image", str),
"background-image-opacity": ("background_image_opacity", float),
}

# Background image scale mapping (Ghostty: background-image-fit)
BACKGROUND_IMAGE_SCALE_MAP = {
"contain": BackgroundImageScale.CONTAIN,
"cover": BackgroundImageScale.COVER,
"stretch": BackgroundImageScale.STRETCH,
"none": BackgroundImageScale.NONE,
}

BACKGROUND_IMAGE_SCALE_REVERSE_MAP = {
v: k for k, v in BACKGROUND_IMAGE_SCALE_MAP.items()
}

# Background image position mapping
BACKGROUND_IMAGE_POSITION_MAP = {
"top-left": BackgroundImagePosition.TOP_LEFT,
"top-center": BackgroundImagePosition.TOP_CENTER,
"top-right": BackgroundImagePosition.TOP_RIGHT,
"center-left": BackgroundImagePosition.CENTER_LEFT,
"center": BackgroundImagePosition.CENTER,
"center-right": BackgroundImagePosition.CENTER_RIGHT,
"bottom-left": BackgroundImagePosition.BOTTOM_LEFT,
"bottom-center": BackgroundImagePosition.BOTTOM_CENTER,
"bottom-right": BackgroundImagePosition.BOTTOM_RIGHT,
}

BACKGROUND_IMAGE_POSITION_REVERSE_MAP = {
v: k for k, v in BACKGROUND_IMAGE_POSITION_MAP.items()
}

# Behavior mapping
Expand Down Expand Up @@ -505,6 +538,26 @@ def on_error(k, v, e):
if value.lower() == "true":
window.startup_mode = "fullscreen"

# Parse background image scale (fit) and position
elif key == "background-image-fit":
scale = cls.BACKGROUND_IMAGE_SCALE_MAP.get(value.lower())
if scale:
window.background_image_scale = scale
else:
ctec.add_warning(f"Unknown background-image-fit value: {value}")

elif key == "background-image-position":
position = cls.BACKGROUND_IMAGE_POSITION_MAP.get(value.lower())
if position:
window.background_image_position = position
else:
ctec.add_warning(f"Unknown background-image-position: {value}")

elif key == "background-image-repeat":
# If repeat is true, treat it as tile mode
if value.lower() == "true":
window.background_image_scale = BackgroundImageScale.TILE

# Parse cursor settings
elif key == "cursor-style":
cursor.style = cls.get_cursor_style(value)
Expand Down Expand Up @@ -629,7 +682,15 @@ def on_error(k, v, e):
ctec.font = font
if cursor.style or cursor.blink is not None:
ctec.cursor = cursor
if window.columns or window.rows or window.opacity:
if (
window.columns
or window.rows
or window.opacity
or window.background_image
or window.background_image_opacity is not None
or window.background_image_scale
or window.background_image_position
):
ctec.window = window
if (
behavior.shell
Expand Down Expand Up @@ -771,6 +832,38 @@ def export(cls, ctec: CTEC) -> str:
lines.append(
f"window-title-show-all = {str(ctec.window.dynamic_title).lower()}"
)
# Export background image settings
if ctec.window.background_image:
lines.append(f"background-image = {ctec.window.background_image}")
if ctec.window.background_image_opacity is not None:
lines.append(
f"background-image-opacity = {ctec.window.background_image_opacity}"
)
if ctec.window.background_image_scale is not None:
if ctec.window.background_image_scale == BackgroundImageScale.TILE:
# Ghostty uses repeat for tiling
lines.append("background-image-repeat = true")
elif (
ctec.window.background_image_scale == BackgroundImageScale.CENTERED
):
# Ghostty doesn't support centered mode, fall back to contain
ctec.add_warning(
"Ghostty does not support 'centered' background image scaling. "
"Using 'contain' as fallback."
)
lines.append("background-image-fit = contain")
else:
fit_val = cls.BACKGROUND_IMAGE_SCALE_REVERSE_MAP.get(
ctec.window.background_image_scale
)
if fit_val:
lines.append(f"background-image-fit = {fit_val}")
if ctec.window.background_image_position is not None:
pos_val = cls.BACKGROUND_IMAGE_POSITION_REVERSE_MAP.get(
ctec.window.background_image_position
)
if pos_val:
lines.append(f"background-image-position = {pos_val}")
lines.append("")

# Export behavior settings
Expand Down
5 changes: 5 additions & 0 deletions console_cowboy/terminals/hyper/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Hyper terminal adapter."""

from .adapter import HyperAdapter

__all__ = ["HyperAdapter"]
Loading
Loading