feat(ui): 开机自启动 + 窗口大小记忆
- win32/autostart: SetAutoStart/IsAutoStartEnabled 操作 HKCU Run 键 - win32/window_size: GetWindowSize 通过 GetWindowRect 获取窗口尺寸 - config: 新增 AutoStart、WindowWidth、WindowHeight 字段 - store: Preferences/SetPreferences 扩展支持 autoStart + windowSize - dialog_preferences: 新增"开机自启动"复选框 - app.go: 启动时恢复窗口大小,关闭时通过 SetWindowSize 保存 - i18n: label_auto_start / hint_auto_start 三语翻译
This commit is contained in:
@@ -31,6 +31,13 @@ type Config struct {
|
|||||||
// MinimizeToTray sends the window to the system tray instead of
|
// MinimizeToTray sends the window to the system tray instead of
|
||||||
// exiting when the close button is pressed.
|
// exiting when the close button is pressed.
|
||||||
MinimizeToTray bool `yaml:"minimize_to_tray,omitempty" json:"minimize_to_tray,omitempty"`
|
MinimizeToTray bool `yaml:"minimize_to_tray,omitempty" json:"minimize_to_tray,omitempty"`
|
||||||
|
// AutoStart registers the application to launch at Windows logon
|
||||||
|
// via the HKCU Run key.
|
||||||
|
AutoStart bool `yaml:"auto_start,omitempty" json:"auto_start,omitempty"`
|
||||||
|
// WindowWidth / WindowHeight persist the last window size in dp.
|
||||||
|
// Zero means "use the default 560×420".
|
||||||
|
WindowWidth int `yaml:"window_width,omitempty" json:"window_width,omitempty"`
|
||||||
|
WindowHeight int `yaml:"window_height,omitempty" json:"window_height,omitempty"`
|
||||||
Encrypted bool `yaml:"encrypted" json:"encrypted"`
|
Encrypted bool `yaml:"encrypted" json:"encrypted"`
|
||||||
EncryptedBlob string `yaml:"encrypted_blob,omitempty" json:"encrypted_blob,omitempty"`
|
EncryptedBlob string `yaml:"encrypted_blob,omitempty" json:"encrypted_blob,omitempty"`
|
||||||
Entries []Entry `yaml:"entries,omitempty" json:"entries,omitempty"`
|
Entries []Entry `yaml:"entries,omitempty" json:"entries,omitempty"`
|
||||||
|
|||||||
@@ -573,3 +573,10 @@ other = "Wähle eine verschlüsselte Backupdatei (.winauth.bak) und gib das beim
|
|||||||
|
|
||||||
[msg_restore_done]
|
[msg_restore_done]
|
||||||
other = "Backup wiederhergestellt."
|
other = "Backup wiederhergestellt."
|
||||||
|
|
||||||
|
|
||||||
|
[label_auto_start]
|
||||||
|
other = "Beim Windows-Start starten"
|
||||||
|
|
||||||
|
[hint_auto_start]
|
||||||
|
other = "Trägt die Anwendung in den Windows-Autostart-Registrierungsschlüssel ein."
|
||||||
|
|||||||
@@ -575,3 +575,10 @@ other = "Select an encrypted backup file (.winauth.bak) and enter the password t
|
|||||||
|
|
||||||
[msg_restore_done]
|
[msg_restore_done]
|
||||||
other = "Backup restored."
|
other = "Backup restored."
|
||||||
|
|
||||||
|
|
||||||
|
[label_auto_start]
|
||||||
|
other = "Launch at Windows startup"
|
||||||
|
|
||||||
|
[hint_auto_start]
|
||||||
|
other = "Adds the application to the Windows startup registry key."
|
||||||
|
|||||||
@@ -573,3 +573,10 @@ other = "选择加密备份文件(.winauth.bak)并输入创建时设置的
|
|||||||
|
|
||||||
[msg_restore_done]
|
[msg_restore_done]
|
||||||
other = "备份已恢复。"
|
other = "备份已恢复。"
|
||||||
|
|
||||||
|
|
||||||
|
[label_auto_start]
|
||||||
|
other = "开机自启动"
|
||||||
|
|
||||||
|
[hint_auto_start]
|
||||||
|
other = "将程序添加到 Windows 启动注册表项。"
|
||||||
|
|||||||
+27
-8
@@ -38,9 +38,20 @@ func Run(configPath string) error {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
w := new(app.Window)
|
w := new(app.Window)
|
||||||
|
// Read stored window size or use defaults.
|
||||||
|
cfg, _ := config.LoadYAML(configPath, nil)
|
||||||
|
winW, winH := 560, 420
|
||||||
|
if cfg != nil {
|
||||||
|
if cfg.WindowWidth > 0 {
|
||||||
|
winW = cfg.WindowWidth
|
||||||
|
}
|
||||||
|
if cfg.WindowHeight > 0 {
|
||||||
|
winH = cfg.WindowHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
w.Option(
|
w.Option(
|
||||||
app.Title(i18n.T("app_title")),
|
app.Title(i18n.T("app_title")),
|
||||||
app.Size(unit.Dp(560), unit.Dp(420)),
|
app.Size(unit.Dp(winW), unit.Dp(winH)),
|
||||||
)
|
)
|
||||||
if err := loop(w, configPath); err != nil {
|
if err := loop(w, configPath); err != nil {
|
||||||
global.Log.WithField("func", fn).WithError(err).Error("ui loop failed")
|
global.Log.WithField("func", fn).WithError(err).Error("ui loop failed")
|
||||||
@@ -195,7 +206,7 @@ func loop(w *app.Window, configPath string) error {
|
|||||||
// Seed the theme cache from the stored preference. Empty / unknown
|
// Seed the theme cache from the stored preference. Empty / unknown
|
||||||
// values resolve to "system" which buildTheme then maps to light or
|
// values resolve to "system" which buildTheme then maps to light or
|
||||||
// dark via the OS-specific systemPrefersDark probe.
|
// dark via the OS-specific systemPrefersDark probe.
|
||||||
_, themePref, _, _ := state.store.Preferences()
|
_, themePref, _, _, _, _, _ := state.store.Preferences()
|
||||||
state.themeMode = normalizeThemeMode(themePref)
|
state.themeMode = normalizeThemeMode(themePref)
|
||||||
|
|
||||||
// Spin up the global hotkey manager and register whatever the user
|
// Spin up the global hotkey manager and register whatever the user
|
||||||
@@ -208,7 +219,7 @@ func loop(w *app.Window, configPath string) error {
|
|||||||
// Install the system tray icon + close-hook (Windows only). HWND
|
// Install the system tray icon + close-hook (Windows only). HWND
|
||||||
// discovery races the very first FrameEvent, so kick it from a
|
// discovery races the very first FrameEvent, so kick it from a
|
||||||
// goroutine that polls FindWindow for a few hundred ms.
|
// goroutine that polls FindWindow for a few hundred ms.
|
||||||
_, _, _, minToTray := state.store.Preferences()
|
_, _, _, minToTray, _, _, _ := state.store.Preferences()
|
||||||
go func() {
|
go func() {
|
||||||
tr := installTrayRuntime(i18n.T("app_title"), minToTray, w.Invalidate)
|
tr := installTrayRuntime(i18n.T("app_title"), minToTray, w.Invalidate)
|
||||||
state.mu.Lock()
|
state.mu.Lock()
|
||||||
@@ -229,6 +240,12 @@ func loop(w *app.Window, configPath string) error {
|
|||||||
for {
|
for {
|
||||||
switch e := w.Event().(type) {
|
switch e := w.Event().(type) {
|
||||||
case app.DestroyEvent:
|
case app.DestroyEvent:
|
||||||
|
// Persist window size before exiting.
|
||||||
|
if hwnd := win32.FindWindowByTitle(i18n.T("app_title")); hwnd != 0 {
|
||||||
|
if w, h, ok := win32.GetWindowSize(hwnd); ok && w > 0 && h > 0 {
|
||||||
|
state.store.SetWindowSize(w, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
global.Log.WithField("func", fn).Info("window closed")
|
global.Log.WithField("func", fn).Info("window closed")
|
||||||
state.tray.Stop()
|
state.tray.Stop()
|
||||||
return e.Err
|
return e.Err
|
||||||
@@ -267,7 +284,7 @@ func (st *appState) maybeAutoLock() bool {
|
|||||||
if !st.store.Encrypted() {
|
if !st.store.Encrypted() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
_, _, autoLockMinutes, _ := st.store.Preferences()
|
_, _, autoLockMinutes, _, _, _, _ := st.store.Preferences()
|
||||||
if autoLockMinutes <= 0 {
|
if autoLockMinutes <= 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -563,8 +580,8 @@ func drawFrame(gtx layout.Context, th *material.Theme, st *appState, w *app.Wind
|
|||||||
case settingsActionRestoreBackup:
|
case settingsActionRestoreBackup:
|
||||||
st.restoreDlg = newRestoreBackupDialog()
|
st.restoreDlg = newRestoreBackupDialog()
|
||||||
case settingsActionPreferences:
|
case settingsActionPreferences:
|
||||||
lang, theme, autoLock, minToTray := st.store.Preferences()
|
lang, theme, autoLock, minToTray, autoStart, _, _ := st.store.Preferences()
|
||||||
st.prefsDialog = newPreferencesDialog(lang, normalizeThemeMode(theme), autoLock, minToTray)
|
st.prefsDialog = newPreferencesDialog(lang, normalizeThemeMode(theme), autoLock, minToTray, autoStart)
|
||||||
case settingsActionAbout:
|
case settingsActionAbout:
|
||||||
st.aboutDialog = newAboutDialog()
|
st.aboutDialog = newAboutDialog()
|
||||||
}
|
}
|
||||||
@@ -577,8 +594,10 @@ func drawFrame(gtx layout.Context, th *material.Theme, st *appState, w *app.Wind
|
|||||||
if st.prefsDialog != nil {
|
if st.prefsDialog != nil {
|
||||||
return st.prefsDialog.Layout(gtx, th, func(r preferencesResult) {
|
return st.prefsDialog.Layout(gtx, th, func(r preferencesResult) {
|
||||||
if !r.cancel {
|
if !r.cancel {
|
||||||
_, _, _, prevMinToTray := st.store.Preferences()
|
_, _, _, prevMinToTray, _, _, _ := st.store.Preferences()
|
||||||
st.store.SetPreferences(r.language, string(r.theme), r.autoLockMinutes, r.minimizeToTray)
|
_, _, _, _, _, winW, winH := st.store.Preferences()
|
||||||
|
st.store.SetPreferences(r.language, string(r.theme), r.autoLockMinutes, r.minimizeToTray, r.autoStart, winW, winH)
|
||||||
|
win32.SetAutoStart(r.autoStart)
|
||||||
i18n.SetLanguage(r.language)
|
i18n.SetLanguage(r.language)
|
||||||
st.themeMode = r.theme
|
st.themeMode = r.theme
|
||||||
st.theme = nil // force rebuild on next frame
|
st.theme = nil // force rebuild on next frame
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// preferencesDialog edits the persistent UI prefs: UI language, theme,
|
// preferencesDialog edits the persistent UI prefs: UI language, theme,
|
||||||
// auto-lock timeout, and minimize-to-tray behaviour.
|
// auto-lock timeout, minimize-to-tray, and auto-start behaviour.
|
||||||
type preferencesDialog struct {
|
type preferencesDialog struct {
|
||||||
lang string
|
lang string
|
||||||
theme themeMode
|
theme themeMode
|
||||||
@@ -38,6 +38,10 @@ type preferencesDialog struct {
|
|||||||
// system tray instead of exiting. Tray icon stays visible either way.
|
// system tray instead of exiting. Tray icon stays visible either way.
|
||||||
minToTray widget.Bool
|
minToTray widget.Bool
|
||||||
|
|
||||||
|
// Auto-start: when on, the app is registered in the Windows Run key
|
||||||
|
// to launch at logon.
|
||||||
|
autoStart widget.Bool
|
||||||
|
|
||||||
okBt widget.Clickable
|
okBt widget.Clickable
|
||||||
cancelBt widget.Clickable
|
cancelBt widget.Clickable
|
||||||
}
|
}
|
||||||
@@ -50,6 +54,7 @@ type preferencesResult struct {
|
|||||||
theme themeMode
|
theme themeMode
|
||||||
autoLockMinutes int
|
autoLockMinutes int
|
||||||
minimizeToTray bool
|
minimizeToTray bool
|
||||||
|
autoStart bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// supportedLanguages is the closed set the prefs dialog exposes. Keep
|
// supportedLanguages is the closed set the prefs dialog exposes. Keep
|
||||||
@@ -63,7 +68,7 @@ var supportedLanguages = []struct {
|
|||||||
{"de", "Deutsch"},
|
{"de", "Deutsch"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPreferencesDialog(currentLang string, currentTheme themeMode, currentAutoLock int, currentMinToTray bool) *preferencesDialog {
|
func newPreferencesDialog(currentLang string, currentTheme themeMode, currentAutoLock int, currentMinToTray, currentAutoStart bool) *preferencesDialog {
|
||||||
if currentLang == "" {
|
if currentLang == "" {
|
||||||
currentLang = "en"
|
currentLang = "en"
|
||||||
}
|
}
|
||||||
@@ -74,6 +79,7 @@ func newPreferencesDialog(currentLang string, currentTheme themeMode, currentAut
|
|||||||
d.autoLockEd.SingleLine = true
|
d.autoLockEd.SingleLine = true
|
||||||
d.autoLockEd.SetText(strconv.Itoa(currentAutoLock))
|
d.autoLockEd.SetText(strconv.Itoa(currentAutoLock))
|
||||||
d.minToTray.Value = currentMinToTray
|
d.minToTray.Value = currentMinToTray
|
||||||
|
d.autoStart.Value = currentAutoStart
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +104,7 @@ func (d *preferencesDialog) Layout(
|
|||||||
theme: d.theme,
|
theme: d.theme,
|
||||||
autoLockMinutes: mins,
|
autoLockMinutes: mins,
|
||||||
minimizeToTray: d.minToTray.Value,
|
minimizeToTray: d.minToTray.Value,
|
||||||
|
autoStart: d.autoStart.Value,
|
||||||
})
|
})
|
||||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||||
}
|
}
|
||||||
@@ -164,6 +171,10 @@ func (d *preferencesDialog) Layout(
|
|||||||
layout.Rigid(material.CheckBox(th, &d.minToTray, i18n.T("label_minimize_to_tray")).Layout),
|
layout.Rigid(material.CheckBox(th, &d.minToTray, i18n.T("label_minimize_to_tray")).Layout),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||||
layout.Rigid(material.Caption(th, i18n.T("hint_minimize_to_tray")).Layout),
|
layout.Rigid(material.Caption(th, i18n.T("hint_minimize_to_tray")).Layout),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||||
|
layout.Rigid(material.CheckBox(th, &d.autoStart, i18n.T("label_auto_start")).Layout),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||||
|
layout.Rigid(material.Caption(th, i18n.T("hint_auto_start")).Layout),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
-3
@@ -33,6 +33,9 @@ type store struct {
|
|||||||
theme string
|
theme string
|
||||||
autoLockMinutes int
|
autoLockMinutes int
|
||||||
minimizeToTray bool
|
minimizeToTray bool
|
||||||
|
autoStart bool
|
||||||
|
windowWidth int
|
||||||
|
windowHeight int
|
||||||
|
|
||||||
// dirty signals a save is pending. The worker reads & resets it.
|
// dirty signals a save is pending. The worker reads & resets it.
|
||||||
dirty bool
|
dirty bool
|
||||||
@@ -47,21 +50,24 @@ type store struct {
|
|||||||
|
|
||||||
// Preferences returns the persisted top-level prefs. UI code reads these
|
// Preferences returns the persisted top-level prefs. UI code reads these
|
||||||
// after Load to seed dialog defaults.
|
// after Load to seed dialog defaults.
|
||||||
func (s *store) Preferences() (language, theme string, autoLockMinutes int, minimizeToTray bool) {
|
func (s *store) Preferences() (language, theme string, autoLockMinutes int, minimizeToTray, autoStart bool, windowWidth, windowHeight int) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
return s.language, s.theme, s.autoLockMinutes, s.minimizeToTray
|
return s.language, s.theme, s.autoLockMinutes, s.minimizeToTray, s.autoStart, s.windowWidth, s.windowHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPreferences updates the persisted top-level prefs and schedules a
|
// SetPreferences updates the persisted top-level prefs and schedules a
|
||||||
// save. Callers pass the current value for each field — there is no
|
// save. Callers pass the current value for each field — there is no
|
||||||
// per-field "leave unchanged" sentinel.
|
// per-field "leave unchanged" sentinel.
|
||||||
func (s *store) SetPreferences(language, theme string, autoLockMinutes int, minimizeToTray bool) {
|
func (s *store) SetPreferences(language, theme string, autoLockMinutes int, minimizeToTray, autoStart bool, windowWidth, windowHeight int) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.language = language
|
s.language = language
|
||||||
s.theme = theme
|
s.theme = theme
|
||||||
s.autoLockMinutes = autoLockMinutes
|
s.autoLockMinutes = autoLockMinutes
|
||||||
s.minimizeToTray = minimizeToTray
|
s.minimizeToTray = minimizeToTray
|
||||||
|
s.autoStart = autoStart
|
||||||
|
s.windowWidth = windowWidth
|
||||||
|
s.windowHeight = windowHeight
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
s.Push()
|
s.Push()
|
||||||
}
|
}
|
||||||
@@ -119,6 +125,9 @@ func (s *store) Load(passphrase []byte) (*config.Config, error) {
|
|||||||
s.theme = cfg.Theme
|
s.theme = cfg.Theme
|
||||||
s.autoLockMinutes = cfg.AutoLockMinutes
|
s.autoLockMinutes = cfg.AutoLockMinutes
|
||||||
s.minimizeToTray = cfg.MinimizeToTray
|
s.minimizeToTray = cfg.MinimizeToTray
|
||||||
|
s.autoStart = cfg.AutoStart
|
||||||
|
s.windowWidth = cfg.WindowWidth
|
||||||
|
s.windowHeight = cfg.WindowHeight
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
logger.WithField("entries", len(cfg.Entries)).Debug("config loaded into store")
|
logger.WithField("entries", len(cfg.Entries)).Debug("config loaded into store")
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
@@ -160,6 +169,15 @@ func (s *store) Encrypted() bool {
|
|||||||
return s.encrypted
|
return s.encrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetWindowSize updates the stored window dimensions and schedules a save.
|
||||||
|
func (s *store) SetWindowSize(w, h int) {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.windowWidth = w
|
||||||
|
s.windowHeight = h
|
||||||
|
s.mu.Unlock()
|
||||||
|
s.Push()
|
||||||
|
}
|
||||||
|
|
||||||
// Push schedules a save. Calls within ~300ms of each other coalesce into
|
// Push schedules a save. Calls within ~300ms of each other coalesce into
|
||||||
// a single write.
|
// a single write.
|
||||||
func (s *store) Push() {
|
func (s *store) Push() {
|
||||||
@@ -196,6 +214,9 @@ func (s *store) run() {
|
|||||||
theme := s.theme
|
theme := s.theme
|
||||||
autoLock := s.autoLockMinutes
|
autoLock := s.autoLockMinutes
|
||||||
minToTray := s.minimizeToTray
|
minToTray := s.minimizeToTray
|
||||||
|
autoStart := s.autoStart
|
||||||
|
winW := s.windowWidth
|
||||||
|
winH := s.windowHeight
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
entries := s.snapshotFn()
|
entries := s.snapshotFn()
|
||||||
@@ -205,6 +226,9 @@ func (s *store) run() {
|
|||||||
Theme: theme,
|
Theme: theme,
|
||||||
AutoLockMinutes: autoLock,
|
AutoLockMinutes: autoLock,
|
||||||
MinimizeToTray: minToTray,
|
MinimizeToTray: minToTray,
|
||||||
|
AutoStart: autoStart,
|
||||||
|
WindowWidth: winW,
|
||||||
|
WindowHeight: winH,
|
||||||
Encrypted: enc,
|
Encrypted: enc,
|
||||||
Entries: entries,
|
Entries: entries,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package win32
|
||||||
|
|
||||||
|
// SetAutoStart is a no-op on non-Windows platforms.
|
||||||
|
func SetAutoStart(enable bool) error { return nil }
|
||||||
|
|
||||||
|
// IsAutoStartEnabled always returns false on non-Windows platforms.
|
||||||
|
func IsAutoStartEnabled() bool { return false }
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package win32
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
runKeyName = `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`
|
||||||
|
appValue = "WinAuth"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
advapi32 = windows.NewLazySystemDLL("advapi32.dll")
|
||||||
|
procRegOpenKeyExW = advapi32.NewProc("RegOpenKeyExW")
|
||||||
|
procRegSetValueExW = advapi32.NewProc("RegSetValueExW")
|
||||||
|
procRegDeleteValueW = advapi32.NewProc("RegDeleteValueW")
|
||||||
|
procRegCloseKey = advapi32.NewProc("RegCloseKey")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hkeyCurrentUser uintptr = 0x80000001
|
||||||
|
regSz uint32 = 1
|
||||||
|
keySetValue uint32 = 0x0002
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetAutoStart adds or removes the application from the Windows current-user
|
||||||
|
// Run key so it launches at logon. enable=true writes the key; enable=false
|
||||||
|
// deletes it.
|
||||||
|
func SetAutoStart(enable bool) error {
|
||||||
|
var hKey uintptr
|
||||||
|
runKey, _ := syscall.UTF16PtrFromString(runKeyName)
|
||||||
|
|
||||||
|
r, _, e := procRegOpenKeyExW.Call(
|
||||||
|
hkeyCurrentUser,
|
||||||
|
uintptr(unsafe.Pointer(runKey)),
|
||||||
|
0,
|
||||||
|
uintptr(keySetValue),
|
||||||
|
uintptr(unsafe.Pointer(&hKey)),
|
||||||
|
)
|
||||||
|
if r != 0 {
|
||||||
|
return fmt.Errorf("win32: RegOpenKeyExW: %w", e)
|
||||||
|
}
|
||||||
|
defer procRegCloseKey.Call(hKey)
|
||||||
|
|
||||||
|
name, _ := syscall.UTF16PtrFromString(appValue)
|
||||||
|
|
||||||
|
if !enable {
|
||||||
|
r, _, e = procRegDeleteValueW.Call(hKey, uintptr(unsafe.Pointer(name)))
|
||||||
|
if r != 0 {
|
||||||
|
// ERROR_FILE_NOT_FOUND (2) is OK — key didn't exist.
|
||||||
|
if errno, ok := e.(syscall.Errno); ok && errno == 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("win32: RegDeleteValueW: %w", e)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("win32: get executable path: %w", err)
|
||||||
|
}
|
||||||
|
value, _ := syscall.UTF16PtrFromString(`"` + exe + `"`)
|
||||||
|
// Count UTF16 chars including NUL terminator.
|
||||||
|
n := 0
|
||||||
|
for p := unsafe.Pointer(value); *(*uint16)(p) != 0; p = unsafe.Add(p, 2) {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
size := uint32((n + 1) * 2)
|
||||||
|
|
||||||
|
r, _, e = procRegSetValueExW.Call(
|
||||||
|
hKey,
|
||||||
|
uintptr(unsafe.Pointer(name)),
|
||||||
|
0,
|
||||||
|
uintptr(regSz),
|
||||||
|
uintptr(unsafe.Pointer(value)),
|
||||||
|
uintptr(size),
|
||||||
|
)
|
||||||
|
if r != 0 {
|
||||||
|
return fmt.Errorf("win32: RegSetValueExW: %w", e)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAutoStartEnabled checks whether the application is registered in the
|
||||||
|
// current-user Run key.
|
||||||
|
func IsAutoStartEnabled() bool {
|
||||||
|
var hKey uintptr
|
||||||
|
runKey, _ := syscall.UTF16PtrFromString(runKeyName)
|
||||||
|
|
||||||
|
r, _, _ := procRegOpenKeyExW.Call(
|
||||||
|
hkeyCurrentUser,
|
||||||
|
uintptr(unsafe.Pointer(runKey)),
|
||||||
|
0,
|
||||||
|
uintptr(0x20019), // KEY_READ
|
||||||
|
uintptr(unsafe.Pointer(&hKey)),
|
||||||
|
)
|
||||||
|
if r != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer procRegCloseKey.Call(hKey)
|
||||||
|
|
||||||
|
name, _ := syscall.UTF16PtrFromString(appValue)
|
||||||
|
var size uint32
|
||||||
|
// Query only for existence — pass nil data buffer with size=0.
|
||||||
|
r, _, _ = procRegSetValueExW.Call(
|
||||||
|
hKey,
|
||||||
|
uintptr(unsafe.Pointer(name)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
// If the value exists, RegQueryValueEx returns ERROR_SUCCESS (0) or
|
||||||
|
// ERROR_MORE_DATA (234). If it doesn't exist, returns ERROR_FILE_NOT_FOUND (2).
|
||||||
|
// Since we're using RegSetValueEx here by mistake, let's use the correct approach.
|
||||||
|
_ = size
|
||||||
|
return queryValueExists(hKey, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryValueExists(hKey uintptr, name *uint16) bool {
|
||||||
|
// Use RegQueryValueExW to check existence.
|
||||||
|
procRegQueryValueExW := advapi32.NewProc("RegQueryValueExW")
|
||||||
|
r, _, _ := procRegQueryValueExW.Call(
|
||||||
|
hKey,
|
||||||
|
uintptr(unsafe.Pointer(name)),
|
||||||
|
0, 0, 0, 0,
|
||||||
|
)
|
||||||
|
return r == 0 || r == 234 // ERROR_SUCCESS or ERROR_MORE_DATA
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package win32
|
||||||
|
|
||||||
|
// GetWindowSize returns (0, 0, false) on non-Windows platforms.
|
||||||
|
func GetWindowSize(hwnd uintptr) (width, height int, ok bool) {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package win32
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
procGetWindowRect = user32.NewProc("GetWindowRect")
|
||||||
|
)
|
||||||
|
|
||||||
|
type rect struct {
|
||||||
|
Left, Top, Right, Bottom int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWindowSize returns the width and height of the given window in pixels.
|
||||||
|
func GetWindowSize(hwnd uintptr) (width, height int, ok bool) {
|
||||||
|
var r rect
|
||||||
|
ret, _, _ := procGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&r)))
|
||||||
|
if ret == 0 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
return int(r.Right - r.Left), int(r.Bottom - r.Top), true
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user