diff --git a/main.go b/main.go index 78207b3..96178b5 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,10 @@ package main import ( + "fmt" "log" "razer/utils" + "time" "github.com/karalabe/hid" "github.com/progrium/darwinkit/macos/appkit" @@ -19,6 +21,76 @@ func main() { app.SetActivationPolicy(appkit.ApplicationActivationPolicyAccessory) utils.SetupStatusBar(app) + // 【新增】在主线程提前初始化色盘单例 + // 这样后面协程调用它时,它已经存在了,不会触发线程检查崩溃 + _ = appkit.ColorPanel_SharedColorPanel() + // 2. 启动颜色监听协程 + go func() { + var lastColor string + for { + time.Sleep(100 * time.Millisecond) + + // 关键:切换回主线程来获取色盘和颜色 + // 在 DarwinKit 中,通常我们可以直接在菜单点击时先初始化它, + // 或者使用 dispatch 保证安全。 + var r, g, b int + var isVisible bool + var colorFound bool + // 使用 WithAutoreleasePool 并在主线程执行 + objc.WithAutoreleasePool(func() { + // 注意:有些 DarwinKit 版本需要 dispatch 包 + // 如果没有 dispatch 包,最简单的办法是在 main 启动前 + // 先在主线程调用一次 ColorPanel_SharedColorPanel() + + cp := appkit.ColorPanel_SharedColorPanel() + if cp.IsVisible() { + isVisible = true + c := cp.Color().ColorUsingColorSpace(appkit.ColorSpace_DeviceRGBColorSpace()) + if !c.IsNil() { + r = int(c.RedComponent() * 255) + g = int(c.GreenComponent() * 255) + b = int(c.BlueComponent() * 255) + colorFound = true + } + } + }) + + if isVisible && colorFound { + curr := fmt.Sprintf("%d-%d-%d", r, g, b) + if curr != lastColor { + lastColor = curr + //fmt.Printf("颜色更新: R:%d G:%d B:%d\n", r, g, b) + //fmt.Printf("内容: R:%#v ", utils.Temp) + // 这里写 HID 设备,这里已经是 Go 协程了,不会阻塞 UI + + // 1. 转换并更新你的缓存 + // 这里的 BreathingState 结构体可能需要增加 R, G, B 字段 + // 设置颜色 + utils.Temp.TempState.SetColor([]byte{byte(r), byte(g), byte(b)}) + for _, dev := range utils.Devs { + key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) + if utils.Temp.TempKey != key { + continue + } + switch utils.Temp.TempState.GetEffect() { + // 处理静态 + case utils.KBD_EFFECT_STATIC: + dev.SetStaticColor(utils.Temp.TempState.GetColor()) + // 处理呼吸 + case utils.KBD_EFFECT_BREATHING: + dev.SetBreathingColor(byte(utils.Temp.TempState.GetModel()), utils.Temp.TempState.GetColor(), []byte{}, byte(utils.Temp.TempState.GetSpeed())) + } + } + utils.DeviceStateCache[utils.Temp.TempKey] = utils.Temp.TempState + //fmt.Printf("内容: R:%#v ", utils.DeviceStateCache[utils.Temp.TempKey]) + // 2. 立即发送到硬件 + // 这里的具体的转换逻辑需要匹配你的 HID 库 + //payload := []byte{byte(r), byte(g), byte(b)} + //fmt.Printf("发送字节流到硬件: %v\n", payload) + } + } + } + }() app.ActivateIgnoringOtherApps(true) app.Run() diff --git a/utils/color.go b/utils/color.go index c975524..bed5e9f 100644 --- a/utils/color.go +++ b/utils/color.go @@ -14,10 +14,55 @@ var PRESET_COLORS = []struct { {"黄色", 0xFF, 0xFF, 0x00}, {"橙色", 0xFF, 0xA5, 0x00}, {"紫色", 0x80, 0x00, 0x80}, + {"自定义", 0x00, 0x00, 0x00}, //{"熄灭", 0x00, 0x00, 0x00}, } +// PRESET_MULTI_COLORS 预设颜色 +var PRESET_MULTI_COLORS = []struct { + Name string + Color1 []byte + Color2 []byte +}{ + // 霓虹紫蓝 (非常电竞范) + {"霓虹紫蓝", + []byte{0xFF, 0x00, 0xFF}, // 紫色 (Magenta) + []byte{0x00, 0xFF, 0xFF}, // 青蓝色 (Cyan) + }, + + // 冰火 (经典红蓝对比) + {"冰火", + []byte{0xFF, 0x00, 0x00}, // 纯红 + []byte{0x00, 0x00, 0xFF}, // 纯蓝 + }, + + // 赛博朋克 (经典配色) + {"赛博粉黄", + []byte{0xFF, 0x00, 0x7F}, // 玫粉色 + []byte{0xFF, 0xD7, 0x00}, // 金黄色 + }, + + // 森林气息 (柔和过渡) + {"森翠", + []byte{0x00, 0xFF, 0x00}, // 绿色 + []byte{0x00, 0xFF, 0x7F}, // 春绿 + }, + + // 晚霞 (暖色调) + {"晚霞", + []byte{0xFF, 0x45, 0x00}, // 橙红 + []byte{0xFF, 0x14, 0x93}, // 深粉 + }, + + // 黑客帝国 (极简) + {"矩阵", + []byte{0x00, 0xFF, 0x00}, // 翠绿 + []byte{0x00, 0x33, 0x00}, // 暗绿 (深邃感) + }, + //{"熄灭", 0x00, 0x00, 0x00}, +} + // 雷蛇绿 (标志性的 Razer Green) //razerGreen := []byte{0x00, 0xFF, 0x00} // 或者稍微深一点的 0x44, 0xFF, 0x00 @@ -85,16 +130,18 @@ func (rd *RazerDevice) SetOffLightsColor(color []byte) { } // SetBreathingColor 设置双色呼吸 -func (rd *RazerDevice) SetBreathingColor() { +func (rd *RazerDevice) SetBreathingColor(subMode byte, rgb1 []byte, rgb2 []byte, speed byte) { // 1. 定义呼吸子模式 - subMode := byte(0x02) // 0x02 为双色切换呼吸 - rgb1 := []byte{0xFF, 0x00, 0x00} // 颜色1: 红色 - rgb2 := []byte{0x00, 0x00, 0xFF} // 颜色2: 蓝色 - speed := byte(0x01) // 速度: 1(快) - 3(慢), 200可能过大,建议先用1测试 + //subMode := byte(0x02) // 0x02 为双色切换呼吸 + //rgb1 := []byte{0xFF, 0x00, 0x00} // 颜色1: 红色 + //rgb2 := []byte{0x00, 0x00, 0xFF} // 颜色2: 蓝色 + //speed := byte(0x01) // 速度: 1(快) - 3(慢), 200可能过大,建议先用1测试 + //fmt.Println(subMode) // 2. 构造参数包 (总长 8 字节) // 结构: [SubMode, R1, G1, B1, R2, G2, B2, Speed] breathingParams := append([]byte{subMode}, append(rgb1, append(rgb2, speed)...)...) + fmt.Println(breathingParams) // 3. 调用新版 buildArguments (内部不再强制加 0x01) args := buildArguments(KBD_EFFECT_BREATHING, KBD_BACKLIGHT_LED, breathingParams) diff --git a/utils/const.go b/utils/const.go index 2a25218..2e81fc3 100644 --- a/utils/const.go +++ b/utils/const.go @@ -1,5 +1,11 @@ package utils +import ( + "fmt" + + "github.com/progrium/darwinkit/macos/appkit" +) + // --- 硬件识别常量 --- // RAZER_VID (Vendor ID) 是雷蛇在 USB 协会注册的厂商唯一识别码 @@ -60,3 +66,32 @@ const KBD_BACKLIGHT_LED = 0x05 // 键盘背光灯光控制索引 const KBD_CMD_CLASS = 0x0F // 键盘指令分类 (Command Class) const KBD_CMD_ID = 0x02 // 键盘指令标识 (Command ID) const KBD_DATA_SIZE = 9 // 键盘数据包的有效负载长度 + +// 全局变量,追踪当前色盘控制的设备 +var ( + activeDeviceKey string + colorPanel appkit.ColorPanel +) + +func initColorPanel() { + // 修复:unsafe.Pointer 与 nil 比较 + if colorPanel.Ptr() == nil { + colorPanel = appkit.ColorPanel_SharedColorPanel() + colorPanel.SetContinuous(true) + } +} + +func openColorPicker() { + // 1. 获取共享的色盘实例(推荐做法) + colorPanel := appkit.ColorPanel_SharedColorPanel() + + // 2. 配置 Target/Action (可选) + // 注意:在 Go 中处理 Selector 回调比较复杂,通常需要注册一个特定的 Class + + // 3. 激活应用并显示色盘 + // 如果你的程序不是 GUI 模式,色盘可能会隐藏在其他窗口后面 + appkit.Application_SharedApplication().ActivateIgnoringOtherApps(true) + colorPanel.MakeKeyAndOrderFront(nil) + + fmt.Println("Color Picker 已经尝试打开") +} diff --git a/utils/menu.go b/utils/menu.go index f3a9fc7..2587660 100644 --- a/utils/menu.go +++ b/utils/menu.go @@ -7,13 +7,105 @@ import ( "github.com/progrium/darwinkit/objc" ) -var deviceStateCache = make(map[string]State) +var Devs map[string]*RazerDevice = make(map[string]*RazerDevice) -type State struct { - Effect int - Color string +var Temp = TempData{} + +type TempData struct { + TempKey string + TempState State } +var DeviceStateCache = make(map[string]State) + +type State interface { + GetEffect() int + GetColorKey() string + GetMultiColorKey() string + GetModel() int + SetColor([]byte) + GetColor() []byte + GetColor1() []byte + GetColor2() []byte + GetSpeed() int +} + +// StaticState 常亮模式 +type StaticState struct { + Effect int + ColorKey string + Color []byte +} + +func (s *StaticState) GetEffect() int { + return s.Effect +} +func (s *StaticState) GetModel() int { + return 0 +} +func (s *StaticState) GetColorKey() string { + return s.ColorKey +} +func (s *StaticState) GetMultiColorKey() string { + return "" +} +func (s *StaticState) SetColor(color []byte) { + s.Color = color +} +func (s *StaticState) GetColor() []byte { + return s.Color +} +func (s *StaticState) GetColor1() []byte { + return []byte{} + +} +func (s *StaticState) GetColor2() []byte { + return []byte{} + +} +func (s *StaticState) GetSpeed() int { + return 0 +} + +// BreathingState 呼吸模式 +type BreathingState struct { + Effect int + Model int // 单色呼吸 双色呼吸 随机颜色 + Speed int // 快 中 慢 + Color []byte + ColorKey string + Color1 []byte // 多色 + Color2 []byte // 多色 + MultiColorKey string // 多色 +} + +func (b *BreathingState) GetEffect() int { + return b.Effect +} +func (b *BreathingState) GetColorKey() string { + return b.ColorKey +} +func (b *BreathingState) GetModel() int { + return b.Model +} +func (b *BreathingState) GetMultiColorKey() string { + return b.MultiColorKey +} +func (b *BreathingState) SetColor(color []byte) { + b.Color = color +} +func (b *BreathingState) GetColor() []byte { + return b.Color +} +func (b *BreathingState) GetSpeed() int { + return b.Speed +} +func (b *BreathingState) GetColor1() []byte { + return b.Color1 +} +func (b *BreathingState) GetColor2() []byte { + return b.Color2 +} func UpdateMenu(menu appkit.Menu, app appkit.Application, statusBarItem appkit.StatusItem) { menu.RemoveAllItems() @@ -25,7 +117,7 @@ func UpdateMenu(menu appkit.Menu, app appkit.Application, statusBarItem appkit.S menu.AddItem(appkit.MenuItem_SeparatorItem()) devices := ScanDevices() - + Devs = devices // 更新 ToolTip 显示当前连接数 statusBarItem.Button().SetToolTip(fmt.Sprintf("雷蛇控制器 - 已连接设备: %d", len(devices))) @@ -35,6 +127,9 @@ func UpdateMenu(menu appkit.Menu, app appkit.Application, statusBarItem appkit.S menu.AddItem(none) } else { for _, dev := range devices { + + key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) + stateVal := DeviceStateCache[key] // 设备标题 (不可点击) devTitle := appkit.NewMenuItemWithAction("⌨️ "+dev.Name, "", func(sender objc.Object) {}) devTitle.SetEnabled(false) @@ -53,14 +148,21 @@ func UpdateMenu(menu appkit.Menu, app appkit.Application, statusBarItem appkit.S colorMenu := appkit.NewMenuWithTitle("Colors") for _, c := range PRESET_COLORS { colorItem := appkit.NewMenuItemWithAction(c.Name, "", func(sender objc.Object) { - //dev.ApplyEffect(KBD_EFFECT_STATIC, c.R, c.G, c.B) - dev.SetStaticColor([]byte{c.R, c.G, c.B}) - // 更新状态与缓存 - key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) - deviceStateCache[key] = State{Effect: KBD_EFFECT_STATIC, Color: c.Name} + if c.Name == "自定义" { + DeviceStateCache[key] = &StaticState{Effect: KBD_EFFECT_STATIC, ColorKey: c.Name} + Temp = TempData{TempKey: key, TempState: DeviceStateCache[key]} // [key] = StaticState{Effect: KBD_EFFECT_STATIC, ColorKey: c.Name} + openColorPicker() + } else { + //dev.ApplyEffect(KBD_EFFECT_STATIC, c.R, c.G, c.B) + dev.SetStaticColor([]byte{c.R, c.G, c.B}) + // 更新状态与缓存 + DeviceStateCache[key] = &StaticState{Effect: KBD_EFFECT_STATIC, ColorKey: c.Name, Color: []byte{c.R, c.G, c.B}} + } + UpdateMenu(menu, app, statusBarItem) }) - if dev.CurrentEffect == KBD_EFFECT_STATIC && dev.CurrentColor == c.Name { + stateVal := DeviceStateCache[key] + if stateVal != nil && dev.CurrentEffect == KBD_EFFECT_STATIC && stateVal.GetColorKey() == c.Name { colorItem.SetState(appkit.ControlStateValueOn) } colorMenu.AddItem(colorItem) @@ -69,52 +171,123 @@ func UpdateMenu(menu appkit.Menu, app appkit.Application, statusBarItem appkit.S menu.AddItem(staticItem) // --- B. 呼吸模式 --- - breathItem := appkit.NewMenuItemWithAction("🌬️ 呼吸模式", "", func(sender objc.Object) { - dev.SetBreathingColor() // 默认绿色 - key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) - deviceStateCache[key] = State{Effect: KBD_EFFECT_BREATHING, Color: ""} - UpdateMenu(menu, app, statusBarItem) - }) + breathItem := appkit.NewMenuItemWithAction("🌬️ 呼吸模式", "", func(sender objc.Object) {}) if dev.CurrentEffect == KBD_EFFECT_BREATHING { breathItem.SetState(appkit.ControlStateValueOn) } + breathRootMenu := appkit.NewMenuWithTitle("BreathingRoot") + + singleItem := appkit.NewMenuItemWithAction("单色模式", "", func(sender objc.Object) {}) + singleSub := appkit.NewMenuWithTitle("Single") + for _, color := range PRESET_COLORS { + item := appkit.NewMenuItemWithAction(color.Name, "", func(sender objc.Object) { + if color.Name == "自定义" { + DeviceStateCache[key] = &StaticState{Effect: KBD_EFFECT_BREATHING, ColorKey: color.Name} + Temp = TempData{TempKey: key, TempState: DeviceStateCache[key]} // [key] = StaticState{Effect: KBD_EFFECT_STATIC, ColorKey: c.Name} + openColorPicker() + } else { + if stateVal != nil { + dev.SetBreathingColor(0x01, []byte{color.R, color.G, color.B}, []byte{}, byte(stateVal.GetSpeed())) + } else { + dev.SetBreathingColor(0x01, []byte{color.R, color.G, color.B}, []byte{}, byte(1)) + } + } + + //dev.SetBreathing(0x01, color.R, color.G, color.B, 0, 0, 0, curState.Speed) + // 0x01: 单色 (只看第一组 RGB) + // 0x02: 双色 (看两组 RGB) + // 0x03: 随机 + DeviceStateCache[key] = &BreathingState{Effect: KBD_EFFECT_BREATHING, Model: 0x01, ColorKey: color.Name, Color: []byte{color.R, color.G, color.B}} + UpdateMenu(menu, app, statusBarItem) + }) + stateVal := DeviceStateCache[key] + if dev.CurrentEffect == KBD_EFFECT_BREATHING && stateVal != nil && stateVal.GetModel() == 0x01 && stateVal.GetColorKey() == color.Name { + item.SetState(appkit.ControlStateValueOn) + } + singleSub.AddItem(item) + } + stateVal = DeviceStateCache[key] + if dev.CurrentEffect == KBD_EFFECT_BREATHING && stateVal != nil && stateVal.GetModel() == 0x01 { + singleItem.SetState(appkit.ControlStateValueOn) + } + singleItem.SetSubmenu(singleSub) + breathRootMenu.AddItem(singleItem) + + // B2. 双色呼吸 (红 <-> 自选) + dualItem := appkit.NewMenuItemWithAction("双色模式", "", func(sender objc.Object) {}) + dualSub := appkit.NewMenuWithTitle("Dual") + for _, multiColor := range PRESET_MULTI_COLORS { + item := appkit.NewMenuItemWithAction(multiColor.Name, "", func(sender objc.Object) { + if stateVal != nil { + dev.SetBreathingColor(0x02, multiColor.Color1, multiColor.Color2, byte(stateVal.GetSpeed())) + } else { + dev.SetBreathingColor(0x02, multiColor.Color1, multiColor.Color2, byte(1)) + } + DeviceStateCache[key] = &BreathingState{Effect: KBD_EFFECT_BREATHING, Model: 0x02, MultiColorKey: multiColor.Name, Color1: multiColor.Color1, Color2: multiColor.Color2} + UpdateMenu(menu, app, statusBarItem) + }) + if dev.CurrentEffect == KBD_EFFECT_BREATHING && stateVal != nil && stateVal.GetModel() == 0x02 && stateVal.GetMultiColorKey() == multiColor.Name { + item.SetState(appkit.ControlStateValueOn) + } + dualSub.AddItem(item) + } + stateVal = DeviceStateCache[key] + if dev.CurrentEffect == KBD_EFFECT_BREATHING && stateVal != nil && stateVal.GetModel() == 0x02 { + dualItem.SetState(appkit.ControlStateValueOn) + } + dualItem.SetSubmenu(dualSub) + breathRootMenu.AddItem(dualItem) + + // B3. 随机 + randItem := appkit.NewMenuItemWithAction("随机颜色", "", func(sender objc.Object) { + DeviceStateCache[key] = &BreathingState{Effect: KBD_EFFECT_BREATHING, Model: 0x03} + UpdateMenu(menu, app, statusBarItem) + }) + stateVal = DeviceStateCache[key] + if dev.CurrentEffect == KBD_EFFECT_BREATHING && stateVal != nil && stateVal.GetModel() == 0x03 { + randItem.SetState(appkit.ControlStateValueOn) + } + breathRootMenu.AddItem(randItem) + breathRootMenu.AddItem(appkit.MenuItem_SeparatorItem()) + + breathItem.SetSubmenu(breathRootMenu) menu.AddItem(breathItem) - // --- C. 波浪模式 --- - waveItem := appkit.NewMenuItemWithAction("🌊 波浪模式", "", func(sender objc.Object) { - //dev.ApplyEffect(KBD_EFFECT_WAVE, 0x01, 0x00, 0x00) - key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) - deviceStateCache[key] = State{Effect: KBD_EFFECT_WAVE, Color: ""} - UpdateMenu(menu, app, statusBarItem) - }) - if dev.CurrentEffect == KBD_EFFECT_WAVE { - waveItem.SetState(appkit.ControlStateValueOn) - } - menu.AddItem(waveItem) + //// --- C. 波浪模式 --- + //waveItem := appkit.NewMenuItemWithAction("🌊 波浪模式", "", func(sender objc.Object) { + // //dev.ApplyEffect(KBD_EFFECT_WAVE, 0x01, 0x00, 0x00) + // key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) + // deviceStateCache[key] = State{Effect: KBD_EFFECT_WAVE, Color: ""} + // UpdateMenu(menu, app, statusBarItem) + //}) + //if dev.CurrentEffect == KBD_EFFECT_WAVE { + // waveItem.SetState(appkit.ControlStateValueOn) + //} + //menu.AddItem(waveItem) // --- D. 响应模式 --- - reactiveItem := appkit.NewMenuItemWithAction("🌊 响应模式", "", func(sender objc.Object) { - //dev.ApplyEffect(KBD_EFFECT_WAVE, 0x01, 0x00, 0x00) - key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) - deviceStateCache[key] = State{Effect: KBD_EFFECT_REACTIVE, Color: ""} - UpdateMenu(menu, app, statusBarItem) - }) - if dev.CurrentEffect == KBD_EFFECT_REACTIVE { - reactiveItem.SetState(appkit.ControlStateValueOn) - } - menu.AddItem(reactiveItem) + //reactiveItem := appkit.NewMenuItemWithAction("🌊 响应模式", "", func(sender objc.Object) { + // //dev.ApplyEffect(KBD_EFFECT_WAVE, 0x01, 0x00, 0x00) + // key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) + // deviceStateCache[key] = State{Effect: KBD_EFFECT_REACTIVE, Color: ""} + // UpdateMenu(menu, app, statusBarItem) + //}) + //if dev.CurrentEffect == KBD_EFFECT_REACTIVE { + // reactiveItem.SetState(appkit.ControlStateValueOn) + //} + //menu.AddItem(reactiveItem) // --- E. 关灯模式 --- 关灯 - offLightsItem := appkit.NewMenuItemWithAction("关灯模式", "", func(sender objc.Object) { - dev.SetOffLightsColor([]byte{0x00, 0x00, 0x00}) - key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) - deviceStateCache[key] = State{Effect: KBD_EFFECT_OFFLIGHTS, Color: ""} - UpdateMenu(menu, app, statusBarItem) - }) - if dev.CurrentEffect == KBD_EFFECT_OFFLIGHTS { - offLightsItem.SetState(appkit.ControlStateValueOn) - } - menu.AddItem(offLightsItem) + //offLightsItem := appkit.NewMenuItemWithAction("关灯模式", "", func(sender objc.Object) { + // dev.SetOffLightsColor([]byte{0x00, 0x00, 0x00}) + // //key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) + // deviceStateCache[key] = State{Effect: KBD_EFFECT_OFFLIGHTS, Color: ""} + // UpdateMenu(menu, app, statusBarItem) + //}) + //if dev.CurrentEffect == KBD_EFFECT_OFFLIGHTS { + // offLightsItem.SetState(appkit.ControlStateValueOn) + //} + //menu.AddItem(offLightsItem) //modeMenuItem.SetSubmenu(modeSubMenu) //menu.AddItem(modeMenuItem) diff --git a/utils/tools.go b/utils/tools.go index aeb8352..90d7064 100644 --- a/utils/tools.go +++ b/utils/tools.go @@ -168,9 +168,9 @@ func ScanDevices() map[string]*RazerDevice { } // 从缓存恢复状态 - if s, found := deviceStateCache[key]; found { - grouped[key].CurrentEffect = s.Effect - grouped[key].CurrentColor = s.Color + if s, found := DeviceStateCache[key]; found { + grouped[key].CurrentEffect = s.GetEffect() + //grouped[key].CurrentColor = s.Color } }