Skip to content
This repository has been archived by the owner on Feb 17, 2024. It is now read-only.

Commit

Permalink
Merge pull request #36 from stealthrocket/timecraft-config-file
Browse files Browse the repository at this point in the history
timecraft: add configuration file
  • Loading branch information
achille-roussel committed May 31, 2023
2 parents 6569b3a + f1cec39 commit 00f0ae1
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 97 deletions.
227 changes: 227 additions & 0 deletions internal/cmd/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package cmd

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"

"github.com/stealthrocket/timecraft/internal/object"
"github.com/stealthrocket/timecraft/internal/print/human"
"github.com/stealthrocket/timecraft/internal/timemachine"
"gopkg.in/yaml.v3"
)

const configUsage = `
Usage: timecraft config [options]
Options:
-c, --config Path to the timecraft configuration file (overrides TIMECRAFTCONFIG)
--edit Open $EDITOR to edit the configuration
-h, --help Show usage information
-o, --ouptut format Output format, one of: text, json, yaml
`

var (
// Path to the timecraft configuration; this is set to the value of the
// TIMECRAFTCONFIG environment variable if it exists, and can be overwritten
// by the --config option.
configPath human.Path = "~/.timecraft/config.yaml"
)

func init() {
if v := os.Getenv("TIMECRAFTCONFIG"); v != "" {
configPath = human.Path(v)
}
}

func config(ctx context.Context, args []string) error {
var (
edit bool
output = outputFormat("text")
)

flagSet := newFlagSet("timecraft config", configUsage)
boolVar(flagSet, &edit, "edit")
customVar(flagSet, &output, "o", "output")
parseFlags(flagSet, args)

r, path, err := openConfig()
if err != nil {
return err
}
defer r.Close()

if edit {
editor := os.Getenv("EDITOR")
if editor == "" {
return errors.New(`$EDITOR is not set`)
}
shell := os.Getenv("SHELL")
if shell == "" {
shell = "/bin/sh"
}

if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
if !errors.Is(err, fs.ErrExist) {
return err
}
}

tmp, err := createTempFile(path, r)
if err != nil {
return err
}
defer os.Remove(tmp)

p, err := os.StartProcess(shell, []string{shell, "-c", editor + " " + tmp}, &os.ProcAttr{
Files: []*os.File{
0: os.Stdin,
1: os.Stdout,
2: os.Stderr,
},
})
if err != nil {
return err
}
if _, err := p.Wait(); err != nil {
return err
}
f, err := os.Open(tmp)
if err != nil {
return err
}
defer f.Close()
if _, err := readConfig(f); err != nil {
return fmt.Errorf("not applying configuration updates because the file has a syntax error: %w", err)
}
if err := os.Rename(tmp, path); err != nil {
return err
}
}

config, err := loadConfig()
if err != nil {
return err
}

w := io.Writer(os.Stdout)
for {
switch output {
case "json":
e := json.NewEncoder(w)
e.SetEscapeHTML(false)
e.SetIndent("", " ")
_ = e.Encode(config)
case "yaml":
e := yaml.NewEncoder(w)
e.SetIndent(2)
_ = e.Encode(config)
_ = e.Close()
default:
r, _, err := openConfig()
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
output = "yaml"
continue
}
return err
}
defer r.Close()
_, _ = io.Copy(w, r)
}
return nil
}
}

type configuration struct {
Registry struct {
Location string `json:"location"`
} `json:"registry"`
}

func defaultConfig() *configuration {
c := new(configuration)
c.Registry.Location = "~/.timecraft"
return c
}

func openConfig() (io.ReadCloser, string, error) {
path, err := configPath.Resolve()
if err != nil {
return nil, path, err
}
f, err := os.Open(path)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return nil, path, err
}
c := defaultConfig()
b, _ := yaml.Marshal(c)
return io.NopCloser(bytes.NewReader(b)), path, nil
}
return f, path, nil
}

func loadConfig() (*configuration, error) {
r, _, err := openConfig()
if err != nil {
return nil, err
}
defer r.Close()
return readConfig(r)
}

func readConfig(r io.Reader) (*configuration, error) {
c := defaultConfig()
d := yaml.NewDecoder(r)
d.KnownFields(true)
if err := d.Decode(c); err != nil {
return nil, err
}
return c, nil
}

func (c *configuration) createRegistry() (*timemachine.Registry, error) {
p, err := human.Path(c.Registry.Location).Resolve()
if err != nil {
return nil, err
}
if err := os.Mkdir(filepath.Dir(p), 0777); err != nil {
if !errors.Is(err, fs.ErrExist) {
return nil, err
}
}
return c.openRegistry()
}

func (c *configuration) openRegistry() (*timemachine.Registry, error) {
p, err := human.Path(c.Registry.Location).Resolve()
if err != nil {
return nil, err
}
store, err := object.DirStore(p)
if err != nil {
return nil, err
}
registry := &timemachine.Registry{
Store: store,
}
return registry, nil
}

func createTempFile(path string, r io.Reader) (string, error) {
dir, file := filepath.Split(path)
w, err := os.CreateTemp(dir, "."+file+".*")
if err != nil {
return "", err
}
defer w.Close()
_, err = io.Copy(w, r)
return w.Name(), err
}
14 changes: 7 additions & 7 deletions internal/cmd/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,18 @@ Examples:
0 27 1 1.68 KiB 3.88 KiB 1.62 KiB 58.27%
Options:
-c, --config Path to the timecraft configuration file (overrides TIMECRAFTCONFIG)
-h, --help Show this usage information
-o, --ouptut format Output format, one of: text, json, yaml
-r, --registry path Path to the timecraft registry (default to ~/.timecraft)
`

func describe(ctx context.Context, args []string) error {
var (
output = outputFormat("text")
registryPath = human.Path("~/.timecraft")
output = outputFormat("text")
)

flagSet := newFlagSet("timecraft describe", describeUsage)
customVar(flagSet, &output, "o", "output")
customVar(flagSet, &registryPath, "r", "registry")
args = parseFlags(flagSet, args)

if len(args) == 0 {
Expand All @@ -71,13 +69,15 @@ func describe(ctx context.Context, args []string) error {
if err != nil {
return err
}

resourceIDs := args[1:]
if len(resourceIDs) == 0 {
return fmt.Errorf(`no resources were specified, use 'timecraft describe %s <resources ids...>'`, resource.typ)
}

registry, err := openRegistry(registryPath)
config, err := loadConfig()
if err != nil {
return err
}
registry, err := config.openRegistry()
if err != nil {
return err
}
Expand Down
17 changes: 7 additions & 10 deletions internal/cmd/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

"github.com/google/uuid"
"github.com/stealthrocket/timecraft/format"
"github.com/stealthrocket/timecraft/internal/print/human"
)

const exportUsage = `
Expand All @@ -23,17 +22,12 @@ Usage: timecraft export <resource type> <resource id> <output file>
resource to stdout.
Options:
-h, --help Show this usage information
-r, --registry path Path to the timecraft registry (default to ~/.timecraft)
-c, --config Path to the timecraft configuration file (overrides TIMECRAFTCONFIG)
-h, --help Show this usage information
`

func export(ctx context.Context, args []string) error {
var (
registryPath = human.Path("~/.timecraft")
)

flagSet := newFlagSet("timecraft export", exportUsage)
customVar(flagSet, &registryPath, "r", "registry")
args = parseFlags(flagSet, args)

if len(args) != 3 {
Expand All @@ -44,8 +38,11 @@ func export(ctx context.Context, args []string) error {
if err != nil {
return err
}

registry, err := openRegistry(registryPath)
config, err := loadConfig()
if err != nil {
return err
}
registry, err := config.openRegistry()
if err != nil {
return err
}
Expand Down
15 changes: 8 additions & 7 deletions internal/cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ Examples:
}
Options:
-c, --config Path to the timecraft configuration file (overrides TIMECRAFTCONFIG)
-h, --help Show this usage information
-o, --ouptut format Output format, one of: text, json, yaml
-r, --registry path Path to the timecraft registry (default to ~/.timecraft)
`

type resource struct {
Expand Down Expand Up @@ -116,14 +116,12 @@ var resources = [...]resource{

func get(ctx context.Context, args []string) error {
var (
timeRange = timemachine.Since(time.Unix(0, 0))
output = outputFormat("text")
registryPath = human.Path("~/.timecraft")
timeRange = timemachine.Since(time.Unix(0, 0))
output = outputFormat("text")
)

flagSet := newFlagSet("timecraft get", getUsage)
customVar(flagSet, &output, "o", "output")
customVar(flagSet, &registryPath, "r", "registry")
args = parseFlags(flagSet, args)

if len(args) != 1 {
Expand All @@ -133,8 +131,11 @@ func get(ctx context.Context, args []string) error {
if err != nil {
return err
}

registry, err := openRegistry(registryPath)
config, err := loadConfig()
if err != nil {
return err
}
registry, err := config.openRegistry()
if err != nil {
return err
}
Expand Down
7 changes: 7 additions & 0 deletions internal/cmd/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ Debugging Commands:
profile Generate performance profile from execution records
Other Commands:
config View or edit the timecraft configuration
help Show usage information about timecraft commands
version Show the timecraft version information
Global Options:
-c, --config Path to the timecraft configuration file (overrides TIMECRAFTCONFIG)
-h, --help Show usage information
For a description of each command, run 'timecraft help <command>'.`

func help(ctx context.Context, args []string) error {
Expand All @@ -41,6 +46,8 @@ func help(ctx context.Context, args []string) error {
}

switch cmd {
case "config":
msg = configUsage
case "describe":
msg = describeUsage
case "export":
Expand Down
Loading

0 comments on commit 00f0ae1

Please sign in to comment.