Skip to content
Closed
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
41 changes: 39 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help lint pytest sync venv run smoke clean-build build build-onedir install install-bin install-desktop mime-query redo ci-test-debian ci-test-local ci-build-image githook install-githook
.PHONY: help lint pytest sync venv run smoke clean-build build build-onedir macos-app macos-install linux-install install install-bin install-desktop mime-query redo ci-test-debian ci-test-local ci-build-image githook install-githook

# Configuration
PREFIX ?= /usr/local
Expand Down Expand Up @@ -58,7 +58,7 @@ smoke: ## Run manual smoke test checklist
uv run --active python cdisplayagain.py "$(FILE)"

clean-build: ## Clean build artifacts
rm -rf build dist *.spec __pycache__ .pytest_cache
rm -rf build dist __pycache__ .pytest_cache

build: clean-build ## Build single-file executable (slower startup)
uv run --active pyinstaller --onefile --name cdisplayagain cdisplayagain.py
Expand All @@ -70,6 +70,37 @@ build-onedir: ## Build onedir bundle (faster startup than onefile)
--name cdisplayagain \
cdisplayagain.py

macos-app: clean-build ## Build a macOS app bundle associated with CBZ/CBR files
@if [ "$$(uname -s)" != "Darwin" ]; then echo "macos-app requires macOS"; exit 1; fi
mkdir -p build/cdisplayagain.iconset
sips -z 16 16 cdisplayagain.png --out build/cdisplayagain.iconset/icon_16x16.png >/dev/null
sips -z 32 32 cdisplayagain.png --out build/cdisplayagain.iconset/icon_16x16@2x.png >/dev/null
sips -z 32 32 cdisplayagain.png --out build/cdisplayagain.iconset/icon_32x32.png >/dev/null
sips -z 64 64 cdisplayagain.png --out build/cdisplayagain.iconset/icon_32x32@2x.png >/dev/null
sips -z 128 128 cdisplayagain.png --out build/cdisplayagain.iconset/icon_128x128.png >/dev/null
sips -z 256 256 cdisplayagain.png --out build/cdisplayagain.iconset/icon_128x128@2x.png >/dev/null
sips -z 256 256 cdisplayagain.png --out build/cdisplayagain.iconset/icon_256x256.png >/dev/null
sips -z 512 512 cdisplayagain.png --out build/cdisplayagain.iconset/icon_256x256@2x.png >/dev/null
sips -z 512 512 cdisplayagain.png --out build/cdisplayagain.iconset/icon_512x512.png >/dev/null
sips -z 1024 1024 cdisplayagain.png --out build/cdisplayagain.iconset/icon_512x512@2x.png >/dev/null
iconutil -c icns build/cdisplayagain.iconset -o build/cdisplayagain.icns
uv run --active pyinstaller --noconfirm cdisplayagain-macos.spec

macos-install: macos-app ## Install the macOS app and register it with Launch Services
mkdir -p "$(HOME)/Applications"
ditto dist/cdisplayagain.app "$(HOME)/Applications/cdisplayagain.app"
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \
-f "$(HOME)/Applications/cdisplayagain.app"
@if command -v duti >/dev/null 2>&1; then \
duti -s com.cdisplayagain.viewer .cbz all; \
duti -s com.cdisplayagain.viewer .cbr all; \
else \
echo "Install duti to make cdisplayagain the default CBZ/CBR opener: brew install duti"; \
fi
@echo "Installed $(HOME)/Applications/cdisplayagain.app"

linux-install: build-onedir install ## Build and install the Linux CBZ/CBR file association


install: install-bin install-desktop ## Install everything

Expand All @@ -94,11 +125,17 @@ install-bin: ## Install binary to system

install-desktop: ## Install desktop entry
mkdir -p $(HOME)/.local/share/applications
mkdir -p $(HOME)/.local/share/mime/packages
install -m 0644 packaging/linux/cdisplayagain.xml $(HOME)/.local/share/mime/packages/cdisplayagain.xml
update-mime-database $(HOME)/.local/share/mime
mkdir -p $(HOME)/.local/share/icons/hicolor/1024x1024/apps
install -m 0644 cdisplayagain.png $(HOME)/.local/share/icons/hicolor/1024x1024/apps/cdisplayagain.png
printf '%s\n' \
'[Desktop Entry]' \
'Type=Application' \
'Name=cdisplayagain' \
"Exec=$(BINDIR)/cdisplayagain %f" \
'Icon=cdisplayagain' \
'Terminal=false' \
'Categories=Graphics;Viewer;' \
'MimeType=application/x-cbz;application/x-cbr;' \
Expand Down
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,18 @@ curl -Ls https://astral.sh/uv/install.sh | sh

### macOS Setup

**Note:** Recent Python versions on macOS no longer include tkinter by default.
You must install it separately:
The viewer needs a Python build with Tk support. `uv`'s managed Python includes
the Tk extension; install the project-supported interpreter and recreate the
virtual environment with it:

```bash
brew install python-tk
uv python install 3.13.11
uv venv --clear --python 3.13.11 --managed-python
uv sync
```

Do not install `tkinter` with pip: it is part of Python, not a PyPI package.

If you encounter issues with the UI not responding or appearing, try:
```bash
export TK_SILENCE_DEPRECATION=1
Expand All @@ -96,6 +101,24 @@ Or via the Makefile:
make run FILE=path/to/comic.cbz
```

To associate comic files with cdisplayagain:

```bash
# macOS
make macos-install

# Linux
make linux-install
```

The macOS target installs an app bundle in `~/Applications` and registers CBZ
and CBR files with Launch Services. The Linux target installs the executable,
desktop entry, MIME definitions, and the existing cdisplayagain icon.

On macOS, `duti` is used to select cdisplayagain as the default opener when it
is installed (`brew install duti`). Otherwise choose cdisplayagain once via
Finder's **Open With → Other…**, then enable **Always Open With**.

While viewing, navigate with the arrow keys, scroll wheel, or spacebar,
and use `Esc` or `q` to close the window.

Expand Down
88 changes: 88 additions & 0 deletions cdisplayagain-macos.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# -*- mode: python ; coding: utf-8 -*-

from PyInstaller.utils.hooks import collect_submodules


hiddenimports = collect_submodules("PIL")

a = Analysis(
["cdisplayagain.py"],
pathex=["."],
binaries=[],
datas=[],
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
[],
name="cdisplayagain",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
argv_emulation=True,
exclude_binaries=True,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name="cdisplayagain",
)
app = BUNDLE(
coll,
name="cdisplayagain.app",
icon="build/cdisplayagain.icns",
bundle_identifier="com.cdisplayagain.viewer",
info_plist={
"CFBundleDisplayName": "cdisplayagain",
"CFBundleDocumentTypes": [
{
"CFBundleTypeExtensions": ["cbz"],
"CFBundleTypeName": "Comic Book ZIP",
"CFBundleTypeRole": "Viewer",
"LSHandlerRank": "Owner",
"LSItemContentTypes": ["com.cdisplayagain.cbz"],
},
{
"CFBundleTypeExtensions": ["cbr"],
"CFBundleTypeName": "Comic Book RAR",
"CFBundleTypeRole": "Viewer",
"LSHandlerRank": "Owner",
"LSItemContentTypes": ["com.cdisplayagain.cbr"],
},
],
"UTExportedTypeDeclarations": [
{
"UTTypeConformsTo": ["public.archive"],
"UTTypeDescription": "Comic Book ZIP archive",
"UTTypeIdentifier": "com.cdisplayagain.cbz",
"UTTypeTagSpecification": {
"public.filename-extension": ["cbz"],
"public.mime-type": "application/x-cbz",
},
},
{
"UTTypeConformsTo": ["public.archive"],
"UTTypeDescription": "Comic Book RAR archive",
"UTTypeIdentifier": "com.cdisplayagain.cbr",
"UTTypeTagSpecification": {
"public.filename-extension": ["cbr"],
"public.mime-type": "application/x-cbr",
},
},
],
},
)
16 changes: 15 additions & 1 deletion cdisplayagain.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
from typing import cast

try:
from tk_bootstrap import configure_tk_library

configure_tk_library()
import tkinter as tk
from tkinter import filedialog, messagebox
except ImportError as e: # pragma: no cover
Expand Down Expand Up @@ -76,7 +79,18 @@ def _as_wm(obj: tk.Misc) -> tk.Wm:
("All files", "*.*"),
]

LOG_ROOT = Path(os.environ.get("CDISPLAYAGAIN_LOG_DIR", "logs")).expanduser()

def _get_log_root() -> Path:
"""Return a writable log directory for source and packaged launches."""
configured_root = os.environ.get("CDISPLAYAGAIN_LOG_DIR")
if configured_root:
return Path(configured_root).expanduser()
if sys.platform == "darwin":
return Path.home() / "Library" / "Logs" / "cdisplayagain"
return Path("logs")


LOG_ROOT = _get_log_root()
LOG_PATH: Path | None = None


Expand Down
11 changes: 11 additions & 0 deletions packaging/linux/cdisplayagain.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-cbz">
<comment>Comic Book ZIP archive</comment>
<glob pattern="*.cbz"/>
</mime-type>
<mime-type type="application/x-cbr">
<comment>Comic Book RAR archive</comment>
<glob pattern="*.cbr"/>
</mime-type>
</mime-info>
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pythonpath = ["."]
include = ["."]
stubPath = "stubs"

[tool.uv]
python-preference = "managed"

[tool.ruff]
line-length = 100
target-version = "py313"
Expand Down
9 changes: 8 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@
import logging
import threading
import time
import tkinter as tk
from unittest.mock import patch

import pytest
from PIL import Image

try:
from tk_bootstrap import configure_tk_library

configure_tk_library()
import tkinter as tk
except ImportError:
raise

import cdisplayagain


Expand Down
9 changes: 9 additions & 0 deletions tests/test_main_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ def _write_image(path: Path) -> None:
img.save(path)


def test_get_log_root_uses_user_directory_for_macos_bundle(monkeypatch, tmp_path):
"""Packaged macOS launches must not write logs relative to the working directory."""
monkeypatch.setattr(cdisplayagain.sys, "platform", "darwin")
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.delenv("CDISPLAYAGAIN_LOG_DIR", raising=False)

assert cdisplayagain._get_log_root() == tmp_path / "Library" / "Logs" / "cdisplayagain"


def test_main_function_with_file_argument(monkeypatch, tmp_path):
"""Test main function with command line file argument."""
_write_image(tmp_path / "test.png")
Expand Down
21 changes: 21 additions & 0 deletions tk_bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Configure Tcl/Tk paths for relocatable Python distributions."""

from __future__ import annotations

import os
import sys
from pathlib import Path


def configure_tk_library() -> None:
"""Point Python at bundled Tcl/Tk libraries when they are available."""
python_prefix = Path(sys.base_prefix)
tcl_library = python_prefix / "lib" / "tcl8.6"
tk_library = python_prefix / "lib" / "tk8.6"
if tcl_library.is_dir():
os.environ.setdefault("TCL_LIBRARY", str(tcl_library))
if tk_library.is_dir():
os.environ.setdefault("TK_LIBRARY", str(tk_library))


configure_tk_library()
Loading