Skip to content

Commit 95b63b8

Browse files
committed
win32: menu removed and position stored
1 parent 0575157 commit 95b63b8

13 files changed

Lines changed: 317 additions & 138 deletions

File tree

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ A compact weather widget for your desktop.
77
### Windows
88
```powershell
99
$env:PATH = "C:\msys64\ucrt64\bin;" + $env:PATH; $env:CGO_ENABLED = "1"; gcc --version | Select-Object -First 1
10-
make build
1110
```
1211

1312
```powershell
@@ -55,11 +54,25 @@ A compact weather widget for your desktop.
5554
"timezone": "Europe/Warsaw"
5655
}
5756
],
58-
"refreshInterval": 23,
59-
"cornerPosition": "bottom-right",
57+
"refreshInterval": 30,
58+
"cornerPosition": "top-right",
6059
"apiConfig": {
6160
"provider": "openweathermap",
6261
"apiKey": "YOUR_API_KEY"
6362
}
6463
}
64+
```
65+
66+
### Windows COnfig location
67+
68+
```powershell
69+
Get-Content "$env:APPDATA\WeatherWidget\WeatherWidget\config.json"
70+
or
71+
type "$env:APPDATA\WeatherWidget\WeatherWidget\config.json"
72+
```
73+
74+
```bash
75+
cat $HOME/.config/WeatherWidget/WeatherWidget/config.json
76+
or
77+
more $HOME/.config/WeatherWidget/WeatherWidget/config.json
6578
```

internal/app/manager.go

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,7 @@ func (a *AppManager) Run() error {
6464
// 3. Create UI manager.
6565
a.ui = ui.NewUIManager(a.app)
6666

67-
// 4. Setup context menu and system tray.
68-
a.ui.SetupContextMenu(
69-
func() { a.openSettings() },
70-
func(pos string) { a.saveCornerPosition(pos) },
71-
func() { a.Shutdown() },
72-
)
67+
// 4. Setup system tray.
7368
a.ui.SetupSystemTray(
7469
func() { a.openSettings() },
7570
func() { a.Shutdown() },
@@ -87,9 +82,21 @@ func (a *AppManager) Run() error {
8782
// 7. Show widget and apply Win32 styles.
8883
a.ui.ShowWidget(cfg.Cities)
8984
a.ui.ApplyWin32Styles()
90-
a.ui.SetCorner(cfg.CornerPosition)
85+
a.applyPosition(cfg)
9186
log.Printf("WeatherWidget window shown at %s", cfg.CornerPosition)
9287

88+
// 8. Enable drag-to-reposition — persists custom coordinates on drag end.
89+
a.ui.EnableDrag(func() {
90+
x, y := a.ui.GetPosition()
91+
a.cfg.CustomX = &x
92+
a.cfg.CustomY = &y
93+
if err := a.config.Save(a.cfg); err != nil {
94+
log.Printf("failed to save custom position (%d, %d): %v", x, y, err)
95+
} else {
96+
log.Printf("custom position saved: (%d, %d)", x, y)
97+
}
98+
})
99+
93100
// 8. Start clocks for each panel.
94101
a.startPanelClocks(cfg.Cities)
95102

@@ -140,15 +147,14 @@ func (a *AppManager) openSettings() {
140147
a.ui.ShowSettings(a.cfg, a.onSettingsSave)
141148
}
142149

143-
// saveCornerPosition updates the in-memory config with the new corner position
144-
// and persists it to disk. Called when the user picks a position from the
145-
// right-click context menu.
146-
func (a *AppManager) saveCornerPosition(pos string) {
147-
a.cfg.CornerPosition = pos
148-
if err := a.config.Save(a.cfg); err != nil {
149-
log.Printf("failed to save corner position %q: %v", pos, err)
150+
// applyPosition moves the widget to custom coordinates if set, otherwise
151+
// falls back to the configured corner position.
152+
func (a *AppManager) applyPosition(cfg *config.Config) {
153+
if cfg.CustomX != nil && cfg.CustomY != nil {
154+
a.ui.SetPosition(*cfg.CustomX, *cfg.CustomY)
155+
log.Printf("positioned widget at custom coordinates (%d, %d)", *cfg.CustomX, *cfg.CustomY)
150156
} else {
151-
log.Printf("corner position saved: %s", pos)
157+
a.ui.SetCorner(cfg.CornerPosition)
152158
}
153159
}
154160

@@ -203,7 +209,7 @@ func (a *AppManager) onSettingsSave(newCfg *config.Config) error {
203209
}
204210

205211
// Always reposition the widget window (position may have changed independently).
206-
a.ui.SetCorner(newCfg.CornerPosition)
212+
a.applyPosition(newCfg)
207213

208214
return nil
209215
}

internal/config/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type Config struct {
1414
Cities []CityConfig `json:"cities"`
1515
RefreshInterval int `json:"refreshInterval"` // minutes, 1–60, default 10
1616
CornerPosition string `json:"cornerPosition"` // "top-left"|"top-right"|"bottom-left"|"bottom-right"
17+
CustomX *int `json:"customX,omitempty"`
18+
CustomY *int `json:"customY,omitempty"`
1719
APIConfig *APIConfig `json:"apiConfig,omitempty"`
1820
DatabaseConfig *DatabaseConfig `json:"databaseConfig,omitempty"`
1921
}

internal/ui/contextmenu.go

Lines changed: 0 additions & 64 deletions
This file was deleted.

internal/ui/drag_linux.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//go:build linux
2+
3+
package ui
4+
5+
import (
6+
"log"
7+
"sync"
8+
"time"
9+
)
10+
11+
var (
12+
linuxDragMu sync.Mutex
13+
linuxDragStop chan struct{}
14+
linuxDragCallback func()
15+
linuxLastX int
16+
linuxLastY int
17+
linuxMovedByUs bool // true briefly after we call moveWindow
18+
)
19+
20+
// enableWindowDrag enables drag-to-reposition detection on Linux.
21+
//
22+
// Since the title bar is removed, most window managers still allow moving
23+
// via Alt+left-click drag or Super+drag. This function starts a background
24+
// poller that detects when the window position changes and calls onDragEnd
25+
// so the caller can persist the new coordinates.
26+
//
27+
// Requires xdotool to be installed.
28+
func enableWindowDrag(onDragEnd func()) {
29+
linuxDragMu.Lock()
30+
defer linuxDragMu.Unlock()
31+
32+
// Stop any existing poller.
33+
if linuxDragStop != nil {
34+
close(linuxDragStop)
35+
}
36+
37+
linuxDragCallback = onDragEnd
38+
linuxDragStop = make(chan struct{})
39+
linuxLastX, linuxLastY = getWindowPosition()
40+
41+
go pollWindowPosition(linuxDragStop)
42+
log.Println("Linux drag: position poller started")
43+
}
44+
45+
// notifyLinuxMoveByUs should be called before programmatic moves so the
46+
// poller doesn't treat them as user drags.
47+
func notifyLinuxMoveByUs() {
48+
linuxDragMu.Lock()
49+
linuxMovedByUs = true
50+
linuxDragMu.Unlock()
51+
}
52+
53+
// pollWindowPosition checks the window position every 500ms and fires the
54+
// callback when it detects a change not initiated by the application.
55+
func pollWindowPosition(stop chan struct{}) {
56+
ticker := time.NewTicker(500 * time.Millisecond)
57+
defer ticker.Stop()
58+
59+
for {
60+
select {
61+
case <-stop:
62+
return
63+
case <-ticker.C:
64+
x, y := getWindowPosition()
65+
66+
linuxDragMu.Lock()
67+
movedByUs := linuxMovedByUs
68+
linuxMovedByUs = false
69+
lastX, lastY := linuxLastX, linuxLastY
70+
cb := linuxDragCallback
71+
linuxDragMu.Unlock()
72+
73+
if movedByUs {
74+
// Update last known position but don't fire callback.
75+
linuxDragMu.Lock()
76+
linuxLastX = x
77+
linuxLastY = y
78+
linuxDragMu.Unlock()
79+
continue
80+
}
81+
82+
if x != lastX || y != lastY {
83+
linuxDragMu.Lock()
84+
linuxLastX = x
85+
linuxLastY = y
86+
linuxDragMu.Unlock()
87+
88+
if cb != nil {
89+
log.Printf("Linux drag: position changed from (%d,%d) to (%d,%d), saving", lastX, lastY, x, y)
90+
cb()
91+
}
92+
}
93+
}
94+
}
95+
}

internal/ui/drag_other.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//go:build !windows && !linux
2+
3+
package ui
4+
5+
// enableWindowDrag is a no-op on unsupported platforms.
6+
func enableWindowDrag(_ func()) {}

internal/ui/drag_windows.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//go:build windows
2+
3+
package ui
4+
5+
import (
6+
"log"
7+
8+
"golang.org/x/sys/windows"
9+
)
10+
11+
var (
12+
procSetWindowLongPtrW = user32.NewProc("SetWindowLongPtrW")
13+
procGetWindowLongPtrW = user32.NewProc("GetWindowLongPtrW")
14+
procCallWindowProcW = user32.NewProc("CallWindowProcW")
15+
procSendMessageW = user32.NewProc("SendMessageW")
16+
procReleaseCapture = user32.NewProc("ReleaseCapture")
17+
)
18+
19+
const (
20+
gwlpWndProc uintptr = ^uintptr(3) // -4, GWLP_WNDPROC
21+
wmLButtonDown = 0x0201 // WM_LBUTTONDOWN
22+
wmNCLButtonDown = 0x00A1 // WM_NCLBUTTONDOWN
23+
wmExitSizeMove = 0x0232 // WM_EXITSIZEMOVE
24+
htCaption = 2 // HTCAPTION
25+
)
26+
27+
// dragState holds the subclass state for a single window.
28+
var dragState struct {
29+
origProc uintptr
30+
onDragEnd func()
31+
}
32+
33+
// dragWndProc is the replacement window procedure that intercepts mouse
34+
// and move events to implement drag-to-reposition.
35+
func dragWndProc(hwnd, msg, wParam, lParam uintptr) uintptr {
36+
switch msg {
37+
case wmLButtonDown:
38+
// Initiate a native title-bar drag so Windows handles the move.
39+
procReleaseCapture.Call()
40+
procSendMessageW.Call(hwnd, wmNCLButtonDown, htCaption, 0)
41+
return 0
42+
43+
case wmExitSizeMove:
44+
// The user released the mouse after dragging. Save position.
45+
if dragState.onDragEnd != nil {
46+
dragState.onDragEnd()
47+
}
48+
}
49+
50+
// Forward everything else to the original window procedure.
51+
ret, _, _ := procCallWindowProcW.Call(dragState.origProc, hwnd, msg, wParam, lParam)
52+
return ret
53+
}
54+
55+
// enableWindowDrag subclasses the widget window so that left-click anywhere
56+
// initiates a native window drag, and calls onDragEnd when the drag finishes.
57+
func enableWindowDrag(onDragEnd func()) {
58+
hwnd := findHWND(widgetTitle)
59+
if hwnd == 0 {
60+
log.Println("enableWindowDrag: could not find HWND")
61+
return
62+
}
63+
64+
dragState.onDragEnd = onDragEnd
65+
66+
// Only subclass once — if origProc is already set, just update the callback.
67+
if dragState.origProc != 0 {
68+
return
69+
}
70+
71+
cb := windows.NewCallback(dragWndProc)
72+
origProc, _, _ := procSetWindowLongPtrW.Call(hwnd, uintptr(gwlpWndProc), cb)
73+
if origProc == 0 {
74+
// On 32-bit Windows, SetWindowLongPtrW may not exist; try GetWindowLongW path.
75+
origProc, _, _ = procGetWindowLongPtrW.Call(hwnd, uintptr(gwlpWndProc))
76+
log.Println("enableWindowDrag: SetWindowLongPtrW returned 0, fallback origProc:", origProc)
77+
}
78+
dragState.origProc = origProc
79+
log.Println("enableWindowDrag: window subclassed for drag support")
80+
}

0 commit comments

Comments
 (0)