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

timecraft: add configuration file #36

Merged
merged 1 commit into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading