From 13192370f5dbeeeb7aba3dafe9461d7a2cc205b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=E1=BB=B3nh=20Thi=E1=BB=87n=20L=E1=BB=99c?= Date: Fri, 11 Sep 2026 21:05:11 +0700 Subject: [PATCH] feat(screenshot): add flag to control HUD scale and visibility (#3398) * feat(screenshot): add --hud flag to control HUD scale and visibility * fix(screenshot): clamp HUD scale to 1-4 and clarify flag description --- core/cmd/dms/commands_screenshot.go | 3 ++ core/internal/screenshot/region.go | 7 +-- core/internal/screenshot/region_render.go | 33 ++++++++++++- .../internal/screenshot/region_resize_test.go | 47 +++++++++++++++++++ core/internal/screenshot/types.go | 2 + 5 files changed, 88 insertions(+), 4 deletions(-) diff --git a/core/cmd/dms/commands_screenshot.go b/core/cmd/dms/commands_screenshot.go index f8362c286..7c4d883a2 100644 --- a/core/cmd/dms/commands_screenshot.go +++ b/core/cmd/dms/commands_screenshot.go @@ -35,6 +35,7 @@ var ( ssJSON bool ssGeometry bool ssAllowMulti bool + ssHUD string ) type screenshotMetadata struct { @@ -181,6 +182,7 @@ func init() { screenshotCmd.PersistentFlags().BoolVar(&ssJSON, "json", false, "Print capture metadata as JSON") screenshotCmd.PersistentFlags().BoolVarP(&ssGeometry, "geometry", "g", false, "Print selected region geometry (X,Y WxH) to stdout without capturing an image") screenshotCmd.PersistentFlags().BoolVar(&ssAllowMulti, "allow-multiple", false, "Open a selector even when another one is already open") + screenshotCmd.PersistentFlags().StringVar(&ssHUD, "hud", "auto", "HUD overlay scale in region selector (auto, off/0, or integer scale 1-4)") ssScrollCmd.Flags().IntVar(&ssScrollInterval, "interval", 45, "Capture interval in milliseconds (30-1000)") @@ -212,6 +214,7 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config { config.Stdout = ssStdout config.Geometry = ssGeometry config.AllowMultiple = ssAllowMulti + config.HUD = ssHUD if ssGeometry { config.Clipboard = false diff --git a/core/internal/screenshot/region.go b/core/internal/screenshot/region.go index ecc088131..7247d86ac 100644 --- a/core/internal/screenshot/region.go +++ b/core/internal/screenshot/region.go @@ -973,11 +973,12 @@ func (r *RegionSelector) renderSurface(os *OutputSurface) { case !slot.cacheValid(srcBuf, r.selection.dragging, r.showCapturedCursor, r.phase, handles, shift): slot.shm.CopyFrom(srcBuf) r.dimBackground(slot.shm) - scale := 1 + effectiveScale := 1.0 if os.output != nil { - scale = scaleFactor(os.output.effectiveScale()) + effectiveScale = os.output.effectiveScale() } - r.drawHUD(slot.shm.Data(), slot.shm.Stride, slot.shm.Width, slot.shm.Height, os.screenFormat, scale) + hudScale := r.hudScale(effectiveScale) + r.drawHUD(slot.shm.Data(), slot.shm.Stride, slot.shm.Width, slot.shm.Height, os.screenFormat, hudScale) slot.backgroundInitialized = true slot.backgroundSource = srcBuf slot.backgroundDragging = r.selection.dragging diff --git a/core/internal/screenshot/region_render.go b/core/internal/screenshot/region_render.go index b0184ecd9..1b7977baa 100644 --- a/core/internal/screenshot/region_render.go +++ b/core/internal/screenshot/region_render.go @@ -4,6 +4,8 @@ import ( "fmt" "math" "slices" + "strconv" + "strings" ) const ( @@ -630,6 +632,32 @@ func (r *RegionSelector) drawScrollBar(data []byte, stride, bufW, bufH int, form style.TextR, style.TextG, style.TextB, format) } +const ( + minHUDScale = 1 + maxHUDScale = 4 +) + +func (r *RegionSelector) hudScale(effectiveScale float64) int { + hudOpt := "auto" + if r.screenshoter != nil && r.screenshoter.config.HUD != "" { + hudOpt = strings.ToLower(strings.TrimSpace(r.screenshoter.config.HUD)) + } + switch hudOpt { + case "off", "none", "false", "0": + return 0 + case "", "auto": + return min(scaleFactor(effectiveScale), maxHUDScale) + default: + if val, err := strconv.ParseFloat(hudOpt, 64); err == nil { + if val <= 0 { + return 0 + } + return min(max(scaleFactor(val), minHUDScale), maxHUDScale) + } + return min(scaleFactor(effectiveScale), maxHUDScale) + } +} + type hudItem struct { key, desc string } @@ -657,6 +685,9 @@ func (r *RegionSelector) hudItems() []hudItem { } func (r *RegionSelector) hudDimensions(bufW, bufH, scale int) (hudX, hudY, hudW, hudH int) { + if scale <= 0 { + return 0, 0, 0, 0 + } scale = scaleFactor(float64(scale)) charH := fontCharH * scale padding, itemSpacing := 12*scale, 24*scale @@ -680,7 +711,7 @@ func (r *RegionSelector) hudDimensions(bufW, bufH, scale int) (hudX, hudY, hudW, } func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uint32, scale int) { - if r.selection.dragging { + if r.selection.dragging || scale <= 0 { return } diff --git a/core/internal/screenshot/region_resize_test.go b/core/internal/screenshot/region_resize_test.go index 264108c83..b8388d28f 100644 --- a/core/internal/screenshot/region_resize_test.go +++ b/core/internal/screenshot/region_resize_test.go @@ -238,6 +238,12 @@ func TestHUDDimensionsContainText(t *testing.T) { }, } + // When scale <= 0, hudDimensions should return zero + hx, hy, hw, hh := r.hudDimensions(bufW, bufH, 0) + if hx != 0 || hy != 0 || hw != 0 || hh != 0 { + t.Fatalf("expected 0 dimensions for scale 0, got %d, %d, %d, %d", hx, hy, hw, hh) + } + for _, scale := range []int{1, 2} { hudX, hudY, hudW, hudH := r.hudDimensions(bufW, bufH, scale) data := make([]byte, bufH*stride) @@ -260,6 +266,47 @@ func TestHUDDimensionsContainText(t *testing.T) { } } +func TestHUDScale(t *testing.T) { + tests := []struct { + hudOpt string + effectiveScale float64 + expectedScale int + }{ + {"auto", 1.0, 1}, + {"auto", 1.8, 2}, + {"auto", 2.4, 2}, + {"", 1.0, 1}, + {"", 2.0, 2}, + {"off", 1.0, 0}, + {"OFF", 2.0, 0}, + {"none", 1.5, 0}, + {"false", 1.0, 0}, + {"0", 2.0, 0}, + {"0.0", 1.0, 0}, + {"-1", 1.0, 0}, + {"1", 1.0, 1}, + {"1.5", 1.0, 2}, + {"2", 1.0, 2}, + {"3", 1.0, 3}, + {"4", 1.0, 4}, + {"1000", 1.0, 4}, + {"0.2", 1.0, 1}, + {"invalid", 1.7, 2}, + } + + for _, tc := range tests { + r := &RegionSelector{ + screenshoter: &Screenshoter{ + config: Config{HUD: tc.hudOpt}, + }, + } + got := r.hudScale(tc.effectiveScale) + if got != tc.expectedScale { + t.Errorf("hudScale(%q, %v) = %d, expected %d", tc.hudOpt, tc.effectiveScale, got, tc.expectedScale) + } + } +} + func TestOverlayDeltaHandlesClampedMonitorEdge(t *testing.T) { prev := &overlay{ interior: dirtyRect{100, 100, 1920, 1080}, diff --git a/core/internal/screenshot/types.go b/core/internal/screenshot/types.go index 16c8cdd72..7b7a632e8 100644 --- a/core/internal/screenshot/types.go +++ b/core/internal/screenshot/types.go @@ -73,6 +73,7 @@ type Config struct { Geometry bool AllowMultiple bool IntervalMs int + HUD string // SelectorHook runs as the interactive selector starts (true) and ends (false). SelectorHook func(begin bool) } @@ -90,5 +91,6 @@ func DefaultConfig() Config { Clipboard: true, SaveFile: true, Notify: true, + HUD: "auto", } }