dotfiles/talon/user/community/plugin/subtitles/subtitles.py

151 lines
3.8 KiB
Python

from typing import Any, Callable, Optional, Sequence, Type
from talon import Module, app, cron, ctrl, settings, ui
from talon.canvas import Canvas
from talon.skia.canvas import Canvas as SkiaCanvas
from talon.skia.imagefilter import ImageFilter
from talon.types import Rect
mod = Module()
def setting(
name: str, type: Type, desc: str, *, default: Optional[Any] = None
) -> Callable[[], type]:
mod.setting(f"subtitles_{name}", type, default=default, desc=f"Subtitles: {desc}")
return lambda: settings.get(f"user.subtitles_{name}")
setting_show = setting(
"show",
bool,
"If true show (custom) subtitles",
default=False,
)
setting_screens = setting(
"screens",
str,
"Show on which screens: 'all', 'main', 'cursor', 'focus'",
)
setting_size = setting(
"size",
int,
"Subtitle size in pixels",
)
setting_color = setting(
"color",
str,
"Subtitle color",
)
setting_color_outline = setting(
"color_outline",
str,
"Subtitle outline color",
)
setting_timeout_per_char = setting(
"timeout_per_char",
int,
"For each character in the subtitle extend the timeout by this amount in ms",
)
setting_timeout_min = setting(
"timeout_min",
int,
"Minimum time for a subtitle to show in ms",
)
setting_timeout_max = setting(
"timeout_max",
int,
"Maximum time for a subtitle to show in ms",
)
setting_y = setting(
"y",
float,
"Percentage of screen hight to show subtitle at. 0=top, 1=bottom",
)
mod = Module()
canvases: list[Canvas] = []
def show_subtitle(text: str):
"""Show subtitle"""
if not setting_show():
return
clear_canvases()
screens = get_screens()
for screen in screens:
canvas = show_text_on_screen(screen, text)
canvases.append(canvas)
def get_screens() -> Sequence[ui.Screen]:
screen = setting_screens()
match screen:
case "main":
return [ui.main_screen()]
case "all":
return ui.screens()
case "cursor":
x, y = ctrl.mouse_pos()
return [ui.screen_containing(x, y)]
case "focus":
return [ui.active_window().screen]
case _:
raise ValueError(f"Unknown screen setting: {screen}")
def show_text_on_screen(screen: ui.Screen, text: str):
timeout = calculate_timeout(text)
canvas = Canvas.from_screen(screen)
canvas.register("draw", lambda c: on_draw(c, screen, text))
canvas.freeze()
cron.after(f"{timeout}ms", canvas.close)
return canvas
def on_draw(c: SkiaCanvas, screen: ui.Screen, text: str):
scale = screen.scale if app.platform != "mac" else 1
size = setting_size() * scale
rect = set_text_size_and_get_rect(c, size, text)
x = c.rect.center.x - rect.center.x
# Clamp coordinate to make sure entire text is visible
y = max(
min(
c.rect.y + setting_y() * c.rect.height + c.paint.textsize / 2,
c.rect.bot - rect.bot,
),
c.rect.top - rect.top,
)
c.paint.imagefilter = ImageFilter.drop_shadow(2, 2, 1, 1, "000000")
c.paint.style = c.paint.Style.FILL
c.paint.color = setting_color()
c.draw_text(text, x, y)
# Outline
c.paint.imagefilter = None
c.paint.style = c.paint.Style.STROKE
c.paint.color = setting_color_outline()
c.draw_text(text, x, y)
def calculate_timeout(text: str) -> int:
ms_per_char = setting_timeout_per_char()
ms_min = setting_timeout_min()
ms_max = setting_timeout_max()
return min(ms_max, max(ms_min, len(text) * ms_per_char))
def set_text_size_and_get_rect(c: SkiaCanvas, size: int, text: str) -> Rect:
while True:
c.paint.textsize = size
rect = c.paint.measure_text(text)[1]
if rect.width < c.width * 0.8:
return rect
size *= 0.9
def clear_canvases():
for canvas in canvases:
canvas.close()
canvases.clear()