feat: init

This commit is contained in:
micah 2026-03-28 19:27:32 +08:00
commit 6108e4d614
17 changed files with 547 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea
alfred-micah-workflow

20
Makefile Normal file
View File

@ -0,0 +1,20 @@
export CGO_CXXFLAGS_ALLOW:=.*
export CGO_LDFLAGS_ALLOW:=.*
export CGO_CFLAGS_ALLOW:=.*
# 跨平台开发,设置交叉编译
#export GOOS=linux
#export GOARCH=amd64
app:="alfred-micah-workflow"
.PHONY: all test clean
all: build
build:
@echo "\033[32m <============== making app ${app} =============> \033[0m"
go build -ldflags='-w -s' $(FLAGS) -o ./${app} ./
clean:
@echo -e "\033[32m ============== cleaning files =============> \033[0m"
rm -fv ${TARGET}

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# Alfred Workflow
## 打包
```shell
make
```
## 测试
```markdown
* 打开 Alfred → 点击左侧「Workflows」→ 右下角「+」→ 新建一个空白 Workflow
* 给 Workflow 命名比如「Command Hub」设置标识符唯一即可
* 添加「Trigger」触发方式
右侧点击 ---> Inputs ---> Script Filter
设置关键词(比如 m勾选「with space」输入关键词后按空格输入参数
添加「Run Script」执行脚本/Users/xxx/xxx/alfred-micah-workflow $1 $2 $3 $4 $5 $6 $7 $8
从 Trigger 拖出连线,选择 Actions → Run Script
「Language」选择「/bin/bash」
脚本内容填写:/path/to/your/alfred-micah-workflow "$1" "$2"(替换为你编译后的可执行文件绝对路径,$1 是 Alfred 传入的输入参数);
勾选「with input as argv」
可选添加「Copy to Clipboard」选中后复制
从 Run Script 拖出连线,选择 Actions → Copy to Clipboard
「Clipboard」选择「Argv」即我们代码中 Arg(url) 传递的参数);
保存后,在 Alfred 输入 gtest go就能看到匹配的结果选中后会自动复制对应的 URL 到剪贴板。
```

31
command/code/decode.go Normal file
View File

@ -0,0 +1,31 @@
package code
import (
"encoding/base64"
aw "github.com/deanishe/awgo"
"gitea.micah.wiki/pandora/alfred/model"
"gitea.micah.wiki/pandora/alfred/pkg/stringx"
)
func DecodeDo(args []string) []*model.Item {
if len(args) == 0 {
return nil
}
value := args[0]
bytes, err := base64.RawURLEncoding.DecodeString(value)
if err != nil {
return nil
}
val := string(bytes)
items := make([]*model.Item, 0)
items = append(items, &model.Item{
Title: val,
Subtitle: stringx.NewStrPoint("URL Decode"),
Icon: aw.IconInfo,
Arg: []string{"copytext", val},
Valid: true,
})
return items
}

28
command/code/encode.go Normal file
View File

@ -0,0 +1,28 @@
package code
import (
"encoding/base64"
aw "github.com/deanishe/awgo"
"gitea.micah.wiki/pandora/alfred/model"
"gitea.micah.wiki/pandora/alfred/pkg/stringx"
)
func EncodeDo(args []string) []*model.Item {
if len(args) == 0 {
return nil
}
encodeStr := args[0]
val := base64.RawURLEncoding.EncodeToString([]byte(encodeStr))
items := make([]*model.Item, 0)
items = append(items, &model.Item{
Title: val,
Subtitle: stringx.NewStrPoint("URL Encode"),
Icon: aw.IconInfo,
Arg: []string{"copytext", val},
Valid: true,
})
return items
}

24
command/command.go Normal file
View File

@ -0,0 +1,24 @@
package command
import (
"gitea.micah.wiki/pandora/alfred/command/code"
"gitea.micah.wiki/pandora/alfred/command/datex"
"gitea.micah.wiki/pandora/alfred/command/gourl"
"gitea.micah.wiki/pandora/alfred/command/help"
"gitea.micah.wiki/pandora/alfred/model"
)
func Do(command string, args []string) []*model.Item {
switch command {
case "date":
return datex.Do(args)
case "url", "go":
return gourl.Do(args)
case "encode":
return code.EncodeDo(args)
case "decode":
return code.DecodeDo(args)
default:
return help.Do(args)
}
}

69
command/datex/date.go Normal file
View File

@ -0,0 +1,69 @@
package datex
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
aw "github.com/deanishe/awgo"
"gitea.micah.wiki/pandora/alfred/model"
"gitea.micah.wiki/pandora/alfred/pkg/stringx"
)
var (
regexpTimestamp = regexp.MustCompile(`^[1-9]{1}\d+$`)
yyyyMMddHHMMSSLayout = "2006-01-02 15:04:05"
moreLayouts = []string{
"2006-01-02",
"2006-01-02 15:04",
"2006-01-02 15:04:05",
}
)
func Do(args []string) []*model.Item {
if len(args) == 0 {
return nil
}
// 处理 now
input := strings.Join(args, " ")
if input == "now" || input == "" {
input = fmt.Sprintf("%d", time.Now().Unix())
}
items := make([]*model.Item, 0)
if regexpTimestamp.MatchString(input) {
v, err := strconv.ParseInt(input, 10, 32)
if err != nil {
return nil
}
value := time.Unix(v, 0).Format(yyyyMMddHHMMSSLayout)
items = append(items, &model.Item{
Title: value,
Subtitle: stringx.NewStrPoint(yyyyMMddHHMMSSLayout),
Arg: []string{"copytext", value},
Valid: true,
Icon: aw.IconClock,
})
} else {
for _, layout := range moreLayouts {
v, err := time.Parse(layout, input)
if err != nil {
continue
}
value := fmt.Sprintf("%d", v.Unix())
items = append(items, &model.Item{
Title: value,
Subtitle: stringx.NewStrPoint(layout),
Arg: []string{"copytext", value},
Valid: true,
Icon: aw.IconClock,
})
}
}
if len(items) == 0 {
return nil
}
return items
}

120
command/gourl/gourl.go Normal file
View File

@ -0,0 +1,120 @@
package gourl
import (
"fmt"
"os"
aw "github.com/deanishe/awgo"
"gopkg.in/yaml.v3"
"gitea.micah.wiki/pandora/alfred/model"
"gitea.micah.wiki/pandora/alfred/pkg/logx"
"gitea.micah.wiki/pandora/alfred/pkg/stringx"
)
func getDefault() map[string][]*URLInfo {
return map[string][]*URLInfo{
"github": {{
Key: "github",
Title: "Github",
URL: "https://github.com/",
}},
"doubao": {{
Key: "doubao",
Title: "豆包",
URL: "https://www.doubao.com/chat",
}},
}
}
func Do(args []string) []*model.Item {
if len(args) == 0 {
return nil
}
logx.Info("gourl: %s", args[0])
urlMap := GetConfig()
if len(urlMap) == 0 {
logx.Info("配置文件未找到")
urlMap = getDefault()
}
if urls, ok := urlMap[args[0]]; ok {
logx.Info("go value: %+v", urls)
items := make([]*model.Item, 0)
if len(urls) > 0 {
for _, v := range urls {
items = append(items, &model.Item{
Title: fmt.Sprintf("回车 → 在浏览器打开 %s", v.Title),
Subtitle: stringx.NewStrPoint(v.URL),
UID: stringx.NewStrPoint("gourl-open-url"),
Arg: []string{"goto", v.URL},
Valid: true,
Icon: aw.IconNetwork,
})
}
}
return items
}
gotoConfigFile := os.Getenv("goto_config")
return []*model.Item{
{
Title: fmt.Sprintf("未找到 %s 相关跳转url配置", args[0]),
Subtitle: stringx.NewStrPoint(fmt.Sprintf("配置地址: %s", gotoConfigFile)),
UID: stringx.NewStrPoint("gourl-open-url"),
Arg: []string{"copytext", gotoConfigFile},
Valid: true,
Icon: aw.IconNetwork,
},
}
}
type URLInfo struct {
Key string `yaml:"key"`
Title string `yaml:"title"`
URL string `yaml:"url"`
}
type Config struct {
GoToConfig map[string][]*URLInfo `yaml:"goto_config"`
}
func GetConfig() map[string][]*URLInfo {
logx.Info("GetConfig start.")
cfgFile := os.Getenv("goto_config")
if cfgFile == "" {
logx.Info("读取配置文件为空")
return getDefault()
}
logx.Info(cfgFile)
// 读取配置文件内容
data, err := os.ReadFile(cfgFile)
if err != nil {
logx.Info("读取配置文件失败: %v", err)
return getDefault()
}
// 解析 YAML 到结构体
var cfg Config
if err = yaml.Unmarshal(data, &cfg); err != nil {
logx.Info("解析 YAML 失败: %v", err)
return getDefault()
}
if len(cfg.GoToConfig) == 0 {
logx.Info("读取配置文件未找到数据")
return getDefault()
}
c := make(map[string][]*URLInfo)
for _, list := range cfg.GoToConfig {
for _, info := range list {
for i := 1; i <= len(info.Key); i++ {
key := info.Key[:i]
v, ok := c[key]
if !ok {
v = make([]*URLInfo, 0)
}
v = append(v, info)
c[key] = v
}
}
}
logx.Info("config size: ", len(c))
return c
}

22
command/help/help.go Normal file
View File

@ -0,0 +1,22 @@
package help
import (
aw "github.com/deanishe/awgo"
"gitea.micah.wiki/pandora/alfred/model"
"gitea.micah.wiki/pandora/alfred/pkg/stringx"
)
func Do(_ []string) []*model.Item {
return []*model.Item{
{
Title: "目前支持命令(goto、decode、encode、date等)", // 主提示词
Subtitle: stringx.NewStrPoint("subtitle"), // 副提示词
Match: stringx.NewStrPoint("match"),
UID: stringx.NewStrPoint("help-goto"),
Arg: []string{"copytext", "help"}, // alfred 输出内容 回车会在剪切板
Valid: true,
Icon: aw.IconHelp,
},
}
}

BIN
dist/Command Hub.alfredworkflow vendored Normal file

Binary file not shown.

17
go.mod Normal file
View File

@ -0,0 +1,17 @@
module gitea.micah.wiki/pandora/alfred
go 1.24.0
toolchain go1.24.12
require (
github.com/deanishe/awgo v0.29.1
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/magefile/mage v1.15.0 // indirect
go.deanishe.net/env v0.5.1 // indirect
go.deanishe.net/fuzzy v1.0.0 // indirect
golang.org/x/text v0.33.0 // indirect
)

46
go.sum Normal file
View File

@ -0,0 +1,46 @@
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deanishe/awgo v0.29.1 h1:yKAyy0e+HR60iPxaKHhY3hdTM5GCsECpWTP79j04bHg=
github.com/deanishe/awgo v0.29.1/go.mod h1:1yGF+uQfWXX99TiDfAYYKjJpHTq5lHEmvHFEVCHo6KA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.deanishe.net/env v0.5.1 h1:WiOncK5uJj8Um57Vj2dc1bq1lMN7fgRag9up7I3LZy0=
go.deanishe.net/env v0.5.1/go.mod h1:ihEYfDm0K0hq3f5ACTCQDrMTWxH9fTiA1lh1i0aMqm0=
go.deanishe.net/fuzzy v1.0.0 h1:3Qp6PCX0DLb9z03b5OHwAGsbRSkgJpSLncsiDdXDt4Y=
go.deanishe.net/fuzzy v1.0.0/go.mod h1:2yEEMfG7jWgT1s5EO0TteVWmx2MXFBRMr5cMm84bQNY=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=

9
main.go Normal file
View File

@ -0,0 +1,9 @@
package main
import (
"gitea.micah.wiki/pandora/alfred/workflow"
)
func main() {
workflow.Do()
}

22
model/model.go Normal file
View File

@ -0,0 +1,22 @@
package model
import aw "github.com/deanishe/awgo"
type Item struct {
Title string
Subtitle *string
Match *string
UID *string
Autocomplete *string
Arg []string
Valid bool
File bool
Copytext *string
LargeType *string
QL *string
Vars map[string]string
Mods map[string]*aw.Modifier
Actions map[string][]string
Icon *aw.Icon
NoUID bool // Suppress UID in JSON
}

10
pkg/logx/log.go Normal file
View File

@ -0,0 +1,10 @@
package logx
import (
"fmt"
"os"
)
func Info(template string, args ...interface{}) {
_, _ = fmt.Fprintf(os.Stderr, template, args)
}

5
pkg/stringx/string.go Normal file
View File

@ -0,0 +1,5 @@
package stringx
func NewStrPoint(val string) *string {
return &val
}

98
workflow/workflow.go Normal file
View File

@ -0,0 +1,98 @@
package workflow
import (
aw "github.com/deanishe/awgo"
"gitea.micah.wiki/pandora/alfred/command"
)
// 初始化 aw 实例,这是 awgo 库的核心对象
var wf *aw.Workflow
func Do() {
// 初始化 Workflow启用日志方便调试日志会输出到 Alfred 的调试面板)
wf = aw.New()
// 初始化错误及校验参数
var err error
args := wf.Args()
if len(args) <= 1 {
return
}
// 默认返回 SendFeed
defer func() {
if err == nil {
wf.SendFeedback()
return
}
}()
// 命令获取
cmd := args[0]
params := make([]string, 0)
if len(args) > 1 {
params = args[1:]
}
items := command.Do(cmd, params)
wfItems := make([]*aw.Item, len(items))
for i, item := range items {
wfItem := &aw.Item{}
if item.Title != "" {
wfItem.Title(item.Title)
}
if item.Subtitle != nil {
wfItem.Subtitle(*item.Subtitle)
}
if item.Match != nil {
wfItem.Match(*item.Match)
}
if item.UID != nil {
wfItem.UID(*item.UID)
}
if item.Autocomplete != nil {
wfItem.Autocomplete(*item.Autocomplete)
}
if len(item.Arg) > 0 {
wfItem.Arg(item.Arg...)
}
if item.Valid {
wfItem.Valid(item.Valid)
}
if item.File {
wfItem.IsFile(item.File)
}
if item.Copytext != nil {
wfItem.Copytext(*item.Copytext)
}
if item.LargeType != nil {
wfItem.Largetype(*item.LargeType)
}
if item.QL != nil {
wfItem.Quicklook(*item.QL)
}
if len(item.Vars) > 0 {
for key, value := range item.Vars {
wfItem.Var(key, value)
}
}
if len(item.Mods) > 0 {
for _, mod := range item.Mods {
wfItem.SetModifier(mod)
}
}
if len(item.Actions) > 0 {
for t, action := range item.Actions {
wfItem.ActionForType(t, action...)
}
}
if item.Icon != nil {
wfItem.Icon(item.Icon)
}
wfItems[i] = wfItem
}
if len(items) > 0 {
wf.Feedback.Items = wfItems
}
}