diff --git a/README.md b/README.md index 1baa2d0..b21b279 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ Layer Shell and X11 hints are requests, not universal guarantees. Same-layer win | `--overlay-backend=auto|wayland|x11` | `auto` | Let GTK detect the backend, or require one explicitly. | | `--overlay-opacity=0.90` | `0.90` | Dark bubble opacity from `0` to `1`. | | `--overlay-font-size=28` | `28` | Font size in logical pixels. | +| `--overlay-font-family=Sans` | `Sans` | GTK/Pango font family for final and provisional captions. | | `--overlay-bottom-margin=100` | `100` | Bottom margin in logical pixels. | | `--overlay-max-width=1200` | `1200` | Fixed caption-bubble width; it is capped at 80% of the selected monitor. | | `--overlay-monitor=auto` | `auto` | `auto`, a zero-based monitor index, or a connector name such as `DP-1`. | @@ -114,9 +115,9 @@ Layer Shell and X11 hints are requests, not universal guarantees. Same-layer win Examples: ```bash -# Larger text, longer final-caption visibility +# Larger text, a custom font, and longer final-caption visibility ./scripts/distrobox/run.sh --mode=vad --output=overlay \ - --overlay-font-size=34 --overlay-final-timeout=6s + --overlay-font-size=34 --overlay-font-family="Noto Sans" --overlay-final-timeout=6s # Explicit XWayland/X11 route on the cursor's monitor ./scripts/distrobox/run.sh --mode=vad --output=both --overlay-backend=x11 diff --git a/src/config/config.go b/src/config/config.go index e35de52..bd56c6b 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -4,11 +4,13 @@ package config import ( "errors" "flag" + "strings" "time" ) const ( DefaultModelsDir = "models" + DefaultOverlayFontFamily = "Sans" MinimumOverlayFinalLifetime = 3 * time.Second ) @@ -60,6 +62,7 @@ type OverlaySettings struct { Backend OverlayBackend Opacity float64 FontSize int + FontFamily string BottomMargin int MaxWidth int Monitor string @@ -88,6 +91,7 @@ func ParseDesktop() DesktopSettings { flags.StringVar(&backend, "overlay-backend", "auto", "display backend: auto, wayland, or x11") flags.Float64Var(&desktop.Overlay.Opacity, "overlay-opacity", 0.90, "caption background opacity from 0 to 1") flags.IntVar(&desktop.Overlay.FontSize, "overlay-font-size", 28, "caption font size in logical pixels") + flags.StringVar(&desktop.Overlay.FontFamily, "overlay-font-family", DefaultOverlayFontFamily, "caption font family") flags.IntVar(&desktop.Overlay.BottomMargin, "overlay-bottom-margin", 100, "distance from the monitor bottom in logical pixels") flags.IntVar(&desktop.Overlay.MaxWidth, "overlay-max-width", 1200, "fixed caption width in logical pixels before the monitor safety cap") flags.StringVar(&desktop.Overlay.Monitor, "overlay-monitor", "auto", "monitor selection: auto, numeric index, or connector name") @@ -129,6 +133,9 @@ func (s DesktopSettings) Validate() error { if s.Overlay.FontSize <= 0 { return errors.New("--overlay-font-size must be positive") } + if strings.TrimSpace(s.Overlay.FontFamily) == "" { + return errors.New("--overlay-font-family cannot be empty") + } if s.Overlay.BottomMargin < 0 { return errors.New("--overlay-bottom-margin cannot be negative") } diff --git a/src/config/config_test.go b/src/config/config_test.go index bbc8e31..a709438 100644 --- a/src/config/config_test.go +++ b/src/config/config_test.go @@ -20,7 +20,7 @@ func validDesktopSettings() DesktopSettings { Settings: Settings{Mode: ModeVAD, ChunkDuration: time.Second, Threads: 2}, Output: OutputOverlay, Overlay: OverlaySettings{ - Backend: BackendAuto, Opacity: 0.9, FontSize: 28, BottomMargin: 100, + Backend: BackendAuto, Opacity: 0.9, FontSize: 28, FontFamily: DefaultOverlayFontFamily, BottomMargin: 100, MaxWidth: 1200, Monitor: "auto", FinalTimeout: 4 * time.Second, ClickThrough: true, }, } @@ -40,6 +40,7 @@ func TestDesktopSettingsValidate(t *testing.T) { {"negative opacity", func(s *DesktopSettings) { s.Overlay.Opacity = -0.1 }}, {"large opacity", func(s *DesktopSettings) { s.Overlay.Opacity = 1.1 }}, {"zero font", func(s *DesktopSettings) { s.Overlay.FontSize = 0 }}, + {"empty font family", func(s *DesktopSettings) { s.Overlay.FontFamily = " " }}, {"negative margin", func(s *DesktopSettings) { s.Overlay.BottomMargin = -1 }}, {"zero width", func(s *DesktopSettings) { s.Overlay.MaxWidth = 0 }}, {"empty monitor", func(s *DesktopSettings) { s.Overlay.Monitor = "" }}, diff --git a/src/output/overlay/overlay_gtk.go b/src/output/overlay/overlay_gtk.go index 441bb67..f43bd4c 100644 --- a/src/output/overlay/overlay_gtk.go +++ b/src/output/overlay/overlay_gtk.go @@ -246,10 +246,27 @@ static gboolean captioneer_window_close(GtkWindow *window, gpointer data) { return TRUE; } +static char *captioneer_font_family_rule(const char *font_family) { + GString *rule = g_string_new("font-family: \""); + for (const char *character = font_family; *character != '\0'; character++) { + if (*character == '\\' || *character == '\"') { + g_string_append_c(rule, '\\'); + } + if (*character == '\n' || *character == '\r' || *character == '\t') { + g_string_append_c(rule, ' '); + } else { + g_string_append_c(rule, *character); + } + } + g_string_append(rule, "\";"); + return g_string_free(rule, FALSE); +} + static CaptioneerOverlay *captioneer_overlay_new( const char *backend, double opacity, int font_size, + const char *font_family, int bottom_margin, int max_width, const char *monitor_selection, @@ -372,17 +389,19 @@ static CaptioneerOverlay *captioneer_overlay_new( gtk_widget_add_css_class(overlay->preview_label, "captioneer-preview"); gtk_window_set_child(overlay->window, box); + char *font_family_rule = captioneer_font_family_rule(font_family); char *css = g_strdup_printf( "window.captioneer-overlay { background-color: transparent; box-shadow: none; }" ".captioneer-bubble { background-color: rgba(0, 0, 0, %.3f); border-radius: 12px; padding: 14px 22px; }" - ".captioneer-final { color: rgba(255, 255, 255, 1.0); font-size: %dpx; }" - ".captioneer-preview { color: rgba(255, 255, 255, 0.78); font-size: %dpx; }", - opacity, font_size, font_size); + ".captioneer-final { color: rgba(255, 255, 255, 1.0); font-size: %dpx; %s }" + ".captioneer-preview { color: rgba(255, 255, 255, 0.78); font-size: %dpx; %s }", + opacity, font_size, font_family_rule, font_size, font_family_rule); GtkCssProvider *provider = gtk_css_provider_new(); gtk_css_provider_load_from_string(provider, css); gtk_style_context_add_provider_for_display(display, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_object_unref(provider); g_free(css); + g_free(font_family_rule); gtk_widget_realize(GTK_WIDGET(overlay->window)); GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window)); @@ -465,8 +484,10 @@ type Sink struct { func New(settings config.OverlaySettings) (*Sink, string, error) { backend := C.CString(string(settings.Backend)) + fontFamily := C.CString(settings.FontFamily) monitor := C.CString(settings.Monitor) defer C.free(unsafe.Pointer(backend)) + defer C.free(unsafe.Pointer(fontFamily)) defer C.free(unsafe.Pointer(monitor)) var warningMessage *C.char @@ -475,6 +496,7 @@ func New(settings config.OverlaySettings) (*Sink, string, error) { backend, C.double(settings.Opacity), C.int(settings.FontSize), + fontFamily, C.int(settings.BottomMargin), C.int(settings.MaxWidth), monitor,