feat: add --overlay-font-family option for caption font customization
Allow users to specify a custom font family for the overlay captions via a new --overlay-font-family flag, defaulting to "Sans". Validation ensures the font family is not empty. Updated README, config, and tests.
This commit is contained in:
@@ -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-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-opacity=0.90` | `0.90` | Dark bubble opacity from `0` to `1`. |
|
||||||
| `--overlay-font-size=28` | `28` | Font size in logical pixels. |
|
| `--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-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-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`. |
|
| `--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:
|
Examples:
|
||||||
|
|
||||||
```bash
|
```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 \
|
./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
|
# Explicit XWayland/X11 route on the cursor's monitor
|
||||||
./scripts/distrobox/run.sh --mode=vad --output=both --overlay-backend=x11
|
./scripts/distrobox/run.sh --mode=vad --output=both --overlay-backend=x11
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ package config
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultModelsDir = "models"
|
DefaultModelsDir = "models"
|
||||||
|
DefaultOverlayFontFamily = "Sans"
|
||||||
MinimumOverlayFinalLifetime = 3 * time.Second
|
MinimumOverlayFinalLifetime = 3 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,6 +62,7 @@ type OverlaySettings struct {
|
|||||||
Backend OverlayBackend
|
Backend OverlayBackend
|
||||||
Opacity float64
|
Opacity float64
|
||||||
FontSize int
|
FontSize int
|
||||||
|
FontFamily string
|
||||||
BottomMargin int
|
BottomMargin int
|
||||||
MaxWidth int
|
MaxWidth int
|
||||||
Monitor string
|
Monitor string
|
||||||
@@ -88,6 +91,7 @@ func ParseDesktop() DesktopSettings {
|
|||||||
flags.StringVar(&backend, "overlay-backend", "auto", "display backend: auto, wayland, or x11")
|
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.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.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.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.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")
|
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 {
|
if s.Overlay.FontSize <= 0 {
|
||||||
return errors.New("--overlay-font-size must be positive")
|
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 {
|
if s.Overlay.BottomMargin < 0 {
|
||||||
return errors.New("--overlay-bottom-margin cannot be negative")
|
return errors.New("--overlay-bottom-margin cannot be negative")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func validDesktopSettings() DesktopSettings {
|
|||||||
Settings: Settings{Mode: ModeVAD, ChunkDuration: time.Second, Threads: 2},
|
Settings: Settings{Mode: ModeVAD, ChunkDuration: time.Second, Threads: 2},
|
||||||
Output: OutputOverlay,
|
Output: OutputOverlay,
|
||||||
Overlay: OverlaySettings{
|
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,
|
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 }},
|
{"negative opacity", func(s *DesktopSettings) { s.Overlay.Opacity = -0.1 }},
|
||||||
{"large opacity", func(s *DesktopSettings) { s.Overlay.Opacity = 1.1 }},
|
{"large opacity", func(s *DesktopSettings) { s.Overlay.Opacity = 1.1 }},
|
||||||
{"zero font", func(s *DesktopSettings) { s.Overlay.FontSize = 0 }},
|
{"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 }},
|
{"negative margin", func(s *DesktopSettings) { s.Overlay.BottomMargin = -1 }},
|
||||||
{"zero width", func(s *DesktopSettings) { s.Overlay.MaxWidth = 0 }},
|
{"zero width", func(s *DesktopSettings) { s.Overlay.MaxWidth = 0 }},
|
||||||
{"empty monitor", func(s *DesktopSettings) { s.Overlay.Monitor = "" }},
|
{"empty monitor", func(s *DesktopSettings) { s.Overlay.Monitor = "" }},
|
||||||
|
|||||||
@@ -246,10 +246,27 @@ static gboolean captioneer_window_close(GtkWindow *window, gpointer data) {
|
|||||||
return TRUE;
|
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(
|
static CaptioneerOverlay *captioneer_overlay_new(
|
||||||
const char *backend,
|
const char *backend,
|
||||||
double opacity,
|
double opacity,
|
||||||
int font_size,
|
int font_size,
|
||||||
|
const char *font_family,
|
||||||
int bottom_margin,
|
int bottom_margin,
|
||||||
int max_width,
|
int max_width,
|
||||||
const char *monitor_selection,
|
const char *monitor_selection,
|
||||||
@@ -372,17 +389,19 @@ static CaptioneerOverlay *captioneer_overlay_new(
|
|||||||
gtk_widget_add_css_class(overlay->preview_label, "captioneer-preview");
|
gtk_widget_add_css_class(overlay->preview_label, "captioneer-preview");
|
||||||
gtk_window_set_child(overlay->window, box);
|
gtk_window_set_child(overlay->window, box);
|
||||||
|
|
||||||
|
char *font_family_rule = captioneer_font_family_rule(font_family);
|
||||||
char *css = g_strdup_printf(
|
char *css = g_strdup_printf(
|
||||||
"window.captioneer-overlay { background-color: transparent; box-shadow: none; }"
|
"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-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-final { color: rgba(255, 255, 255, 1.0); font-size: %dpx; %s }"
|
||||||
".captioneer-preview { color: rgba(255, 255, 255, 0.78); font-size: %dpx; }",
|
".captioneer-preview { color: rgba(255, 255, 255, 0.78); font-size: %dpx; %s }",
|
||||||
opacity, font_size, font_size);
|
opacity, font_size, font_family_rule, font_size, font_family_rule);
|
||||||
GtkCssProvider *provider = gtk_css_provider_new();
|
GtkCssProvider *provider = gtk_css_provider_new();
|
||||||
gtk_css_provider_load_from_string(provider, css);
|
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);
|
gtk_style_context_add_provider_for_display(display, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||||
g_object_unref(provider);
|
g_object_unref(provider);
|
||||||
g_free(css);
|
g_free(css);
|
||||||
|
g_free(font_family_rule);
|
||||||
|
|
||||||
gtk_widget_realize(GTK_WIDGET(overlay->window));
|
gtk_widget_realize(GTK_WIDGET(overlay->window));
|
||||||
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(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) {
|
func New(settings config.OverlaySettings) (*Sink, string, error) {
|
||||||
backend := C.CString(string(settings.Backend))
|
backend := C.CString(string(settings.Backend))
|
||||||
|
fontFamily := C.CString(settings.FontFamily)
|
||||||
monitor := C.CString(settings.Monitor)
|
monitor := C.CString(settings.Monitor)
|
||||||
defer C.free(unsafe.Pointer(backend))
|
defer C.free(unsafe.Pointer(backend))
|
||||||
|
defer C.free(unsafe.Pointer(fontFamily))
|
||||||
defer C.free(unsafe.Pointer(monitor))
|
defer C.free(unsafe.Pointer(monitor))
|
||||||
|
|
||||||
var warningMessage *C.char
|
var warningMessage *C.char
|
||||||
@@ -475,6 +496,7 @@ func New(settings config.OverlaySettings) (*Sink, string, error) {
|
|||||||
backend,
|
backend,
|
||||||
C.double(settings.Opacity),
|
C.double(settings.Opacity),
|
||||||
C.int(settings.FontSize),
|
C.int(settings.FontSize),
|
||||||
|
fontFamily,
|
||||||
C.int(settings.BottomMargin),
|
C.int(settings.BottomMargin),
|
||||||
C.int(settings.MaxWidth),
|
C.int(settings.MaxWidth),
|
||||||
monitor,
|
monitor,
|
||||||
|
|||||||
Reference in New Issue
Block a user