Skip to content

Commit

Permalink
Support tab completions in the shell
Browse files Browse the repository at this point in the history
* Basic shell completion

* Skeleton for customizing klog command completions

* Custom completion generator that lists *.klg files and klog bookmarks

* Command completions for bookmarks commands

* Switch to patched `kongplete` module

* Add dedicated command and restructure

* Setup hidden alias

* Include more detailed setup instructions

* Rename file

* Naming

Co-authored-by: Steve M. Kim <[email protected]>
  • Loading branch information
jotaen and chairmank authored Aug 17, 2022
1 parent 16c7c63 commit 46141fa
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 30 deletions.
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
6 changes: 3 additions & 3 deletions src/app/cli/bookmarks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
22 changes: 22 additions & 0 deletions src/app/cli/completion.go
Original file line number Diff line number Diff line change
@@ -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
}
12 changes: 7 additions & 5 deletions src/app/cli/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
38 changes: 18 additions & 20 deletions src/app/cli/lib/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
File renamed without changes.
15 changes: 14 additions & 1 deletion src/app/cli/main/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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()
Expand Down
29 changes: 29 additions & 0 deletions src/app/cli/main/completion_predictors.go
Original file line number Diff line number Diff line change
@@ -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)),
}
}
4 changes: 4 additions & 0 deletions src/app/cli/testutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {}
20 changes: 19 additions & 1 deletion src/app/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand All @@ -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?.?"
}
Expand All @@ -104,6 +107,7 @@ func NewContext(homeDir string, meta Meta, serialiser parser.Serialiser, isDebug
serialiser,
meta,
isDebug,
completion,
}
}

Expand All @@ -112,6 +116,7 @@ type context struct {
serialiser parser.Serialiser
meta Meta
isDebug bool
completion func() (string, error)
}

func (ctx *context) Print(text string) {
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 46141fa

Please sign in to comment.