diff --git a/go.mod b/go.mod index e43d57c..cdb4b01 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,19 @@ go 1.18 require ( cloud.google.com/go v0.97.0 github.com/alecthomas/kong v0.2.22 + github.com/posener/complete v1.2.3 github.com/stretchr/testify v1.7.0 + github.com/willabides/kongplete v0.3.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) + +replace github.com/willabides/kongplete v0.3.0 => github.com/jotaen/kongplete v0.3.1-0.20220728081706-d7fdeab9b894 diff --git a/go.sum b/go.sum index bcb7071..0b3fda8 100644 --- a/go.sum +++ b/go.sum @@ -147,10 +147,16 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jotaen/kongplete v0.3.1-0.20220728081706-d7fdeab9b894 h1:7iMyO3bat2s1pQP3oeaRf7fAxrd7UijSNpUM0X92g0c= +github.com/jotaen/kongplete v0.3.1-0.20220728081706-d7fdeab9b894/go.mod h1:VPdrG6LY+tP0LMkSBuTgIQ8c6+P8wvIDHVJzDdDh9Fw= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -163,7 +169,11 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab h1:ZjX6I48eZSFetPb41dHudEyVr5v953N15TsNZXlkcWY= +github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab/go.mod h1:/PfPXh0EntGc3QAAyUaviy4S9tzy4Zp0e2ilq4voC6E= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= diff --git a/src/app/cli/bookmarks.go b/src/app/cli/bookmarks.go index c3f8d97..969f791 100644 --- a/src/app/cli/bookmarks.go +++ b/src/app/cli/bookmarks.go @@ -50,7 +50,7 @@ func (opt *BookmarksList) Run(ctx app.Context) error { type BookmarksInfo struct { Dir bool `name:"dir" type:"string" help:"Display the directory"` File bool `name:"file" type:"string" help:"Display the file name"` - Name string `arg:"" name:"bookmark" type:"string" help:"The path of the bookmark"` + Name string `arg:"" name:"bookmark" type:"string" predictor:"bookmark" help:"The path of the bookmark"` } func (opt *BookmarksInfo) Run(ctx app.Context) error { @@ -78,7 +78,7 @@ func (opt *BookmarksInfo) Run(ctx app.Context) error { } type BookmarksSet struct { - File string `arg:"" type:"string" help:".klg source file"` + File string `arg:"" type:"string" predictor:"file" help:".klg source file"` Name string `arg:"" name:"bookmark" type:"string" optional:"1" help:"The name of the bookmark."` Force bool `name:"force" help:"Force to set, even if target file does not exist or is invalid"` lib.QuietArgs @@ -128,7 +128,7 @@ func (opt *BookmarksSet) Run(ctx app.Context) error { type BookmarksUnset struct { // The name is not optional here, to avoid accidental invocations - Name string `arg:"" name:"bookmark" type:"string" help:"The name of the bookmark"` + Name string `arg:"" name:"bookmark" type:"string" predictor:"bookmark" help:"The name of the bookmark"` lib.QuietArgs } diff --git a/src/app/cli/completion.go b/src/app/cli/completion.go new file mode 100644 index 0000000..34bf321 --- /dev/null +++ b/src/app/cli/completion.go @@ -0,0 +1,22 @@ +package cli + +import ( + "github.com/jotaen/klog/src/app" +) + +type Completion struct{} + +func (c *Completion) Help() string { + return "The printed shell code is for instructing your shell to use tab completions for klog. " + + "Place the code into your shell initialization file, e.g. `~/.bashrc`. " + + "You can either paste it verbatim, or you source it dynamically via `. <(klog completion)`." +} + +func (c *Completion) Run(ctx app.Context) error { + completion, err := ctx.Completion() + if err != nil { + return err + } + ctx.Print(completion) + return nil +} diff --git a/src/app/cli/index.go b/src/app/cli/index.go index e62921a..612a6f2 100644 --- a/src/app/cli/index.go +++ b/src/app/cli/index.go @@ -25,9 +25,11 @@ type Cli struct { Bk Bookmarks `cmd:"" group:"Bookmarks" hidden:"" help:"Alias"` // Misc - Edit Edit `cmd:"" group:"Misc" help:"Opens a file or bookmark in your editor"` - Goto Goto `cmd:"" group:"Misc" help:"Opens the file explorer at the given location"` - Json Json `cmd:"" group:"Misc" help:"Converts records to JSON"` - Info Info `cmd:"" group:"Misc" default:"withargs" help:"Displays meta info about klog"` - Version Version `cmd:"" group:"Misc" help:"Prints version info and check for updates"` + Edit Edit `cmd:"" group:"Misc" help:"Opens a file or bookmark in your editor"` + Goto Goto `cmd:"" group:"Misc" help:"Opens the file explorer at the given location"` + Json Json `cmd:"" group:"Misc" help:"Converts records to JSON"` + Info Info `cmd:"" group:"Misc" default:"withargs" help:"Displays meta info about klog"` + Version Version `cmd:"" group:"Misc" help:"Prints version info and check for updates"` + Completion Completion `cmd:"" group:"Misc" help:"Output shell code for initialising shell completions for klog"` + Completions Completion `cmd:"" group:"Misc" hidden:"" help:"Alias"` } diff --git a/src/app/cli/lib/args.go b/src/app/cli/lib/args.go index 4528966..a49ffcd 100644 --- a/src/app/cli/lib/args.go +++ b/src/app/cli/lib/args.go @@ -11,11 +11,11 @@ import ( ) type InputFilesArgs struct { - File []app.FileOrBookmarkName `arg:"" optional:"" type:"string" name:"file or bookmark" help:".klg source file(s) (if empty the bookmark is used)"` + File []app.FileOrBookmarkName `arg:"" optional:"" type:"string" predictor:"file_or_bookmark" name:"file or bookmark" help:".klg source file(s) (if empty the bookmark is used)"` } type OutputFileArgs struct { - File app.FileOrBookmarkName `arg:"" optional:"" type:"string" name:"file or bookmark" help:".klg source file (if empty the bookmark is used)"` + File app.FileOrBookmarkName `arg:"" optional:"" type:"string" predictor:"file_or_bookmark" name:"file or bookmark" help:".klg source file (if empty the bookmark is used)"` } type AtDateArgs struct { @@ -110,24 +110,22 @@ type FilterArgs struct { Today bool `name:"today" group:"Filter (shortcuts)" help:"Records at today’s date"` Yesterday bool `name:"yesterday" group:"Filter (shortcuts)" help:"Records at yesterday’s date"` Tomorrow bool `name:"tomorrow" group:"Filter (shortcuts)" help:"Records at tomorrow’s date"` - ThisXXX bool `name:"this-***" group:"Filter (shortcuts)" help:"Records of the current week/quarter/month/year (e.g. --this-year)"` - LastXXX bool `name:"last-***" group:"Filter (shortcuts)" help:"Records of the previous week/quarter/month/year (e.g. --last-month)"` - ThisWeek bool `name:"this-week" hidden:""` - ThisWeekAlias bool `name:"thisweek" hidden:""` - LastWeek bool `name:"last-week" hidden:""` - LastWeekAlias bool `name:"lastweek" hidden:""` - ThisMonth bool `name:"this-month" hidden:""` - ThisMonthAlias bool `name:"thismonth" hidden:""` - LastMonth bool `name:"last-month" hidden:""` - LastMonthAlias bool `name:"lastmonth" hidden:""` - ThisQuarter bool `name:"this-quarter" hidden:""` - ThisQuarterAlias bool `name:"thisquarter" hidden:""` - LastQuarter bool `name:"last-quarter" hidden:""` - LastQuarterAlias bool `name:"lastquarter" hidden:""` - ThisYear bool `name:"this-year" hidden:""` - ThisYearAlias bool `name:"thisyear" hidden:""` - LastYear bool `name:"last-year" hidden:""` - LastYearAlias bool `name:"lastyear" hidden:""` + ThisWeek bool `name:"this-week" group:"Filter (shortcuts)" help:"Records of the current week"` + ThisWeekAlias bool `name:"thisweek" group:"Filter (shortcuts)" hidden:""` + LastWeek bool `name:"last-week" group:"Filter (shortcuts)" help:"Records of the last week"` + LastWeekAlias bool `name:"lastweek" group:"Filter (shortcuts)" hidden:""` + ThisMonth bool `name:"this-month" group:"Filter (shortcuts)" help:"Records of the current month"` + ThisMonthAlias bool `name:"thismonth" group:"Filter (shortcuts)" hidden:""` + LastMonth bool `name:"last-month" group:"Filter (shortcuts)" help:"Records of the last month"` + LastMonthAlias bool `name:"lastmonth" group:"Filter (shortcuts)" hidden:""` + ThisQuarter bool `name:"this-quarter" group:"Filter (shortcuts)" help:"Records of the current quarter"` + ThisQuarterAlias bool `name:"thisquarter" group:"Filter (shortcuts)" hidden:""` + LastQuarter bool `name:"last-quarter" group:"Filter (shortcuts)" help:"Records of the last quarter"` + LastQuarterAlias bool `name:"lastquarter" group:"Filter (shortcuts)" hidden:""` + ThisYear bool `name:"this-year" group:"Filter (shortcuts)" help:"Records of the current year"` + ThisYearAlias bool `name:"thisyear" group:"Filter (shortcuts)" hidden:""` + LastYear bool `name:"last-year" group:"Filter (shortcuts)" help:"Records of the last year"` + LastYearAlias bool `name:"lastyear" group:"Filter (shortcuts)" hidden:""` } func (args *FilterArgs) ApplyFilter(now gotime.Time, rs []Record) []Record { diff --git a/src/app/cli/lib/commons.go b/src/app/cli/lib/helper.go similarity index 100% rename from src/app/cli/lib/commons.go rename to src/app/cli/lib/helper.go diff --git a/src/app/cli/main/cli.go b/src/app/cli/main/cli.go index a51ad44..d9baae3 100644 --- a/src/app/cli/main/cli.go +++ b/src/app/cli/main/cli.go @@ -11,11 +11,11 @@ import ( "github.com/jotaen/klog/src/app/cli/lib" "github.com/jotaen/klog/src/service" "github.com/jotaen/klog/src/service/period" + "github.com/willabides/kongplete" "reflect" ) func Run(homeDir string, meta app.Meta, isDebug bool, args []string) (int, error) { - ctx := app.NewContext(homeDir, meta, lib.CliSerialiser{}, isDebug) kongApp, nErr := kong.New( &cli.Cli{}, kong.Name("klog"), @@ -66,6 +66,19 @@ func Run(homeDir string, meta app.Meta, isDebug bool, args []string) (int, error if cErr != nil { return -1, cErr } + + completion := func() (string, error) { + return kongplete.GetCompletionFromContext(kongCtx) + } + + ctx := app.NewContext(homeDir, meta, lib.CliSerialiser{}, isDebug, completion) + + // When klog is invoked by shell completion (specifically, when the + // bash-specific COMP_LINE environment variable is set), the + // kongplete.Complete function generates a list of possible completions, + // prints them one per line to stdout, and then exits the program early. + kongplete.Complete(kongApp, kongplete.WithPredictors(CompletionPredictors(ctx))) + kongCtx.BindTo(ctx, (*app.Context)(nil)) rErr := kongCtx.Run() diff --git a/src/app/cli/main/completion_predictors.go b/src/app/cli/main/completion_predictors.go new file mode 100644 index 0000000..38dc41c --- /dev/null +++ b/src/app/cli/main/completion_predictors.go @@ -0,0 +1,29 @@ +package klog + +import ( + "github.com/jotaen/klog/src/app" + "github.com/posener/complete" +) + +func predictBookmarks(ctx app.Context) complete.Predictor { + thunk := func() []string { + names := make([]string, 0) + bookmarksCollection, err := ctx.ReadBookmarks() + if err != nil { + return names + } + for _, bookmark := range bookmarksCollection.All() { + names = append(names, bookmark.Name().ValuePretty()) + } + return names + } + return complete.PredictFunc(func(a complete.Args) []string { return thunk() }) +} + +func CompletionPredictors(ctx app.Context) map[string]complete.Predictor { + return map[string]complete.Predictor{ + "file": complete.PredictFiles("*.klg"), + "bookmark": predictBookmarks(ctx), + "file_or_bookmark": complete.PredictOr(complete.PredictFiles("*.klg"), predictBookmarks(ctx)), + } +} diff --git a/src/app/cli/testutil_test.go b/src/app/cli/testutil_test.go index 241c6e8..6d7ca7f 100644 --- a/src/app/cli/testutil_test.go +++ b/src/app/cli/testutil_test.go @@ -137,4 +137,8 @@ func (ctx *TestingContext) SetSerialiser(s parser.Serialiser) { ctx.serialiser = s } +func (ctx *TestingContext) Completion() (string, app.Error) { + return "", nil +} + func (ctx *TestingContext) Debug(_ func()) {} diff --git a/src/app/context.go b/src/app/context.go index 9d68917..f2f5453 100644 --- a/src/app/context.go +++ b/src/app/context.go @@ -68,6 +68,9 @@ type Context interface { // SetSerialiser sets a new serialiser. SetSerialiser(parser.Serialiser) + // Completion returns the shell initialization code for CLI completions. + Completion() (string, Error) + // Debug takes a void function that is only executed in debug mode. Debug(func()) } @@ -92,7 +95,7 @@ type Meta struct { } // NewContext creates a new Context object. -func NewContext(homeDir string, meta Meta, serialiser parser.Serialiser, isDebug bool) Context { +func NewContext(homeDir string, meta Meta, serialiser parser.Serialiser, isDebug bool, completion func() (string, error)) Context { if meta.Version == "" { meta.Version = "v?.?" } @@ -104,6 +107,7 @@ func NewContext(homeDir string, meta Meta, serialiser parser.Serialiser, isDebug serialiser, meta, isDebug, + completion, } } @@ -112,6 +116,7 @@ type context struct { serialiser parser.Serialiser meta Meta isDebug bool + completion func() (string, error) } func (ctx *context) Print(text string) { @@ -394,6 +399,19 @@ func (ctx *context) SetSerialiser(s parser.Serialiser) { ctx.serialiser = s } +func (ctx *context) Completion() (string, Error) { + c, err := ctx.completion() + if err != nil { + return "", NewErrorWithCode( + GENERAL_ERROR, + "Cannot determine completion initialization", + "The only supported shells are zsh, bash and fish.", + err, + ) + } + return c, nil +} + func (ctx *context) Debug(task func()) { if ctx.isDebug { task()