Skip to content

feat: add presets offsets#9

Open
ByKripsy wants to merge 1 commit into
adamk33n3r:mainfrom
ByKripsy:main
Open

feat: add presets offsets#9
ByKripsy wants to merge 1 commit into
adamk33n3r:mainfrom
ByKripsy:main

Conversation

@ByKripsy
Copy link
Copy Markdown

@ByKripsy ByKripsy commented Nov 4, 2025

Summary of Changes

1. New Feature: Presets for Screen Offsets

  • Adds the ability to define and select custom offset presets. This allows users to quickly switch between different screen configurations (e.g., Fullscreen, Half-left, Half-right) without manual input.

2. Fix: Reliable Screen Size Detection

  • This PR fixes the screen size by using EnumDisplaySettingsW to reliably retrieve the proper current screen resolution and boundaries.

Summary by CodeRabbit

  • Chores

    • Released patch version 1.3.2
  • Improvements

    • Refactored application settings interface for improved organization and maintainability
    • Enhanced display and monitor detection with extended settings support

add presets offsets
add using of EnumDisplaySettingsW to properly get screen size
fix validator of offsets
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 4, 2025

Walkthrough

Version update to 1.3.2 with refactored UI construction into helper functions and expanded display device settings via DEVMODE and EnumDisplaySettingsW for monitor dimension retrieval.

Changes

Cohort / File(s) Summary
Metadata Update
FyneApp.toml
Version bumped from 1.3.1 to 1.3.2; Build incremented from 79 to 81.
Validation Helper
gui_app.go
Added non-exported offsetIntValidator() function to validate offset inputs as integers, accepting empty strings as valid.
UI Refactoring
gui_appsetting.go
Introduced embed support for SVG/icon assets; created 11 new helper functions (isValidWindowForSelection, createApplicationSelect, createDisplaySelect, createMatchTypeRadio, createSizeButton, createOffsetEntry, createSizeEntry, createTextGrid, createPresetsContent, subscribeToWindowUpdates, getDefaultMonitorIndex) to modularize UI construction; refactored makeAppSettingWindow to delegate UI building logic to helpers; modified window subscription and selection reset behavior.
Display Settings Integration
winapi.go
Exported new DEVMODE type; introduced procEnumDisplaySettingsW to retrieve monitor dimensions (PelsWidth/PelsHeight) via EnumDisplaySettingsW, with fallback to monitor RECT; extended monitor data gathering to populate width, height, and coordinates from infoEx structure and DEVMODE values.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • gui_appsetting.go: Substantial refactoring with 11 new helper functions—verify contracts are well-defined, state management is preserved across helpers, and presets/window subscription logic functions correctly
  • winapi.go: DEVMODE struct integration and EnumDisplaySettingsW fallback logic—confirm proper initialization, error handling, and rect fallback behavior
  • Cross-module interaction: Verify that UI helpers in gui_appsetting.go correctly consume the expanded monitor data from winapi.go

Poem

🐰 A version hops from 1.3.1 to 1.3.2 with glee,
Helpers modularize the UI tree,
DEVMODE brings dimensions so bright,
Monitors measured with EnumDisplay might,
The codebase dances, refactored and free! ✨

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add presets offsets' directly aligns with the PR's main objective of adding a new feature for custom offset presets, enabling quick switching between predefined screen configurations.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebbda79 and 09cb665.

⛔ Files ignored due to path filters (4)
  • assets/fullscreen.svg is excluded by !**/*.svg
  • assets/halfleft.svg is excluded by !**/*.svg
  • assets/halfright.svg is excluded by !**/*.svg
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • FyneApp.toml (1 hunks)
  • gui_app.go (1 hunks)
  • gui_appsetting.go (6 hunks)
  • winapi.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
gui_appsetting.go (5)
main.go (1)
  • Window (19-23)
settings.go (2)
  • AppSetting (57-71)
  • Settings (86-94)
ui/select.go (2)
  • Select (14-18)
  • NewSelect (50-69)
winapi.go (1)
  • Monitor (96-103)
rx/observable.go (1)
  • Subscription (12-14)
🔇 Additional comments (10)
FyneApp.toml (1)

7-8: LGTM! Version bump aligns with PR objectives.

The version and build number updates correctly reflect the new preset offsets feature.

gui_app.go (1)

45-53: LGTM! Well-designed validator for optional numeric input.

The function correctly allows empty strings (defaulting to zero) while validating non-empty input as integers. This is appropriate for offset fields where zero is a valid default.

winapi.go (3)

110-110: LGTM! Enhanced monitor information display.

The addition of resolution information to the String() method improves usability by showing monitor dimensions alongside the display number.


114-138: LGTM! Proper Windows API integration.

The DEVMODE structure and EnumDisplaySettingsW proc are correctly defined for retrieving monitor display settings.


140-183: LGTM! Robust monitor detection with proper fallback.

The refactored getMonitors() function correctly:

  • Uses EnumDisplaySettingsW to get actual display resolution
  • Falls back to RECT-based calculation when needed
  • Populates monitor position (left, top) for multi-monitor support

This addresses the PR objective of fixing screen size detection.

gui_appsetting.go (5)

79-93: LGTM! Clean extraction of validation logic.

The refactoring of window validation into isValidWindowForSelection improves code organization and reusability. The validation logic correctly checks for windows with borders/captions.


95-111: LGTM! Excellent refactoring with modular helper functions.

The extraction of UI construction into dedicated helper functions significantly improves:

  • Code organization and readability
  • Maintainability and testability
  • Separation of concerns

The validators are correctly applied:

  • offsetIntValidator for optional offset fields (allows empty = 0)
  • intValidator for required size fields

Also applies to: 128-155, 172-258


260-284: LGTM! Thread-safe window list subscription.

The function correctly:

  • Only subscribes for new app settings
  • Uses fyne.Do to ensure UI updates on the main thread
  • Validates and clears selection when windows are removed

324-379: LGTM! Well-structured dialog creation with proper cleanup.

The refactored makeAppSettingWindow function:

  • Uses mutex correctly for thread-safe window list access
  • Properly unsubscribes from window updates on dialog close
  • Delegates UI construction to modular helper functions
  • Maintains clean separation of concerns

Note: The critical issues with preset button calculations (flagged in previous comments) should be addressed before merging.


315-322: Embedded asset files verified—no action required.

All three SVG files referenced by the //go:embed directives are present in the repository:

  • assets/fullscreen.svg
  • assets/halfleft.svg
  • assets/halfright.svg

No build failures will occur due to missing embedded assets.

Comment thread gui_appsetting.go
Comment on lines +113 to +126
func getDefaultMonitorIndex(settings *Settings, appSetting AppSetting, isNew bool) int {
if !isNew {
return appSetting.Monitor - 1
}

monitorIdx := settings.Defaults.Monitor - 1
if monitorIdx < 0 {
monitorIdx = slices.IndexFunc(monitors, func(m Monitor) bool {
return m.isPrimary
})
}

return monitorIdx
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Potential index out of bounds panic.

The function doesn't validate the monitor index before returning it:

  1. If appSetting.Monitor or settings.Defaults.Monitor is 0 or exceeds the number of monitors, the returned index will be invalid
  2. If no primary monitor is found, slices.IndexFunc returns -1
  3. Line 337 uses this index directly: selectedMonitor := monitors[monitorIdx] which will panic

Apply this defensive fix:

 func getDefaultMonitorIndex(settings *Settings, appSetting AppSetting, isNew bool) int {
+	if len(monitors) == 0 {
+		return 0
+	}
+	
 	if !isNew {
-		return appSetting.Monitor - 1
+		idx := appSetting.Monitor - 1
+		if idx >= 0 && idx < len(monitors) {
+			return idx
+		}
 	}
 
 	monitorIdx := settings.Defaults.Monitor - 1
-	if monitorIdx < 0 {
+	if monitorIdx < 0 || monitorIdx >= len(monitors) {
 		monitorIdx = slices.IndexFunc(monitors, func(m Monitor) bool {
 			return m.isPrimary
 		})
+		if monitorIdx == -1 {
+			monitorIdx = 0
+		}
 	}
 
 	return monitorIdx
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func getDefaultMonitorIndex(settings *Settings, appSetting AppSetting, isNew bool) int {
if !isNew {
return appSetting.Monitor - 1
}
monitorIdx := settings.Defaults.Monitor - 1
if monitorIdx < 0 {
monitorIdx = slices.IndexFunc(monitors, func(m Monitor) bool {
return m.isPrimary
})
}
return monitorIdx
}
func getDefaultMonitorIndex(settings *Settings, appSetting AppSetting, isNew bool) int {
if len(monitors) == 0 {
return 0
}
if !isNew {
idx := appSetting.Monitor - 1
if idx >= 0 && idx < len(monitors) {
return idx
}
}
monitorIdx := settings.Defaults.Monitor - 1
if monitorIdx < 0 || monitorIdx >= len(monitors) {
monitorIdx = slices.IndexFunc(monitors, func(m Monitor) bool {
return m.isPrimary
})
if monitorIdx == -1 {
monitorIdx = 0
}
}
return monitorIdx
}

Comment thread gui_appsetting.go
Comment on lines +157 to +170
func createSizeButton(monitor Monitor, xDivider int32, xOffset int32, label string, icon fyne.Resource) *widget.Button {
monitorWidth := monitor.width / xDivider
offsetWidth := int32(0)
if xOffset == 1 {
offsetWidth = monitorWidth
}
setOnFocusChanged(xOffsetText, func(focused bool) {
if focused {
xOffsetText.DoubleTapped(&fyne.PointEvent{})
}

return widget.NewButtonWithIcon(label, icon, func() {
widthText.SetText(strconv.Itoa(int(monitorWidth)))
heightText.SetText(strconv.Itoa(int(monitor.height)))
xOffsetText.SetText(strconv.Itoa(int(offsetWidth)))
yOffsetText.SetText("0")
})
xOffsetText.SetPlaceHolder("0")
if isNew {
xOffsetText.SetText(strconv.Itoa(int(settings.Defaults.OffsetX)))
} else {
xOffsetText.SetText(strconv.Itoa(int(appSetting.OffsetX)))
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Preset offsets don't account for monitor position in multi-monitor setups.

The preset buttons calculate offsets relative to (0,0) but don't include the monitor's actual position (monitor.left, monitor.top). This will cause incorrect positioning on secondary monitors.

For example, with a secondary monitor at position (1920, 0):

  • "Full" preset sets xOffset to 0, but should set it to 1920
  • "Half Right" preset sets xOffset to monitor.width/2, but should set it to 1920 + monitor.width/2

Apply this fix:

 func createSizeButton(monitor Monitor, xDivider int32, xOffset int32, label string, icon fyne.Resource) *widget.Button {
 	monitorWidth := monitor.width / xDivider
-	offsetWidth := int32(0)
+	offsetWidth := monitor.left
 	if xOffset == 1 {
-		offsetWidth = monitorWidth
+		offsetWidth = monitor.left + monitorWidth
 	}
 
 	return widget.NewButtonWithIcon(label, icon, func() {
 		widthText.SetText(strconv.Itoa(int(monitorWidth)))
 		heightText.SetText(strconv.Itoa(int(monitor.height)))
 		xOffsetText.SetText(strconv.Itoa(int(offsetWidth)))
-		yOffsetText.SetText("0")
+		yOffsetText.SetText(strconv.Itoa(int(monitor.top)))
 	})
 }
🤖 Prompt for AI Agents
In gui_appsetting.go around lines 157 to 170, the preset button handler sets x/y
offsets relative to (0,0) and ignores monitor.left/monitor.top; update the
offset calculation so xOffsetText gets the monitor-relative value and
yOffsetText gets monitor.top. Concretely, compute offsetWidth including
monitor.left (e.g. if xOffset==1 set offsetWidth = monitor.left + monitorWidth,
otherwise offsetWidth = monitor.left + 0) and use strconv.Itoa(int(offsetWidth))
for xOffsetText, and set yOffsetText to strconv.Itoa(int(monitor.top)) instead
of "0".

Comment thread gui_appsetting.go
Comment on lines +332 to +334
leftIcon := fyne.NewStaticResource("right.svg", halfLeftSVGBytes)
rightIcon := fyne.NewStaticResource("right.svg", halfRightSVGBytes)
fullIcon := fyne.NewStaticResource("full.svg", fullscreenSVGBytes)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: Copy-paste error in resource names.

Both leftIcon and rightIcon are named "right.svg", which is incorrect. While this may not cause functional issues, it harms code clarity and could cause confusion during debugging.

Apply this fix:

-	leftIcon := fyne.NewStaticResource("right.svg", halfLeftSVGBytes)
-	rightIcon := fyne.NewStaticResource("right.svg", halfRightSVGBytes)
+	leftIcon := fyne.NewStaticResource("halfleft.svg", halfLeftSVGBytes)
+	rightIcon := fyne.NewStaticResource("halfright.svg", halfRightSVGBytes)
 	fullIcon := fyne.NewStaticResource("full.svg", fullscreenSVGBytes)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
leftIcon := fyne.NewStaticResource("right.svg", halfLeftSVGBytes)
rightIcon := fyne.NewStaticResource("right.svg", halfRightSVGBytes)
fullIcon := fyne.NewStaticResource("full.svg", fullscreenSVGBytes)
leftIcon := fyne.NewStaticResource("halfleft.svg", halfLeftSVGBytes)
rightIcon := fyne.NewStaticResource("halfright.svg", halfRightSVGBytes)
fullIcon := fyne.NewStaticResource("full.svg", fullscreenSVGBytes)
🤖 Prompt for AI Agents
In gui_appsetting.go around lines 332 to 334, the StaticResource names are
incorrect: both leftIcon and rightIcon are given "right.svg"; rename leftIcon to
use "left.svg" (and ensure rightIcon remains "right.svg") while keeping fullIcon
as "full.svg" so the resource names accurately match their contents and improve
code clarity.

Comment thread gui_appsetting.go
Comment on lines +339 to +341
halfLeftBtn = createSizeButton(selectedMonitor, 2, 0, "Half Left", leftIcon)
halfRightBtn = createSizeButton(selectedMonitor, 2, 1, "Half Right", rightIcon)
fullBtn = createSizeButton(selectedMonitor, 1, 0, "Full", fullIcon)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Major: Preset buttons don't update when display selection changes.

The preset buttons (Half Left, Half Right, Full) are created once using the initial selectedMonitor. When the user changes the display selection via displaySelect, the buttons continue to use the original monitor's dimensions rather than updating to the newly selected monitor.

Consider recreating the buttons in the displaySelect onChange handler:

displaySelect = ui.NewSelect(monitors, func(selected Monitor) {
	appSetting.Monitor = selected.number
	
	// Recreate preset buttons with new monitor
	halfLeftBtn = createSizeButton(selected, 2, 0, "Half Left", leftIcon)
	halfRightBtn = createSizeButton(selected, 2, 1, "Half Right", rightIcon)
	fullBtn = createSizeButton(selected, 1, 0, "Full", fullIcon)
	// Refresh the UI...
	
	setConfirmButtonState(isNew)
})

Note: This would require restructuring the code to allow button recreation and UI refresh.

🤖 Prompt for AI Agents
In gui_appsetting.go around lines 339 to 341, the preset buttons are created
once with the initial selectedMonitor so they do not reflect changes when
displaySelect changes; update the displaySelect onChange handler to recreate the
halfLeftBtn, halfRightBtn and fullBtn using the newly selected monitor (call
createSizeButton with the new selected monitor and same params), replace the old
button references, and refresh the UI layout (re-add or re-render the button
container) so the new buttons use the updated monitor dimensions; ensure
setConfirmButtonState(isNew) is still called after recreation.

@adamk33n3r
Copy link
Copy Markdown
Owner

adamk33n3r commented Jan 25, 2026

I somehow completely missed this PR and just now noticed it when I was adding a simple thing. I'll take a look now but there are now conflicts because of my changes.

Copy link
Copy Markdown
Owner

@adamk33n3r adamk33n3r left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seem to be a few issues coderabbit pointed out, specifically the one where changing the display doesn't update what the preset buttons do so this basically makes it non-functional.

Comment thread assets/halfleft.svg
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks to be a copy of halfright instead of mirrored

Comment thread assets/fullscreen.svg
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you make these images yourself? Just want to make sure of attribution. Also, they aren't vertically centered. Is that intentional?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, these should go into res and use the bundler

Comment thread FyneApp.toml
ID = "com.adamk33n3r.goborderless"
Version = "1.3.1"
Build = 79
Version = "1.3.2"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't call this a patch version.

Comment thread go.sum
github.com/jeandeaual/go-locale v0.0.0-20250421151639-a9d6ed1b3d45 h1:vFdvrlsVU+p/KFBWTq0lTG4fvWvG88sawGlCzM+RUEU=
github.com/jeandeaual/go-locale v0.0.0-20250421151639-a9d6ed1b3d45/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this for?

Comment thread gui_app.go
}

func offsetIntValidator(s string) error {
if s == "" {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is empty string valid for offsets?

Comment thread gui_appsetting.go
if !isNew {
confirmButton.SetText("Save")
}
func isValidWindowForSelection(window Window) bool {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the point of putting this into a function?

Comment thread gui_appsetting.go
})
}

func createPresetsContent(settings *Settings, appSetting *AppSetting, isNew bool, cancelButton, confirmBtn *widget.Button) *fyne.Container {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird having cancelButton and confirmBtn

Comment thread winapi.go
var devMode DEVMODE
devMode.DmSize = uint16(unsafe.Sizeof(devMode))

ret, _, _ := procEnumDisplaySettingsW.Call(
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain to me why this is here? Why isn't the Right - Left sufficient?

Comment thread winapi.go
}

var (
procEnumDisplaySettingsW = user32.NewProc("EnumDisplaySettingsW")
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be placed up with the others

Comment thread gui_appsetting.go
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really fond of all the extra functions being created unnecessarily. The one for the size and offset entries is perhaps warranted, but the other ones are introducing so much clutter. And it's not important to your PR, it's just there to be there. I also noticed a lot of unused parameters in the functions. Like why are settings and appSetting being passed into the entry functions when they aren't being used. I'm hesitant to point a finger at AI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants