Add a global configuration file (#12)

pull/14/head
Mickaël Menu 3 years ago committed by GitHub
parent 925e5337bc
commit b82f1f547c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,7 +2,16 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
<!-- ## Unreleased --> ## Unreleased
### Added
* Global `zk` configuration at `~/.config/zk/config.toml`.
* Useful to share aliases or default settings across several [notebooks](docs/notebook.md).
* This is the same format as a notebook [configuration file](docs/config.md).
* Shared templates can be stored in `~/.config/zk/templates/`.
* `XDG_CONFIG_HOME` is taken into account.
## 0.2.1 ## 0.2.1

@ -3,7 +3,7 @@ package cmd
import ( import (
"io" "io"
"os" "os"
"sync" "path/filepath"
"time" "time"
"github.com/mickael-menu/zk/adapter/fzf" "github.com/mickael-menu/zk/adapter/fzf"
@ -22,35 +22,79 @@ import (
) )
type Container struct { type Container struct {
Config zk.Config
Date date.Provider Date date.Provider
Logger util.Logger Logger util.Logger
Terminal *term.Terminal Terminal *term.Terminal
templateLoader *handlebars.Loader templateLoader *handlebars.Loader
zk *zk.Zk
zkOnce sync.Once zkErr error
zk *zk.Zk
zkErr error
} }
func NewContainer() *Container { func NewContainer() (*Container, error) {
wrap := errors.Wrapper("initialization")
config := zk.NewDefaultConfig()
// Load global user config
configPath, err := locateGlobalConfig()
if err != nil {
return nil, wrap(err)
}
if configPath != "" {
config, err = zk.OpenConfig(configPath, config)
if err != nil {
return nil, wrap(err)
}
}
// Open current notebook
zk, zkErr := zk.Open(".", config)
if zkErr == nil {
config = zk.Config
os.Setenv("ZK_PATH", zk.Path)
}
date := date.NewFrozenNow() date := date.NewFrozenNow()
return &Container{ return &Container{
Config: config,
Logger: util.NewStdLogger("zk: ", 0), Logger: util.NewStdLogger("zk: ", 0),
// zk is short-lived, so we freeze the current date to use the same // zk is short-lived, so we freeze the current date to use the same
// date for any rendering during the execution. // date for any rendering during the execution.
Date: &date, Date: &date,
Terminal: term.New(), Terminal: term.New(),
} zk: zk,
zkErr: zkErr,
}, nil
} }
func (c *Container) OpenZk() (*zk.Zk, error) { // locateGlobalConfig looks for the global zk config file following the
c.zkOnce.Do(func() { // XDG Base Directory specification
c.zk, c.zkErr = zk.Open(".") // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
if c.zkErr == nil { func locateGlobalConfig() (string, error) {
os.Setenv("ZK_PATH", c.zk.Path) configHome, ok := os.LookupEnv("XDG_CONFIG_HOME")
if !ok {
home, ok := os.LookupEnv("HOME")
if !ok {
home = "~/"
} }
}) configHome = filepath.Join(home, ".config")
}
configPath := filepath.Join(configHome, "zk/config.toml")
exists, err := paths.Exists(configPath)
switch {
case err != nil:
return "", err
case exists:
return configPath, nil
default:
return "", nil
}
}
func (c *Container) Zk() (*zk.Zk, error) {
return c.zk, c.zkErr return c.zk, c.zkErr
} }

@ -21,7 +21,7 @@ type Edit struct {
} }
func (cmd *Edit) Run(container *Container) error { func (cmd *Edit) Run(container *Container) error {
zk, err := container.OpenZk() zk, err := container.Zk()
if err != nil { if err != nil {
return err return err
} }

@ -15,7 +15,7 @@ func (cmd *Index) Help() string {
} }
func (cmd *Index) Run(container *Container) error { func (cmd *Index) Run(container *Container) error {
zk, err := container.OpenZk() zk, err := container.Zk()
if err != nil { if err != nil {
return err return err
} }

@ -29,7 +29,7 @@ func (cmd *List) Run(container *Container) error {
cmd.Delimiter = "\x00" cmd.Delimiter = "\x00"
} }
zk, err := container.OpenZk() zk, err := container.Zk()
if err != nil { if err != nil {
return err return err
} }

@ -30,7 +30,7 @@ func (cmd *New) ConfigOverrides() zk.ConfigOverrides {
} }
func (cmd *New) Run(container *Container) error { func (cmd *New) Run(container *Container) error {
zk, err := container.OpenZk() zk, err := container.Zk()
if err != nil { if err != nil {
return err return err
} }
@ -46,6 +46,7 @@ func (cmd *New) Run(container *Container) error {
} }
opts := note.CreateOpts{ opts := note.CreateOpts{
Config: zk.Config,
Dir: *dir, Dir: *dir,
Title: opt.NewNotEmptyString(cmd.Title), Title: opt.NewNotEmptyString(cmd.Title),
Content: content, Content: content,

@ -17,6 +17,8 @@ import (
// CreateOpts holds the options to create a new note. // CreateOpts holds the options to create a new note.
type CreateOpts struct { type CreateOpts struct {
// Current configuration.
Config zk.Config
// Parent directory for the new note. // Parent directory for the new note.
Dir zk.Dir Dir zk.Dir
// Title of the note. // Title of the note.
@ -52,7 +54,11 @@ func Create(
var bodyTemplate templ.Renderer = templ.NullRenderer var bodyTemplate templ.Renderer = templ.NullRenderer
if templatePath := opts.Dir.Config.Note.BodyTemplatePath.Unwrap(); templatePath != "" { if templatePath := opts.Dir.Config.Note.BodyTemplatePath.Unwrap(); templatePath != "" {
bodyTemplate, err = templateLoader.LoadFile(templatePath) absPath, ok := opts.Config.LocateTemplate(templatePath)
if !ok {
return "", wrap(fmt.Errorf("%s: cannot find template", templatePath))
}
bodyTemplate, err = templateLoader.LoadFile(absPath)
if err != nil { if err != nil {
return "", wrap(err) return "", wrap(err)
} }

@ -1,10 +1,12 @@
package zk package zk
import ( import (
"io/ioutil"
"path/filepath" "path/filepath"
"github.com/mickael-menu/zk/util/errors" "github.com/mickael-menu/zk/util/errors"
"github.com/mickael-menu/zk/util/opt" "github.com/mickael-menu/zk/util/opt"
"github.com/mickael-menu/zk/util/paths"
toml "github.com/pelletier/go-toml" toml "github.com/pelletier/go-toml"
) )
@ -16,6 +18,37 @@ type Config struct {
Tool ToolConfig Tool ToolConfig
Aliases map[string]string Aliases map[string]string
Extra map[string]string Extra map[string]string
// Base directories for the relative template paths used in NoteConfig.
TemplatesDirs []string
}
// NewDefaultConfig creates a new Config with the default settings.
func NewDefaultConfig() Config {
return Config{
Note: NoteConfig{
FilenameTemplate: "{{id}}",
Extension: "md",
BodyTemplatePath: opt.NullString,
Lang: "en",
DefaultTitle: "Untitled",
IDOptions: IDOptions{
Charset: CharsetAlphanum,
Length: 4,
Case: CaseLower,
},
},
Groups: map[string]GroupConfig{},
Format: FormatConfig{
Markdown: MarkdownConfig{
Hashtags: true,
ColonTags: false,
MultiwordTags: false,
},
},
Aliases: map[string]string{},
Extra: map[string]string{},
TemplatesDirs: []string{},
}
} }
// RootGroupConfig returns the default GroupConfig for the root directory and its descendants. // RootGroupConfig returns the default GroupConfig for the root directory and its descendants.
@ -27,6 +60,31 @@ func (c Config) RootGroupConfig() GroupConfig {
} }
} }
// LocateTemplate returns the absolute path for the given template path, by
// looking for it in the templates directories registered in this Config.
func (c Config) LocateTemplate(path string) (string, bool) {
if path == "" {
return "", false
}
exists := func(path string) bool {
exists, err := paths.Exists(path)
return exists && err == nil
}
if filepath.IsAbs(path) {
return path, exists(path)
}
for _, dir := range c.TemplatesDirs {
if candidate := filepath.Join(dir, path); exists(candidate) {
return candidate, true
}
}
return path, false
}
// FormatConfig holds the configuration for document formats, such as Markdown. // FormatConfig holds the configuration for document formats, such as Markdown.
type FormatConfig struct { type FormatConfig struct {
Markdown MarkdownConfig Markdown MarkdownConfig
@ -35,11 +93,11 @@ type FormatConfig struct {
// MarkdownConfig holds the configuration for Markdown documents. // MarkdownConfig holds the configuration for Markdown documents.
type MarkdownConfig struct { type MarkdownConfig struct {
// Hashtags indicates whether #hashtags are supported. // Hashtags indicates whether #hashtags are supported.
Hashtags bool `toml:"hashtags" default:"true"` Hashtags bool
// ColonTags indicates whether :colon:tags: are supported. // ColonTags indicates whether :colon:tags: are supported.
ColonTags bool `toml:"colon-tags" default:"false"` ColonTags bool
// MultiwordTags indicates whether #multi-word tags# are supported. // MultiwordTags indicates whether #multi-word tags# are supported.
MultiwordTags bool `toml:"multiword-tags" default:"false"` MultiwordTags bool
} }
// ToolConfig holds the external tooling configuration. // ToolConfig holds the external tooling configuration.
@ -107,90 +165,110 @@ func (c *GroupConfig) Override(overrides ConfigOverrides) {
} }
} }
// OpenConfig creates a new Config instance from its TOML representation stored
// in the given file.
func OpenConfig(path string, parentConfig Config) (Config, error) {
content, err := ioutil.ReadFile(path)
if err != nil {
return parentConfig, errors.Wrapf(err, "failed to open config file at %s", path)
}
return ParseConfig(content, path, parentConfig)
}
// ParseConfig creates a new Config instance from its TOML representation. // ParseConfig creates a new Config instance from its TOML representation.
// templatesDir is the base path for the relative templates. // path is the config absolute path, from which will be derived the base path
func ParseConfig(content []byte, templatesDir string) (*Config, error) { // for templates.
//
// The parentConfig will be used to inherit default config settings.
func ParseConfig(content []byte, path string, parentConfig Config) (Config, error) {
config := parentConfig
var tomlConf tomlConfig var tomlConf tomlConfig
err := toml.Unmarshal(content, &tomlConf) err := toml.Unmarshal(content, &tomlConf)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to read config") return config, errors.Wrap(err, "failed to read config")
}
root := GroupConfig{
Paths: []string{},
Note: NoteConfig{
FilenameTemplate: "{{id}}",
Extension: "md",
BodyTemplatePath: opt.NullString,
Lang: "en",
DefaultTitle: "Untitled",
IDOptions: IDOptions{
Charset: CharsetAlphanum,
Length: 4,
Case: CaseLower,
},
},
Extra: make(map[string]string),
} }
// Note
note := tomlConf.Note note := tomlConf.Note
if note.Filename != "" { if note.Filename != "" {
root.Note.FilenameTemplate = note.Filename config.Note.FilenameTemplate = note.Filename
} }
if note.Extension != "" { if note.Extension != "" {
root.Note.Extension = note.Extension config.Note.Extension = note.Extension
} }
if note.Template != "" { if note.Template != "" {
root.Note.BodyTemplatePath = templatePathFromString(note.Template, templatesDir) config.Note.BodyTemplatePath = opt.NewNotEmptyString(note.Template)
} }
if note.IDLength != 0 { if note.IDLength != 0 {
root.Note.IDOptions.Length = note.IDLength config.Note.IDOptions.Length = note.IDLength
} }
if note.IDCharset != "" { if note.IDCharset != "" {
root.Note.IDOptions.Charset = charsetFromString(note.IDCharset) config.Note.IDOptions.Charset = charsetFromString(note.IDCharset)
} }
if note.IDCase != "" { if note.IDCase != "" {
root.Note.IDOptions.Case = caseFromString(note.IDCase) config.Note.IDOptions.Case = caseFromString(note.IDCase)
} }
if note.Lang != "" { if note.Lang != "" {
root.Note.Lang = note.Lang config.Note.Lang = note.Lang
} }
if note.DefaultTitle != "" { if note.DefaultTitle != "" {
root.Note.DefaultTitle = note.DefaultTitle config.Note.DefaultTitle = note.DefaultTitle
} }
if tomlConf.Extra != nil { if tomlConf.Extra != nil {
for k, v := range tomlConf.Extra { for k, v := range tomlConf.Extra {
root.Extra[k] = v config.Extra[k] = v
} }
} }
groups := make(map[string]GroupConfig) // Groups
for name, dirTOML := range tomlConf.Groups { for name, dirTOML := range tomlConf.Groups {
groups[name] = root.merge(dirTOML, name, templatesDir) parent, ok := config.Groups[name]
if !ok {
parent = config.RootGroupConfig()
}
config.Groups[name] = parent.merge(dirTOML, name)
} }
aliases := make(map[string]string) // Format
markdown := tomlConf.Format.Markdown
if markdown.Hashtags != nil {
config.Format.Markdown.Hashtags = *markdown.Hashtags
}
if markdown.ColonTags != nil {
config.Format.Markdown.ColonTags = *markdown.ColonTags
}
if markdown.MultiwordTags != nil {
config.Format.Markdown.MultiwordTags = *markdown.MultiwordTags
}
// Tool
tool := tomlConf.Tool
if tool.Editor != nil {
config.Tool.Editor = opt.NewNotEmptyString(*tool.Editor)
}
if tool.Pager != nil {
config.Tool.Pager = opt.NewStringWithPtr(tool.Pager)
}
if tool.FzfPreview != nil {
config.Tool.FzfPreview = opt.NewStringWithPtr(tool.FzfPreview)
}
// Aliases
if tomlConf.Aliases != nil { if tomlConf.Aliases != nil {
for k, v := range tomlConf.Aliases { for k, v := range tomlConf.Aliases {
aliases[k] = v config.Aliases[k] = v
} }
} }
return &Config{ config.TemplatesDirs = append([]string{filepath.Join(filepath.Dir(path), "templates")}, config.TemplatesDirs...)
Note: root.Note,
Groups: groups, return config, nil
Format: tomlConf.Format,
Tool: ToolConfig{
Editor: opt.NewNotEmptyString(tomlConf.Tool.Editor),
Pager: opt.NewStringWithPtr(tomlConf.Tool.Pager),
FzfPreview: opt.NewStringWithPtr(tomlConf.Tool.FzfPreview),
},
Aliases: aliases,
Extra: root.Extra,
}, nil
} }
func (c GroupConfig) merge(tomlConf tomlGroupConfig, name string, templatesDir string) GroupConfig { func (c GroupConfig) merge(tomlConf tomlGroupConfig, name string) GroupConfig {
res := c.Clone() res := c.Clone()
if tomlConf.Paths != nil { if tomlConf.Paths != nil {
@ -211,7 +289,7 @@ func (c GroupConfig) merge(tomlConf tomlGroupConfig, name string, templatesDir s
res.Note.Extension = note.Extension res.Note.Extension = note.Extension
} }
if note.Template != "" { if note.Template != "" {
res.Note.BodyTemplatePath = templatePathFromString(note.Template, templatesDir) res.Note.BodyTemplatePath = opt.NewNotEmptyString(note.Template)
} }
if note.IDLength != 0 { if note.IDLength != 0 {
res.Note.IDOptions.Length = note.IDLength res.Note.IDOptions.Length = note.IDLength
@ -241,7 +319,7 @@ func (c GroupConfig) merge(tomlConf tomlGroupConfig, name string, templatesDir s
type tomlConfig struct { type tomlConfig struct {
Note tomlNoteConfig Note tomlNoteConfig
Groups map[string]tomlGroupConfig `toml:"group"` Groups map[string]tomlGroupConfig `toml:"group"`
Format FormatConfig Format tomlFormatConfig
Tool tomlToolConfig Tool tomlToolConfig
Extra map[string]string Extra map[string]string
Aliases map[string]string `toml:"alias"` Aliases map[string]string `toml:"alias"`
@ -264,8 +342,18 @@ type tomlGroupConfig struct {
Extra map[string]string Extra map[string]string
} }
type tomlFormatConfig struct {
Markdown tomlMarkdownConfig
}
type tomlMarkdownConfig struct {
Hashtags *bool `toml:"hashtags"`
ColonTags *bool `toml:"colon-tags"`
MultiwordTags *bool `toml:"multiword-tags"`
}
type tomlToolConfig struct { type tomlToolConfig struct {
Editor string Editor *string
Pager *string Pager *string
FzfPreview *string `toml:"fzf-preview"` FzfPreview *string `toml:"fzf-preview"`
} }
@ -297,13 +385,3 @@ func caseFromString(c string) Case {
return CaseLower return CaseLower
} }
} }
func templatePathFromString(template string, templatesDir string) opt.String {
if template == "" {
return opt.NullString
}
if !filepath.IsAbs(template) {
template = filepath.Join(templatesDir, template)
}
return opt.NewString(template)
}

@ -2,7 +2,10 @@ package zk
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"testing" "testing"
"time"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/mickael-menu/zk/util/opt" "github.com/mickael-menu/zk/util/opt"
@ -10,10 +13,10 @@ import (
) )
func TestParseDefaultConfig(t *testing.T) { func TestParseDefaultConfig(t *testing.T) {
conf, err := ParseConfig([]byte(""), "") conf, err := ParseConfig([]byte(""), ".zk/config.toml", NewDefaultConfig())
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, conf, &Config{ assert.Equal(t, conf, Config{
Note: NoteConfig{ Note: NoteConfig{
FilenameTemplate: "{{id}}", FilenameTemplate: "{{id}}",
Extension: "md", Extension: "md",
@ -39,16 +42,15 @@ func TestParseDefaultConfig(t *testing.T) {
Pager: opt.NullString, Pager: opt.NullString,
FzfPreview: opt.NullString, FzfPreview: opt.NullString,
}, },
Aliases: make(map[string]string), Aliases: make(map[string]string),
Extra: make(map[string]string), Extra: make(map[string]string),
TemplatesDirs: []string{".zk/templates"},
}) })
} }
func TestParseInvalidConfig(t *testing.T) { func TestParseInvalidConfig(t *testing.T) {
conf, err := ParseConfig([]byte(`;`), "") _, err := ParseConfig([]byte(`;`), ".zk/config.toml", NewDefaultConfig())
assert.NotNil(t, err) assert.NotNil(t, err)
assert.Nil(t, conf)
} }
func TestParseComplete(t *testing.T) { func TestParseComplete(t *testing.T) {
@ -104,10 +106,10 @@ func TestParseComplete(t *testing.T) {
[group."without path"] [group."without path"]
paths = [] paths = []
`), "") `), ".zk/config.toml", NewDefaultConfig())
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, conf, &Config{ assert.Equal(t, conf, Config{
Note: NoteConfig{ Note: NoteConfig{
FilenameTemplate: "{{id}}.note", FilenameTemplate: "{{id}}.note",
Extension: "txt", Extension: "txt",
@ -200,6 +202,7 @@ func TestParseComplete(t *testing.T) {
"hello": "world", "hello": "world",
"salut": "le monde", "salut": "le monde",
}, },
TemplatesDirs: []string{".zk/templates"},
}) })
} }
@ -231,10 +234,10 @@ func TestParseMergesGroupConfig(t *testing.T) {
log-ext = "value" log-ext = "value"
[group.inherited] [group.inherited]
`), "") `), ".zk/config.toml", NewDefaultConfig())
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, conf, &Config{ assert.Equal(t, conf, Config{
Note: NoteConfig{ Note: NoteConfig{
FilenameTemplate: "root-filename", FilenameTemplate: "root-filename",
Extension: "txt", Extension: "txt",
@ -300,6 +303,7 @@ func TestParseMergesGroupConfig(t *testing.T) {
"hello": "world", "hello": "world",
"salut": "le monde", "salut": "le monde",
}, },
TemplatesDirs: []string{".zk/templates"},
}) })
} }
@ -310,7 +314,7 @@ func TestParsePreservePropertiesAllowingEmptyValues(t *testing.T) {
[tool] [tool]
pager = "" pager = ""
fzf-preview = "" fzf-preview = ""
`), "") `), ".zk/config.toml", NewDefaultConfig())
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, conf.Tool.Pager.IsNull(), false) assert.Equal(t, conf.Tool.Pager.IsNull(), false)
@ -325,7 +329,7 @@ func TestParseIDCharset(t *testing.T) {
[note] [note]
id-charset = "%v" id-charset = "%v"
`, charset) `, charset)
conf, err := ParseConfig([]byte(toml), "") conf, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig())
assert.Nil(t, err) assert.Nil(t, err)
if !cmp.Equal(conf.Note.IDOptions.Charset, expected) { if !cmp.Equal(conf.Note.IDOptions.Charset, expected) {
t.Errorf("Didn't parse ID charset `%v` as expected", charset) t.Errorf("Didn't parse ID charset `%v` as expected", charset)
@ -346,7 +350,7 @@ func TestParseIDCase(t *testing.T) {
[note] [note]
id-case = "%v" id-case = "%v"
`, letterCase) `, letterCase)
conf, err := ParseConfig([]byte(toml), "") conf, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig())
assert.Nil(t, err) assert.Nil(t, err)
if !cmp.Equal(conf.Note.IDOptions.Case, expected) { if !cmp.Equal(conf.Note.IDOptions.Case, expected) {
t.Errorf("Didn't parse ID case `%v` as expected", letterCase) t.Errorf("Didn't parse ID case `%v` as expected", letterCase)
@ -359,21 +363,35 @@ func TestParseIDCase(t *testing.T) {
test("unknown", CaseLower) test("unknown", CaseLower)
} }
func TestParseResolvesTemplatePaths(t *testing.T) { func TestLocateTemplate(t *testing.T) {
test := func(template string, expected string) { root := fmt.Sprintf("/tmp/zk-test-%d", time.Now().Unix())
toml := fmt.Sprintf(` os.Remove(root)
[note] os.MkdirAll(filepath.Join(root, "templates"), os.ModePerm)
template = "%v"
`, template) test := func(template string, expected string, exists bool) {
conf, err := ParseConfig([]byte(toml), "/test/.zk/templates") conf, err := ParseConfig([]byte(""), filepath.Join(root, "config.toml"), NewDefaultConfig())
assert.Nil(t, err) assert.Nil(t, err)
if !cmp.Equal(conf.Note.BodyTemplatePath, opt.NewString(expected)) {
t.Errorf("Didn't resolve template `%v` as expected: %v", template, conf.Note.BodyTemplatePath) path, ok := conf.LocateTemplate(template)
if exists {
assert.True(t, ok)
if path != expected {
t.Errorf("Didn't resolve template `%v` as expected: %v", template, expected)
}
} else {
assert.False(t, ok)
} }
} }
test("template.tpl", "/test/.zk/templates/template.tpl") tpl1 := filepath.Join(root, "templates/template.tpl")
test("/abs/template.tpl", "/abs/template.tpl") test("template.tpl", tpl1, false)
os.Create(tpl1)
test("template.tpl", tpl1, true)
tpl2 := filepath.Join(root, "abs.tpl")
test(tpl2, tpl2, false)
os.Create(tpl2)
test(tpl2, tpl2, true)
} }
func TestGroupConfigClone(t *testing.T) { func TestGroupConfigClone(t *testing.T) {

@ -2,7 +2,6 @@ package zk
import ( import (
"fmt" "fmt"
"io/ioutil"
"path/filepath" "path/filepath"
"strings" "strings"
@ -178,7 +177,7 @@ type Dir struct {
} }
// Open locates a notebook at the given path and parses its configuration. // Open locates a notebook at the given path and parses its configuration.
func Open(path string) (*Zk, error) { func Open(path string, parentConfig Config) (*Zk, error) {
wrap := errors.Wrapper("open failed") wrap := errors.Wrapper("open failed")
path, err := filepath.Abs(path) path, err := filepath.Abs(path)
@ -190,20 +189,14 @@ func Open(path string) (*Zk, error) {
return nil, wrap(err) return nil, wrap(err)
} }
configContent, err := ioutil.ReadFile(filepath.Join(path, ".zk/config.toml")) config, err := OpenConfig(filepath.Join(path, ".zk/config.toml"), parentConfig)
if err != nil {
return nil, wrap(err)
}
templatesDir := filepath.Join(path, ".zk/templates")
config, err := ParseConfig(configContent, templatesDir)
if err != nil { if err != nil {
return nil, wrap(err) return nil, wrap(err)
} }
return &Zk{ return &Zk{
Path: path, Path: path,
Config: *config, Config: config,
}, nil }, nil
} }

@ -12,6 +12,12 @@ Each [notebook](notebook.md) contains a configuration file used to customize you
* [`fzf`](tool-fzf.md) * [`fzf`](tool-fzf.md)
* `[alias]` holds your [command aliases](config-alias.md) * `[alias]` holds your [command aliases](config-alias.md)
## Global configuration file
You can also create a global configuration file to share aliases and settings across several notebooks. The global configuration is by default located at `~/.config/zk/config.toml`, but you can customize its location with the [`XDG_CONFIG_HOME`](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) environment variable.
Notebook configuration files will inherit the settings defined in the global configuration file. You can also share templates by storing them under `~/.config/zk/templates/`.
## Complete example ## Complete example
Here's an example of a complete configuration file: Here's an example of a complete configuration file:

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"strings"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
"github.com/mickael-menu/zk/cmd" "github.com/mickael-menu/zk/cmd"
@ -53,7 +54,8 @@ func (cmd *ShowHelp) Run(container *cmd.Container) error {
func main() { func main() {
// Create the dependency graph. // Create the dependency graph.
container := cmd.NewContainer() container, err := cmd.NewContainer()
fatalIfError(err)
if isAlias, err := runAlias(container, os.Args[1:]); isAlias { if isAlias, err := runAlias(container, os.Args[1:]); isAlias {
fatalIfError(err) fatalIfError(err)
@ -97,31 +99,33 @@ func fatalIfError(err error) {
// runAlias will execute a user alias if the command is one of them. // runAlias will execute a user alias if the command is one of them.
func runAlias(container *cmd.Container, args []string) (bool, error) { func runAlias(container *cmd.Container, args []string) (bool, error) {
if len(args) < 1 {
return false, nil
}
runningAlias := os.Getenv("ZK_RUNNING_ALIAS") runningAlias := os.Getenv("ZK_RUNNING_ALIAS")
if zk, err := container.OpenZk(); err == nil && len(args) >= 1 { for alias, cmdStr := range container.Config.Aliases {
for alias, cmdStr := range zk.Config.Aliases { if alias == runningAlias || alias != args[0] {
if alias == runningAlias || alias != args[0] { continue
continue }
}
// Prevent infinite loop if an alias calls itself. // Prevent infinite loop if an alias calls itself.
os.Setenv("ZK_RUNNING_ALIAS", alias) os.Setenv("ZK_RUNNING_ALIAS", alias)
cmd := executil.CommandFromString(cmdStr, args[1:]...) cmd := executil.CommandFromString(cmdStr, args[1:]...)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
if err, ok := err.(*exec.ExitError); ok { if err, ok := err.(*exec.ExitError); ok {
os.Exit(err.ExitCode()) os.Exit(err.ExitCode())
return true, nil return true, nil
} else { } else {
return true, err return true, err
}
} }
return true, nil
} }
return true, nil
} }
return false, nil return false, nil

@ -10,6 +10,18 @@ import (
"github.com/mickael-menu/pretty" "github.com/mickael-menu/pretty"
) )
func True(t *testing.T, value bool) {
if !value {
t.Errorf("Expected to be true")
}
}
func False(t *testing.T, value bool) {
if value {
t.Errorf("Expected to be false")
}
}
func Nil(t *testing.T, value interface{}) { func Nil(t *testing.T, value interface{}) {
if !isNil(value) { if !isNil(value) {
t.Errorf("Expected `%v` (type %v) to be nil", value, reflect.TypeOf(value)) t.Errorf("Expected `%v` (type %v) to be nil", value, reflect.TypeOf(value))

Loading…
Cancel
Save