Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bbc8846
fix: guard against `window.TKroot` being `None`
definite-d Oct 22, 2025
52c9bf3
fix: fix minor type issues with `recurse_menu`
definite-d Oct 22, 2025
7fddb41
chore: add line breaks to prompt in `sg.py`
definite-d Oct 22, 2025
4a5d4ff
feat: add and use `_compat` module
definite-d Oct 22, 2025
fa454c4
fix: fix minor type errors in `ease` function
definite-d Oct 22, 2025
644fc46
fix: fix type errors with `window.TKroot` being `None`
definite-d Oct 22, 2025
b6e5e16
chore: sort imports and format
definite-d Oct 22, 2025
00a0736
fix: remove `v` prefix in commit message
definite-d Oct 22, 2025
800a3e5
fix: add `--diff` to `ruff check`
definite-d Oct 22, 2025
c61aec2
feat: add `all` param to `_compat.py`
definite-d Oct 22, 2025
db35bc3
chore: update the links to the homepage and bug tracker
definite-d Dec 4, 2025
03f4c8f
fix: fixed regression that caused menus with dividers to crash `reski…
definite-d Dec 4, 2025
a428903
feat: add `freesimplegui` to project's dev dependencies.
definite-d Jan 22, 2026
55f0ad9
fix: return the demo interpolation mode to `hue`.
definite-d Jan 22, 2026
41ab2c9
feat: replace the `ElementName` system with a class-based dispatcher …
definite-d Jan 22, 2026
04b3f46
fix: revert python version to `3.13` for uv-system-tkinter compatibility
definite-d Jan 22, 2026
cccac55
chore: formatting
definite-d Jan 22, 2026
0cbe1b0
bump version 4.0.1 -> 4.1.0
definite-d Jan 22, 2026
492799f
fix: close the default window explicitly
definite-d Jan 22, 2026
30ee4d8
bump version 4.1.0 -> 4.1.1
definite-d Jan 22, 2026
c1e5261
feat: add line to shrink the window via tkinter's `geometry` method
definite-d Jan 22, 2026
efbd1f0
fix: improve conditional
definite-d Jan 22, 2026
6bccfa7
chore: formatting
definite-d Jan 23, 2026
4fc0080
feat: shrink and hide the default window
definite-d Jan 23, 2026
8accde7
chore: update comment
definite-d Jan 23, 2026
85750c6
bump version 4.1.1 -> 4.1.2
definite-d Jan 23, 2026
94ac81e
chore: update `pyproject.toml`
definite-d Jan 23, 2026
bf6113a
feat: add makefile
definite-d Jan 23, 2026
d40b8b7
chore: remove unused imports
definite-d Jan 23, 2026
3c07842
chore: remove unused variable
definite-d Jan 23, 2026
3acc75f
bump version 4.1.2 -> 4.1.3
definite-d Jan 23, 2026
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: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- name: Ruff lint
run: uv run ruff check .
- name: Ruff check
run: uv run ruff check . --diff
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.14
3.13
46 changes: 46 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# bump and tag version
bump-patch:
@echo "Updating patch version..."
bumpver update --patch

bump-minor:
@echo "Updating minor version..."
bumpver update --minor

bump-major:
@echo "Updating major version..."
bumpver update --major

# generate changelog since last version tag
changelog:
@echo "## Changelog since $(shell git describe --tags --match 'v[0-9]*.[0-9]*.[0-9]*' --abbrev=0)" && \
echo "" && \
git log --oneline $(shell git describe --tags --match 'v[0-9]*.[0-9]*.[0-9]*' --abbrev=0)..HEAD | sed 's/^/- /'

# run linting checks
lint:
@echo "Running linting checks..."
uv run ruff check . --diff

# push tag and create PR (for when we've already bumped the version)
push-release:
@echo "Creating release tag with v prefix..."
git tag "v$(shell bumpver get current)" $(shell bumpver get current)
@echo "Pushing tags to trigger build..."
git push origin --tags
@echo "Creating PR to sync dev to main..."
gh pr create --base main --head dev --title "Sync dev to main after release" --body "Automated PR to sync dev branch changes to main after release v$(shell bumpver get current)\n\n## Changelog\n$$(make changelog)"
@echo "Release pushed successfully!"

# create release with specified level: make release LEVEL=patch|minor|major
release: lint
@echo "Creating release..."
@echo "Changelog for this release:" && \
$(MAKE) changelog
@read -p "Continue with release? [y/N] " confirm && \
if [ "$$confirm" != "y" ] && [ "$$confirm" != "Y" ]; then \
echo "Release cancelled."; exit 1; \
fi
@echo "Updating $(LEVEL) version..."
bumpver update --$(LEVEL)
$(MAKE) push-release
15 changes: 10 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "reskinner"
requires-python = ">=3.7"
description = "Instantaneous theme changing for PySimpleGUI and FreeSimpleGUI windows."
version = "4.0.1"
version = "4.1.3"
authors = [{ name = "Divine U. Afam-Ifediogor" }]
license = { file = "LICENSE" }
readme = "./res/DESCRIPTION.md"
Expand Down Expand Up @@ -41,8 +41,8 @@ dependencies = [
]

[project.urls]
"Homepage" = "https://github.com/definite-d/psg_reskinner/"
"Bug Tracker" = "https://github.com/definite-d/psg_reskinner/issues/"
"Homepage" = "https://github.com/definite-d/reskinner/"
"Bug Tracker" = "https://github.com/definite-d/reskinner/issues/"

[project.optional-dependencies]
psg = [
Expand Down Expand Up @@ -78,14 +78,19 @@ build-backend = "uv_build"
[dependency-groups]
dev = [
"bumpver",
"freesimplegui>=5.2.0.post1",
"isort",
"ruff",
]

[tool.ruff]
target-version = "py37"
line-length = 88

[tool.bumpver]
current_version = "4.0.1"
current_version = "4.1.3"
version_pattern = "MAJOR.MINOR.PATCH"
commit_message = "bump version v{old_version} -> v{new_version}"
commit_message = "bump version {old_version} -> {new_version}"
tag_message = "{new_version}"
tag_scope = "default"
pre_commit_hook = ""
Expand Down
5 changes: 3 additions & 2 deletions src/reskinner/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ def _reskin_job():
theme_function=sg.theme,
lf_table=sg.LOOK_AND_FEEL_TABLE,
duration=450,
interpolation_mode="hsl",
interpolation_mode="hue",
)
window.TKroot.after(2000, _reskin_job)
if window.TKroot:
window.TKroot.after(2000, _reskin_job)

started = False

Expand Down
5 changes: 1 addition & 4 deletions src/reskinner/__version__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
try:
from importlib.metadata import PackageNotFoundError, version
except ModuleNotFoundError:
from importlib_metadata import PackageNotFoundError, version
from ._compat import PackageNotFoundError, version

try:
__version__ = version("reskinner")
Expand Down
17 changes: 17 additions & 0 deletions src/reskinner/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import sys

v = sys.version_info

if v >= (3, 8):
from importlib.metadata import PackageNotFoundError, version
from typing import Literal, Protocol
else:
from importlib_metadata import PackageNotFoundError, version
from typing_extensions import Literal, Protocol

if v >= (3, 11):
from enum import StrEnum
else:
from strenum import StrEnum

__all__ = ["PackageNotFoundError", "version", "Literal", "Protocol", "StrEnum"]
103 changes: 67 additions & 36 deletions src/reskinner/colorizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@
from tkinter import Widget
from tkinter.ttk import Style
from typing import Any, Callable, Dict, Optional, Tuple, TypeVar, Union
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal

from colour import Color # type: ignore[import-untyped]

from .constants import LRU_MAX_SIZE, ElementName, ScrollbarColorKey
from ._compat import Literal
from .constants import LRU_MAX_SIZE, ScrollbarColorKey
from .default_window import DEFAULT_ELEMENTS, DEFAULT_WINDOW
from .easing import EasingName, ease
from .interpolation import INTERPOLATION_MODES, InterpolationMethod
Expand Down Expand Up @@ -107,19 +104,31 @@ def _default_window_cget(attribute: str) -> Any:


@lru_cache(maxsize=LRU_MAX_SIZE)
def _default_element_cget(element_name: str, attribute: str) -> Union[str, Widget]:
"""Get an element attribute using cget.
def _default_element_cget(element_class: Type, attribute: str) -> Union[str, Widget]:
"""
Get the default value for an element's attribute.

Internal use only.

:param element_name: The name of the element
:type element_name: str
:param element_class: The class of the element
:type element_class: Type
:param attribute: The attribute to pass to the cget function
:type attribute: str
:return: The result of the cget function
:rtype: Union[str, Widget]
"""
return DEFAULT_ELEMENTS[element_name].widget[attribute]
# Try to find the element in DEFAULT_ELEMENTS, checking base classes if needed
for cls in element_class.__mro__:
if cls in DEFAULT_ELEMENTS:
return DEFAULT_ELEMENTS[cls].widget[attribute]

# Fallback: try to create a temporary element
try:
temp_element = element_class()
return temp_element.widget[attribute]
except Exception:
# Final fallback: return a reasonable default
return "black"


def _run_progressbar_computation(theme_dict: ThemeDict) -> ThemeDict:
Expand Down Expand Up @@ -147,7 +156,7 @@ def _get_checkbox_radio_selectcolor(background_color, text_color) -> str:
# PySimpleGUI's color conversion functions give different results than those of the colour module
# due to floating point truncation, so I can't use the color module's functionality for everything here.
if not all([_is_valid_color(background_color), _is_valid_color(text_color)]):
return _default_element_cget(ElementName.CHECKBOX, "selectcolor") or "black"
return _default_element_cget(sg.Checkbox, "selectcolor") or "black"
background_color: str = Color(background_color).get_hex_l()
text_color: str = Color(text_color).get_hex_l()
background_hsl: Tuple[float, float, float] = sg._hex_to_hsl(background_color)
Expand Down Expand Up @@ -177,7 +186,7 @@ def _default_combo_popdown_cget(attribute: str) -> str:
"""
DEFAULT_WINDOW.TKroot.tk.call(
"eval",
f"set defaultcombo [ttk::combobox::PopdownWindow {DEFAULT_ELEMENTS['combo'].widget}]",
f"set defaultcombo [ttk::combobox::PopdownWindow {DEFAULT_ELEMENTS[sg.Combo].widget}]",
)
return DEFAULT_WINDOW.TKroot.tk.call("eval", f"$defaultcombo.f.l cget -{attribute}")

Expand Down Expand Up @@ -257,7 +266,7 @@ def element(
configuration,
element.widget.configure,
lambda attribute: _default_element_cget(
ElementName.from_element(element),
type(element),
attribute,
),
)
Expand Down Expand Up @@ -314,7 +323,12 @@ def window(
window: sg.Window,
configuration: ThemeConfiguration,
):
self._configure(configuration, window.TKroot.configure, _default_window_cget)
if window.TKroot:
self._configure(
configuration,
window.TKroot.configure,
_default_window_cget,
)

# Specific

Expand All @@ -326,7 +340,7 @@ def parent_row_frame(
self._configure(
configuration,
parent_row_frame.configure,
getattr(DEFAULT_ELEMENTS["text"], "ParentRowFrame").cget,
getattr(DEFAULT_ELEMENTS[sg.Text], "ParentRowFrame").cget,
)

def menu_entry(
Expand All @@ -335,10 +349,18 @@ def menu_entry(
index: int,
configuration: ThemeConfiguration,
):
configuration = dict(
filter(
lambda item: item[0] in menu.entryconfigure(index).keys(),
configuration.items(),
)
)
# Filter the configs for menu entries that don't accept the full config dict. Fixes issue #11.
# Brought back in v4.0.2 after its omission caused a regression leading to issue #22.
self._configure(
configuration,
lambda **_configurations: menu.entryconfigure(index, _configurations),
lambda attribute: _default_element_cget("menu", attribute),
lambda attribute: _default_element_cget(sg.Menu, attribute),
)

def optionmenu_menu(
Expand All @@ -349,7 +371,7 @@ def optionmenu_menu(
self._configure(
configuration,
optionmenu.widget["menu"].configure,
_default_element_cget(ElementName.OPTIONMENU, "menu").cget,
_default_element_cget(sg.OptionMenu, "menu").cget,
)

def scrollbar(
Expand Down Expand Up @@ -385,7 +407,7 @@ def scrollbar(
default_style,
)

def recurse_menu(self, tkmenu: Union[TKMenu, Widget]):
def recurse_menu(self, tkmenu: TKMenu):
"""
Internal use only.

Expand All @@ -397,12 +419,13 @@ def recurse_menu(self, tkmenu: Union[TKMenu, Widget]):
:param tkmenu: The Tkinter menu object.
:return: None
"""
end_menu_index = tkmenu.index("end")

# This fixes issue #8. Thank you, @richnanney for reporting!
if tkmenu.index("end") is None:
if end_menu_index is None:
return

for index in range(0, tkmenu.index("end") + 1):
for index in range(0, end_menu_index + 1):
self.menu_entry(
tkmenu,
index,
Expand All @@ -422,15 +445,23 @@ def scrollable_column(self, column: sg.Column):
self._configure(
{"background": "BACKGROUND"},
column.TKColFrame.configure,
DEFAULT_ELEMENTS["column"].TKColFrame.cget,
)
self._configure(
{"background": "BACKGROUND"},
getattr(column.TKColFrame, "canvas").children["!frame"].configure,
getattr(DEFAULT_ELEMENTS["column"].TKColFrame, "canvas")
.children["!frame"]
.cget,
DEFAULT_ELEMENTS[sg.Column].TKColFrame.cget,
)
# Handle the inner frame if it exists
canvas = getattr(column.TKColFrame, "canvas", None)
if canvas and hasattr(canvas, "children") and "!frame" in canvas.children:
self._configure(
{"background": "BACKGROUND"},
canvas.children["!frame"].configure,
getattr(DEFAULT_ELEMENTS[sg.Column].TKColFrame, "canvas")
.children.get("!frame")
.cget
if "!frame"
in getattr(
DEFAULT_ELEMENTS[sg.Column].TKColFrame, "canvas", {}
).children
else lambda _: "white",
)

def combo(self, combo: sg.Combo):
# Configuring the listbox (popdown) of the combo.
Expand Down Expand Up @@ -468,29 +499,29 @@ def _configure_combo_popdown(**kwargs):
"background": ("BUTTON", 1),
"arrowcolor": ("BUTTON", 0),
},
_default_element_cget("combo", "style"),
_default_element_cget(sg.Combo, "style"),
)
self.map(
style_name,
{
"foreground": {"readonly": "TEXT_INPUT"},
"fieldbackground": {"readonly": "INPUT"},
},
_default_element_cget("combo", "style"),
_default_element_cget(sg.Combo, "style"),
True,
)

def checkbox_or_radio(self, element: Union[sg.Checkbox, sg.Radio]):
element_name = ElementName.from_element(element)
element_type = type(element)
toggle = (
_get_checkbox_radio_selectcolor(
self._color(
"BACKGROUND",
lambda: _default_element_cget(element_name, "selectcolor"),
lambda: _default_element_cget(element_type, "selectcolor"),
),
self._color(
"TEXT",
lambda: _default_element_cget(element_name, "selectcolor"),
lambda: _default_element_cget(element_type, "selectcolor"),
),
),
)
Expand All @@ -509,7 +540,7 @@ def checkbox_or_radio(self, element: Union[sg.Checkbox, sg.Radio]):

def table_or_tree(self, element: Union[sg.Table, sg.Tree]):
style_name = element.widget["style"]
element_name = ElementName.from_element(element)
element_type = type(element)
default_style = element.widget.winfo_class()
self.style(
style_name,
Expand Down Expand Up @@ -545,7 +576,7 @@ def table_or_tree(self, element: Union[sg.Table, sg.Tree]):
f"{default_style}.Heading",
)

if element_name == "table":
if element_type == sg.Table:
self.map(
f"{style_name}.Heading",
{
Expand All @@ -561,5 +592,5 @@ def progressbar(self, element: sg.ProgressBar):
self.style(
style_name,
{"background": ("PROGRESS", 0), "troughcolor": ("PROGRESS", 1)},
_default_element_cget("progressbar", "style"),
_default_element_cget(sg.ProgressBar, "style"),
)
Loading