feat(app): implement basic function
General - 配置界面支持配置对象存储桶 - 快捷键进入截图 - 一键上传截图 & 复制截图链接 MacOS - 状态栏指示器和应用图标
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
// Package hotkey wraps golang.design/x/hotkey to provide a process-wide
|
||||
// global shortcut. The MVP only supports a single hard-coded combination
|
||||
// (Cmd+Shift+A on macOS) but the abstraction below allows the binding to
|
||||
// be replaced at runtime once we add per-platform parsing of user input.
|
||||
package hotkey
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
hk "golang.design/x/hotkey"
|
||||
)
|
||||
|
||||
// Manager owns the lifecycle of one global hotkey.
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
current *hk.Hotkey
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
// NewManager returns a fresh manager with no hotkey registered yet.
|
||||
func NewManager() *Manager { return &Manager{} }
|
||||
|
||||
// Register parses the spec (e.g. "cmd+shift+a") and binds the callback.
|
||||
// If a previous hotkey is registered, it is unregistered first so callers
|
||||
// can swap shortcuts at runtime when the user changes the setting.
|
||||
func (m *Manager) Register(spec string, onTrigger func()) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.current != nil {
|
||||
_ = m.current.Unregister()
|
||||
close(m.stop)
|
||||
m.current = nil
|
||||
m.stop = nil
|
||||
}
|
||||
|
||||
mods, key, err := parseSpec(spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hot := hk.New(mods, key)
|
||||
if err := hot.Register(); err != nil {
|
||||
return fmt.Errorf("register hotkey %q: %w", spec, err)
|
||||
}
|
||||
|
||||
stop := make(chan struct{})
|
||||
m.current = hot
|
||||
m.stop = stop
|
||||
|
||||
// Listen on a dedicated goroutine. We never run the callback inline
|
||||
// because the keydown channel is unbuffered.
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-hot.Keydown():
|
||||
if onTrigger != nil {
|
||||
go onTrigger()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unregister releases the active hotkey, if any.
|
||||
func (m *Manager) Unregister() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.current != nil {
|
||||
_ = m.current.Unregister()
|
||||
close(m.stop)
|
||||
m.current = nil
|
||||
m.stop = nil
|
||||
}
|
||||
}
|
||||
|
||||
// parseSpec parses a "+"-separated hotkey description into (modifiers, key).
|
||||
//
|
||||
// Supported tokens (case-insensitive): cmd, command, ctrl, control,
|
||||
// option, alt, shift, plus a single letter A–Z or digit 0–9.
|
||||
//
|
||||
// We keep the parser intentionally tiny — full key mapping is out of scope
|
||||
// for the MVP.
|
||||
func parseSpec(spec string) ([]hk.Modifier, hk.Key, error) {
|
||||
parts := strings.Split(strings.ToLower(strings.TrimSpace(spec)), "+")
|
||||
if len(parts) == 0 {
|
||||
return nil, 0, fmt.Errorf("empty hotkey")
|
||||
}
|
||||
var mods []hk.Modifier
|
||||
var key hk.Key
|
||||
hasKey := false
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
switch p {
|
||||
case "cmd", "command", "meta", "super":
|
||||
mods = append(mods, modCmd())
|
||||
case "ctrl", "control":
|
||||
mods = append(mods, modCtrl())
|
||||
case "option", "alt":
|
||||
mods = append(mods, modOption())
|
||||
case "shift":
|
||||
mods = append(mods, hk.ModShift)
|
||||
default:
|
||||
k, ok := lookupKey(p)
|
||||
if !ok {
|
||||
return nil, 0, fmt.Errorf("unsupported token: %q", p)
|
||||
}
|
||||
key = k
|
||||
hasKey = true
|
||||
}
|
||||
}
|
||||
if !hasKey {
|
||||
return nil, 0, fmt.Errorf("hotkey %q is missing a key", spec)
|
||||
}
|
||||
return mods, key, nil
|
||||
}
|
||||
Reference in New Issue
Block a user