Skip to content

Commit

Permalink
Introduce file-based user configuration
Browse files Browse the repository at this point in the history
This PR addresses #158.

klog now looks for a config file at `~/.klog/config.yml`, where users
are able to specify some preferences. For now, that is:

- `default_rounding`: the value for the `--round` flag, unless
specified, e.g. in `klog start` or `klog stop`.
- `default_should_total`: the default for the `--should-total` flag,
unless specified, that is used whenever klog creates a new record, e.g.
in `klog create`, but also in `klog track` or `klog start`, as these
might create new records on the fly.

More configuration parameters may follow later, now that the groundwork
is set up.

When the config file is absent, everything behaves as before, so it’s
strictly optional right now.

What’s also new is that klog will respect the `$KLOG_FOLDER_LOCATION` or
`$XDG_CONFIG_HOME` environment variables, if set. In this case, **and**
when users have bookmarks, then this will be a breaking change. The fix
is for them to manually move over their `bookmarks.json` file to the new
klog folder location. I’ll take note in the Changelog about this.

The `klog config` command is hidden from the help output for now, but it
can be invoked nevertheless on the nightly build (when compiling klog
from the `main` branch). I’ll do some more testing, refactoring, and
enhancements in the next time, and plan to include this with the next
release – probably within the next few weeks.
  • Loading branch information
jotaen authored Feb 13, 2023
1 parent 52432a9 commit dc2053c
Show file tree
Hide file tree
Showing 24 changed files with 652 additions and 138 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/posener/complete v1.2.3
github.com/stretchr/testify v1.7.2
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand All @@ -18,5 +19,4 @@ require (
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.1 // indirect
)
86 changes: 73 additions & 13 deletions klog.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
_ "embed"
"fmt"
"github.com/jotaen/klog/klog/app"
"github.com/jotaen/klog/klog/app/cli/lib"
"github.com/jotaen/klog/klog/app/cli/main"
"os"
"os/user"
Expand All @@ -24,26 +25,85 @@ func main() {
BinaryBuildHash = BinaryBuildHash[:7]
}

homeDir, err := user.Current()
if err != nil {
fmt.Println("Failed to initialise application. Error:")
fmt.Println(err)
os.Exit(1)
}
klogFolder := func() app.File {
f, err := determineKlogFolderLocation()
if err != nil {
fail(err, false)
}
return app.Join(f, app.KLOG_FOLDER_NAME)
}()

config := app.NewConfig(
app.ConfigFromStaticValues{NumCpus: runtime.NumCPU()},
app.ConfigFromEnvVars{GetVar: os.Getenv},
)
configFile := func() string {
c, err := readConfigFile(app.Join(klogFolder, app.CONFIG_FILE_NAME))
if err != nil {
fail(err, false)
}
return c
}()

exitCode, runErr := klog.Run(homeDir.HomeDir, app.Meta{
config := func() app.Config {
c, err := app.NewConfig(
app.FromStaticValues{NumCpus: runtime.NumCPU()},
app.FromEnvVars{GetVar: os.Getenv},
app.FromConfigFile{FileContents: configFile},
)
if err != nil {
fail(err, false)
}
return c
}()

err := klog.Run(klogFolder, app.Meta{
Specification: specification,
License: license,
Version: BinaryVersion,
SrcHash: BinaryBuildHash,
}, config, os.Args[1:])
if runErr != nil {
fmt.Println(runErr)
if err != nil {
fail(err, config.IsDebug.Value())
}
}

// fail terminates the process with an error.
func fail(e error, isDebug bool) {
exitCode := -1
if e != nil {
fmt.Println(lib.PrettifyError(e, isDebug))
if appErr, isAppError := e.(app.Error); isAppError {
exitCode = appErr.Code().ToInt()
}
}
os.Exit(exitCode)
}

// readConfigFile reads the config file from disk, if present.
// If not present, it returns empty string.
func readConfigFile(location app.File) (string, error) {
contents, rErr := app.ReadFile(location)
if rErr != nil {
if rErr.Code() == app.NO_SUCH_FILE {
return "", nil
}
return "", rErr
}
return contents, nil
}

// determineKlogFolderLocation returns the location where the `.klog` folder should be place.
// This is determined by following this lookup precedence:
// - $KLOG_FOLDER_LOCATION, if set
// - $XDG_CONFIG_HOME, if set
// - The default home folder, e.g. `~`
func determineKlogFolderLocation() (app.File, error) {
location := os.Getenv("KLOG_FOLDER_LOCATION")
if os.Getenv("XDG_CONFIG_HOME") != "" {
location = os.Getenv("XDG_CONFIG_HOME")
} else if location == "" {
homeDir, hErr := user.Current()
if hErr != nil {
return nil, hErr
}
location = homeDir.HomeDir
}
return app.NewFile(location)
}
36 changes: 36 additions & 0 deletions klog/app/cli/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cli

import (
"github.com/jotaen/klog/klog/app"
"github.com/jotaen/klog/klog/app/cli/lib"
"github.com/jotaen/klog/klog/app/cli/lib/terminalformat"
)

type Config struct {
ConfigFilePath bool `name:"file-path" help:"Prints the path to your config file"`
}

func (opt *Config) Help() string {
return `You are able to configure some of klog’s behaviour via a YAML file in your ` + "`" + app.KLOG_FOLDER_NAME + "`" + ` folder. (Run ` + "`" + `klog config --file-path` + "`" + ` to print the exact location.)
If you run ` + "`" + `klog config` + "`" + `, you can learn about the supported YAML properties in the file, and you also see what values are in effect at the moment.
Note: the output of the command does not print the actual file. You may, however, use the output as template for setting up the file, as its YAML-formatted.`
}

func (opt *Config) Run(ctx app.Context) app.Error {
if opt.ConfigFilePath {
ctx.Print(app.Join(ctx.KlogFolder(), app.CONFIG_FILE_NAME).Path() + "\n")
return nil
}
for i, e := range app.CONFIG_FILE_ENTRIES {
ctx.Print(lib.Subdued.Format(lib.Reflower.Reflow(e.Description+"\n"+e.Instructions, "# ")))
ctx.Print("\n")
ctx.Print(lib.Red.Format(e.Name) + `: ` + terminalformat.Style{Color: "227"}.Format(e.Value(ctx.Config())))
if i < len(app.CONFIG_FILE_ENTRIES)-1 {
ctx.Print("\n\n")
}
}
ctx.Print("\n")
return nil
}
11 changes: 7 additions & 4 deletions klog/app/cli/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ func (opt *Create) Run(ctx app.Context) app.Error {
opt.NoStyleArgs.Apply(&ctx)
date, isAutoDate := opt.AtDate(ctx.Now())
atDate := reconciling.NewStyled[klog.Date](date, isAutoDate)
additionalData := reconciling.AdditionalData{ShouldTotal: opt.ShouldTotal, Summary: opt.Summary}
if additionalData.ShouldTotal == nil {
ctx.Config().DefaultShouldTotal.Map(func(s klog.ShouldTotal) {
additionalData.ShouldTotal = s
})
}
return lib.Reconcile(ctx, lib.ReconcileOpts{OutputFileArgs: opt.OutputFileArgs, WarnArgs: opt.WarnArgs},
[]reconciling.Creator{
reconciling.NewReconcilerForNewRecord(
atDate,
reconciling.AdditionalData{ShouldTotal: opt.ShouldTotal, Summary: opt.Summary},
),
reconciling.NewReconcilerForNewRecord(atDate, additionalData),
},

func(reconciler *reconciling.Reconciler) (*reconciling.Result, error) {
Expand Down
50 changes: 50 additions & 0 deletions klog/app/cli/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,53 @@ This is a
new record!
`, state.writtenFileContents)
}

func TestCreateWithFileConfig(t *testing.T) {
// With should-total from config file
{
state, err := NewTestingContext()._SetRecords(`
1920-02-01
4h33m
1920-02-02
9:00-12:00
`)._SetFileConfig(`
default_should_total: 30m!
`)._SetNow(1920, 2, 3, 15, 24)._Run((&Create{}).Run)
require.Nil(t, err)
assert.Equal(t, `
1920-02-01
4h33m
1920-02-02
9:00-12:00
1920-02-03 (30m!)
`, state.writtenFileContents)
}

// --should-total flag trumps should-total from config file
{
state, err := NewTestingContext()._SetRecords(`
1920-02-01
4h33m
1920-02-02
9:00-12:00
`)._SetFileConfig(`
default_should_total: 30m!
`)._SetNow(1920, 2, 3, 15, 24)._Run((&Create{
ShouldTotal: klog.NewShouldTotal(5, 55),
}).Run)
require.Nil(t, err)
assert.Equal(t, `
1920-02-01
4h33m
1920-02-02
9:00-12:00
1920-02-03 (5h55m!)
`, state.writtenFileContents)
}
}
1 change: 1 addition & 0 deletions klog/app/cli/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Cli struct {
// 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"`
Config Config `cmd:"" group:"Misc" hidden:"" help:"Prints the current configuration"` // Still experimental / WIP
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"`
Expand Down
10 changes: 7 additions & 3 deletions klog/app/cli/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ const DESCRIPTION = "klog: command line app for time tracking with plain-text fi
"Documentation online at " + KLOG_WEBSITE_URL

type Info struct {
Version bool `short:"v" name:"version" help:"Alias for 'klog version'"`
Spec bool `name:"spec" help:"Print file format specification"`
License bool `name:"license" help:"Print license"`
Version bool `short:"v" name:"version" help:"Alias for 'klog version'"`
Spec bool `name:"spec" help:"Print file format specification"`
License bool `name:"license" help:"Print license"`
KlogFolder bool `name:"klog-folder" help:"Print path of .klog folder"`
}

func (opt *Info) Help() string {
Expand All @@ -28,6 +29,9 @@ func (opt *Info) Run(ctx app.Context) app.Error {
} else if opt.License {
ctx.Print(ctx.Meta().License + "\n")
return nil
} else if opt.KlogFolder {
ctx.Print(ctx.KlogFolder().Path() + "\n")
return nil
}
ctx.Print(DESCRIPTION + "\n")
return nil
Expand Down
6 changes: 5 additions & 1 deletion klog/app/cli/lib/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type AtDateAndTimeArgs struct {
Round service.Rounding `name:"round" short:"r" help:"Round time to nearest multiple of 5m, 10m, 15m, 30m, or 60m / 1h"`
}

func (args *AtDateAndTimeArgs) AtTime(now gotime.Time) (klog.Time, bool, app.Error) {
func (args *AtDateAndTimeArgs) AtTime(now gotime.Time, config app.Config) (klog.Time, bool, app.Error) {
if args.Time != nil {
return args.Time, false, nil
}
Expand All @@ -52,6 +52,10 @@ func (args *AtDateAndTimeArgs) AtTime(now gotime.Time) (klog.Time, bool, app.Err
time := klog.NewTimeFromGo(now)
if args.Round != nil {
time = service.RoundToNearest(time, args.Round)
} else {
config.DefaultRounding.Map(func(r service.Rounding) {
time = service.RoundToNearest(time, r)
})
}
if today.IsEqualTo(date) {
return time, true, nil
Expand Down
7 changes: 4 additions & 3 deletions klog/app/cli/lib/prettifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import (
"strings"
)

var Reflower = terminalformat.NewReflower(60, "\n")

// PrettifyError turns an error into a coloured and well-structured form.
func PrettifyError(err error, isDebug bool) error {
reflower := terminalformat.NewReflower(60, "\n")
switch e := err.(type) {
case app.ParserErrors:
message := ""
Expand All @@ -34,13 +35,13 @@ func PrettifyError(err error, isDebug bool) error {
) + "\n"
message += fmt.Sprintf(
terminalformat.Style{Color: "227"}.Format("%s"),
reflower.Reflow(e.Message(), INDENT),
Reflower.Reflow(e.Message(), INDENT),
) + "\n\n"
}
return errors.New(message)
case app.Error:
message := "Error: " + e.Error() + "\n"
message += reflower.Reflow(e.Details(), "")
message += Reflower.Reflow(e.Details(), "")
if isDebug && e.Original() != nil {
message += "\n\nOriginal Error:\n" + e.Original().Error()
}
Expand Down
17 changes: 4 additions & 13 deletions klog/app/cli/main/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"reflect"
)

func Run(homeDir string, meta app.Meta, config app.Config, args []string) (int, error) {
func Run(homeDir app.File, meta app.Meta, config app.Config, args []string) error {
kongApp, nErr := kong.New(
&cli.Cli{},
kong.Name("klog"),
Expand Down Expand Up @@ -59,7 +59,7 @@ func Run(homeDir string, meta app.Meta, config app.Config, args []string) (int,
}),
)
if nErr != nil {
return -1, nErr
return nErr
}

ctx := app.NewContext(
Expand All @@ -76,18 +76,9 @@ func Run(homeDir string, meta app.Meta, config app.Config, args []string) (int,
kongcompletion.Register(kongApp, kongcompletion.WithPredictors(CompletionPredictors(ctx)))
kongCtx, cErr := kongApp.Parse(args)
if cErr != nil {
return -1, cErr
return cErr
}
kongCtx.BindTo(ctx, (*app.Context)(nil))

rErr := kongCtx.Run()
if rErr != nil {
ctx.Print(lib.PrettifyError(rErr, config.IsDebug.Value()).Error() + "\n")
if appErr, isAppError := rErr.(app.Error); isAppError {
return int(appErr.Code()), nil
} else {
return -1, rErr
}
}
return 0, nil
return kongCtx.Run()
}
4 changes: 2 additions & 2 deletions klog/app/cli/main/testutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ func (e *Env) run(invocation ...[]string) []string {
r, w, _ := os.Pipe()
os.Stdout = w

code, runErr := Run(tmpDir, app.Meta{
runErr := Run(app.NewFileOrPanic(tmpDir), app.Meta{
Specification: "[Specification text]",
License: "[License text]",
Version: "v0.0",
SrcHash: "abc1234",
}, app.NewDefaultConfig(), args)

_ = w.Close()
if runErr != nil || code != 0 {
if runErr != nil {
outs[i] = runErr.Error()
continue
}
Expand Down
8 changes: 6 additions & 2 deletions klog/app/cli/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,20 @@ func (opt *Start) Run(ctx app.Context) app.Error {
opt.NoStyleArgs.Apply(&ctx)
now := ctx.Now()
date, isAutoDate := opt.AtDate(now)
time, isAutoTime, err := opt.AtTime(now)
time, isAutoTime, err := opt.AtTime(now, ctx.Config())
if err != nil {
return err
}
atDate := reconciling.NewStyled[klog.Date](date, isAutoDate)
startTime := reconciling.NewStyled[klog.Time](time, isAutoTime)
additionalData := reconciling.AdditionalData{}
ctx.Config().DefaultShouldTotal.Map(func(s klog.ShouldTotal) {
additionalData.ShouldTotal = s
})
return lib.Reconcile(ctx, lib.ReconcileOpts{OutputFileArgs: opt.OutputFileArgs, WarnArgs: opt.WarnArgs},
[]reconciling.Creator{
reconciling.NewReconcilerAtRecord(atDate.Value),
reconciling.NewReconcilerForNewRecord(atDate, reconciling.AdditionalData{}),
reconciling.NewReconcilerForNewRecord(atDate, additionalData),
},

func(reconciler *reconciling.Reconciler) (*reconciling.Result, error) {
Expand Down
Loading

0 comments on commit dc2053c

Please sign in to comment.