From 79e28e454ab8c73e936d42653f15d66cbda223f0 Mon Sep 17 00:00:00 2001 From: Shen Junzheng Date: Tue, 31 Oct 2023 20:00:03 +0800 Subject: [PATCH] alfred workflow support (#14) --- cmd/ips/cmd_myip.go | 6 +- cmd/ips/cmd_root.go | 4 + cmd/ips/config.go | 7 ++ internal/ips/config.go | 3 + internal/ips/find.go | 90 ++++++++++++++-------- pkg/model/output.go | 92 +++++++++++++++++++++++ script/alfredworkflow/ips.alfredworkflow | Bin 0 -> 2499 bytes 7 files changed, 168 insertions(+), 34 deletions(-) create mode 100644 pkg/model/output.go create mode 100644 script/alfredworkflow/ips.alfredworkflow diff --git a/cmd/ips/cmd_myip.go b/cmd/ips/cmd_myip.go index d8840a1..8048093 100644 --- a/cmd/ips/cmd_myip.go +++ b/cmd/ips/cmd_myip.go @@ -17,6 +17,8 @@ package ips import ( + "fmt" + "github.com/spf13/cobra" ) @@ -48,6 +50,8 @@ func init() { myipCmd.Flags().StringVarP(&rootTextValuesSep, "text-values-sep", "", "", "Specify the separator for values in text output. (default is space)") myipCmd.Flags().BoolVarP(&rootJson, "json", "j", false, "Output the results in JSON format.") myipCmd.Flags().BoolVarP(&rootJsonIndent, "json-indent", "", false, "Output the results in indent JSON format.") + myipCmd.Flags().BoolVarP(&rootAlfred, "alfred", "", false, "Output the results in Alfred format.") + } var myipCmd = &cobra.Command{ @@ -66,5 +70,5 @@ func MyIP(cmd *cobra.Command, args []string) { if err != nil { return } - cmd.Println(ip) + fmt.Println(ip) } diff --git a/cmd/ips/cmd_root.go b/cmd/ips/cmd_root.go index 079713c..75e61b2 100644 --- a/cmd/ips/cmd_root.go +++ b/cmd/ips/cmd_root.go @@ -57,6 +57,7 @@ func init() { rootCmd.Flags().StringVarP(&rootTextValuesSep, "text-values-sep", "", "", "Specify the separator for values in text output. (default is space)") rootCmd.Flags().BoolVarP(&rootJson, "json", "j", false, "Output the results in JSON format.") rootCmd.Flags().BoolVarP(&rootJsonIndent, "json-indent", "", false, "Output the results in indent JSON format.") + rootCmd.Flags().BoolVarP(&rootAlfred, "alfred", "", false, "Output the results in Alfred format.") } var rootCmd = &cobra.Command{ @@ -101,6 +102,9 @@ func Root(cmd *cobra.Command, args []string) { if err != nil { log.Fatal(err) } + if len(ret) == 0 { + continue + } fmt.Println(ret) } return diff --git a/cmd/ips/config.go b/cmd/ips/config.go index ba7fc20..9f3314e 100644 --- a/cmd/ips/config.go +++ b/cmd/ips/config.go @@ -85,6 +85,9 @@ var ( // rootJsonIndent defines whether to output in indented JSON format. rootJsonIndent bool + // rootAlfred defines whether to output in Alfred format. + rootAlfred bool + // dump & pack command flags // operate // dpFields specifies the fields to output for dump and pack operations. @@ -194,6 +197,10 @@ func GetFlagConfig() *ips.Config { conf.JsonIndent = rootJsonIndent } + if rootAlfred { + conf.OutputType = ips.OutputTypeAlfred + } + // dump & pack command flags if len(dpFields) != 0 { conf.DPFields = dpFields diff --git a/internal/ips/config.go b/internal/ips/config.go index 04e3d06..f6383dc 100644 --- a/internal/ips/config.go +++ b/internal/ips/config.go @@ -35,6 +35,9 @@ const ( // OutputTypeJSON represents the JSON output format. OutputTypeJSON = "json" + // OutputTypeAlfred represents the Alfred output format. + OutputTypeAlfred = "alfred" + // DefaultFields represents the default output fields. DefaultFields = "country,province,city,isp" ) diff --git a/internal/ips/find.go b/internal/ips/find.go index cbe940c..1caf132 100644 --- a/internal/ips/find.go +++ b/internal/ips/find.go @@ -42,24 +42,25 @@ import ( // based on the Manager configuration. It returns the combined result as a string. func (m *Manager) ParseText(text string) (string, error) { - buf := &bytes.Buffer{} tp := parser.NewTextParser(text).Parse() + infoList := make([]interface{}, 0, len(tp.Segments)) for _, segment := range tp.Segments { info, err := m.parseSegment(segment) if err != nil { log.Debug("m.parseSegment error: ", err) return "", err } - result, err := m.serialize(segment, info) - if err != nil { - log.Debug("m.serialize error: ", err) - return "", err - } - buf.WriteString(result) + infoList = append(infoList, info) + } + + result, err := m.serialize(infoList) + if err != nil { + log.Debug("m.serialize error: ", err) + return "", err } - return buf.String(), nil + return result, nil } // parseSegment processes the provided segment and returns the corresponding data. @@ -127,49 +128,72 @@ func (m *Manager) parseDomain(content string) (*model.DomainInfo, error) { // serialize takes a segment and its associated data, then serializes the data // based on the Manager configuration and returns the serialized string. -func (m *Manager) serialize(segment parser.Segment, data interface{}) (string, error) { +func (m *Manager) serialize(data []interface{}) (string, error) { switch m.Conf.OutputType { case OutputTypeJSON: - switch v := data.(type) { - case *model.IPInfo: - return m.serializeIPInfoToJSON(v) - case *model.DomainInfo: - case string: - return v, nil + list := &model.DataList{} + for _, info := range data { + switch v := info.(type) { + case *model.IPInfo: + list.AddItem(v.Output(m.Conf.UseDBFields)) + case *model.DomainInfo: + case string: + continue + } + } + return m.serializeDataToJSON(list) + case OutputTypeAlfred: + list := &model.DataList{} + for _, info := range data { + switch v := info.(type) { + case *model.IPInfo: + list.AddAlfredItemByIPInfo(v) + case *model.DomainInfo: + case string: + continue + } } + list.AddAlfredItemEmpty() + return m.serializeDataToJSON(list) default: // default is OutputTypeText - switch v := data.(type) { - case *model.IPInfo: - return m.serializeIPInfoToText(segment.Content, v) - case *model.DomainInfo: - case string: - return v, nil + buf := &bytes.Buffer{} + for _, info := range data { + switch v := info.(type) { + case *model.IPInfo: + ret, err := m.serializeIPInfoToText(v) + if err != nil { + return "", err + } + buf.WriteString(ret) + case *model.DomainInfo: + case string: + buf.WriteString(v) + } } + return buf.String(), nil } - - // impossible - return "", nil } -// serializeIPInfoToText takes an IPInfo and the original content, then serializes +// serializeIPInfoToText takes an IPInfo, then serializes // the IPInfo to a text format based on the Manager configuration. -func (m *Manager) serializeIPInfoToText(content string, ipInfo *model.IPInfo) (string, error) { +func (m *Manager) serializeIPInfoToText(ipInfo *model.IPInfo) (string, error) { values := strings.Join(util.DeleteEmptyValue(ipInfo.Values()), m.Conf.TextValuesSep) if values != "" { - ret := strings.Replace(m.Conf.TextFormat, "%origin", content, 1) + ret := strings.Replace(m.Conf.TextFormat, "%origin", ipInfo.IP.String(), 1) ret = strings.Replace(ret, "%values", values, 1) return ret, nil } - return content, nil + return ipInfo.IP.String(), nil } -// serializeIPInfoToJSON serializes the provided IPInfo to a JSON format +// serializeDataToJSON serializes the provided DataList to a JSON format // based on the Manager configuration. It returns the JSON string. -func (m *Manager) serializeIPInfoToJSON(ipInfo *model.IPInfo) (string, error) { - values := ipInfo.Output(m.Conf.UseDBFields) - +func (m *Manager) serializeDataToJSON(values *model.DataList) (string, error) { + if len(values.Items) == 0 { + return "", nil + } var ret []byte var err error if m.Conf.JsonIndent { @@ -182,7 +206,7 @@ func (m *Manager) serializeIPInfoToJSON(ipInfo *model.IPInfo) (string, error) { return "", err } - return string(ret) + "\n", nil + return string(ret), nil } // createReader sets up and returns an IP reader based on the specified format and file. diff --git a/pkg/model/output.go b/pkg/model/output.go new file mode 100644 index 0000000..9110861 --- /dev/null +++ b/pkg/model/output.go @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 shenjunzheng@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import ( + "fmt" + "strings" + + "github.com/sjzar/ips/internal/util" +) + +// AlfredItem represents an item to be displayed in Alfred's result list. +type AlfredItem struct { + Title string `json:"title"` + Subtitle string `json:"subtitle"` + Arg string `json:"arg"` + Icon AlfredIcon `json:"icon"` + Valid bool `json:"valid"` + Text AlfredText `json:"text"` +} + +// AlfredIcon represents the icon for an AlfredItem. +type AlfredIcon struct { + Type string `json:"type"` + Path string `json:"path"` +} + +// AlfredText provides additional text information for an AlfredItem. +type AlfredText struct { + Copy string `json:"copy"` +} + +// DataList holds a list of items to be displayed in Alfred's result list. +type DataList struct { + Items []interface{} `json:"items"` +} + +// AddItem appends a new item to the DataList's Items slice. +func (d *DataList) AddItem(item interface{}) { + if d.Items == nil { + d.Items = make([]interface{}, 0) + } + d.Items = append(d.Items, item) +} + +// AddAlfredItemByIPInfo creates an AlfredItem from the provided IPInfo +// and adds it to the DataList. +func (d *DataList) AddAlfredItemByIPInfo(info *IPInfo) { + values := strings.Join(util.DeleteEmptyValue(info.Values()), " ") + item := AlfredItem{ + Title: fmt.Sprintf("%s [%s]", info.IP, values), + Subtitle: "Copy to clipboard", + Arg: values, + Icon: AlfredIcon{}, + Valid: true, + Text: AlfredText{ + Copy: values, + }, + } + d.AddItem(item) +} + +// AddAlfredItemEmpty adds a default "Not found" AlfredItem to the DataList +// if the list is empty. +func (d *DataList) AddAlfredItemEmpty() { + if len(d.Items) > 0 { + return + } + item := AlfredItem{ + Title: "Not found", + Subtitle: "No information found", + Arg: "", + Icon: AlfredIcon{}, + Valid: false, + Text: AlfredText{}, + } + d.AddItem(item) +} diff --git a/script/alfredworkflow/ips.alfredworkflow b/script/alfredworkflow/ips.alfredworkflow new file mode 100644 index 0000000000000000000000000000000000000000..2fd67628c0829ec43e331c31b33efec75846b4c2 GIT binary patch literal 2499 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 kq5#Fw{^ub4I{?r7{s;W5?y#_a|C#Z87n~