commit 6108e4d61405143e2b9071c0a9672218273759fb Author: micah Date: Sat Mar 28 19:27:32 2026 +0800 feat: init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..015bc30 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +alfred-micah-workflow \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4e714f2 --- /dev/null +++ b/Makefile @@ -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} + diff --git a/README.md b/README.md new file mode 100644 index 0000000..92ca164 --- /dev/null +++ b/README.md @@ -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 到剪贴板。 +``` diff --git a/command/code/decode.go b/command/code/decode.go new file mode 100644 index 0000000..4af54e3 --- /dev/null +++ b/command/code/decode.go @@ -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 +} diff --git a/command/code/encode.go b/command/code/encode.go new file mode 100644 index 0000000..610d9f9 --- /dev/null +++ b/command/code/encode.go @@ -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 +} diff --git a/command/command.go b/command/command.go new file mode 100644 index 0000000..ffb1ac9 --- /dev/null +++ b/command/command.go @@ -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) + } +} diff --git a/command/datex/date.go b/command/datex/date.go new file mode 100644 index 0000000..66401ee --- /dev/null +++ b/command/datex/date.go @@ -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 +} diff --git a/command/gourl/gourl.go b/command/gourl/gourl.go new file mode 100644 index 0000000..be629bd --- /dev/null +++ b/command/gourl/gourl.go @@ -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 +} diff --git a/command/help/help.go b/command/help/help.go new file mode 100644 index 0000000..d0a7752 --- /dev/null +++ b/command/help/help.go @@ -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, + }, + } +} diff --git a/dist/Command Hub.alfredworkflow b/dist/Command Hub.alfredworkflow new file mode 100644 index 0000000..bfd7c7a Binary files /dev/null and b/dist/Command Hub.alfredworkflow differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4d31479 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4a1f3ee --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..2df4526 --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "gitea.micah.wiki/pandora/alfred/workflow" +) + +func main() { + workflow.Do() +} diff --git a/model/model.go b/model/model.go new file mode 100644 index 0000000..6583da0 --- /dev/null +++ b/model/model.go @@ -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 +} diff --git a/pkg/logx/log.go b/pkg/logx/log.go new file mode 100644 index 0000000..8ebb733 --- /dev/null +++ b/pkg/logx/log.go @@ -0,0 +1,10 @@ +package logx + +import ( + "fmt" + "os" +) + +func Info(template string, args ...interface{}) { + _, _ = fmt.Fprintf(os.Stderr, template, args) +} diff --git a/pkg/stringx/string.go b/pkg/stringx/string.go new file mode 100644 index 0000000..9598239 --- /dev/null +++ b/pkg/stringx/string.go @@ -0,0 +1,5 @@ +package stringx + +func NewStrPoint(val string) *string { + return &val +} diff --git a/workflow/workflow.go b/workflow/workflow.go new file mode 100644 index 0000000..ada42e5 --- /dev/null +++ b/workflow/workflow.go @@ -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 + } +}