2020-12-25 11:14:01 +00:00
|
|
|
package zk
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2020-12-25 19:25:52 +00:00
|
|
|
"io/ioutil"
|
2020-12-25 11:14:01 +00:00
|
|
|
"path/filepath"
|
2021-02-20 17:15:47 +00:00
|
|
|
"strings"
|
2020-12-25 11:14:01 +00:00
|
|
|
|
|
|
|
"github.com/mickael-menu/zk/util/errors"
|
2020-12-31 12:44:17 +00:00
|
|
|
"github.com/mickael-menu/zk/util/paths"
|
2020-12-25 11:14:01 +00:00
|
|
|
)
|
|
|
|
|
2021-02-14 22:05:11 +00:00
|
|
|
const defaultConfig = `# zk configuration file
|
|
|
|
#
|
|
|
|
# Uncomment the properties you want to customize.
|
|
|
|
|
|
|
|
# NOTE SETTINGS
|
|
|
|
#
|
|
|
|
# Defines the default options used when generating new notes.
|
|
|
|
[note]
|
|
|
|
|
|
|
|
# Language used when writing notes.
|
|
|
|
# This is used to generate slugs or with date formats.
|
|
|
|
#language = "en"
|
|
|
|
|
|
|
|
# The default title used for new note, if no ` + "`" + `--title` + "`" + ` flag is provided.
|
|
|
|
#default-title = "Untitled"
|
|
|
|
|
|
|
|
# Template used to generate a note's filename, without extension.
|
|
|
|
#filename = "{{id}}"
|
|
|
|
|
|
|
|
# The file extension used for the notes.
|
|
|
|
#extension = "md"
|
|
|
|
|
|
|
|
# Template used to generate a note's content.
|
|
|
|
# If not an absolute path, it is relative to .zk/templates/
|
2021-02-27 13:24:40 +00:00
|
|
|
template = "default.md"
|
2021-02-14 22:05:11 +00:00
|
|
|
|
|
|
|
# Configure random ID generation.
|
|
|
|
|
|
|
|
# The charset used for random IDs. You can use:
|
|
|
|
# * letters: only letters from a to z.
|
|
|
|
# * numbers: 0 to 9
|
|
|
|
# * alphanum: letters + numbers
|
|
|
|
# * hex: hexadecimal, from a to f and 0 to 9
|
|
|
|
# * custom string: will use any character from the provided value
|
|
|
|
#id-charset = "alphanum"
|
|
|
|
|
|
|
|
# Length of the generated IDs.
|
|
|
|
#id-length = 4
|
|
|
|
|
|
|
|
# Letter case for the random IDs, among lower, upper or mixed.
|
|
|
|
#id-case = "lower"
|
|
|
|
|
|
|
|
|
|
|
|
# EXTRA VARIABLES
|
|
|
|
#
|
|
|
|
# A dictionary of variables you can use for any custom values when generating
|
|
|
|
# new notes. They are accessible in templates with {{extra.<key>}}
|
|
|
|
[extra]
|
|
|
|
|
|
|
|
#key = "value"
|
|
|
|
|
|
|
|
|
2021-02-19 21:41:36 +00:00
|
|
|
# GROUP OVERRIDES
|
2021-02-14 22:05:11 +00:00
|
|
|
#
|
|
|
|
# You can override global settings from [note] and [extra] for a particular
|
2021-02-19 21:41:36 +00:00
|
|
|
# group of notes by declaring a [group."<name>"] section.
|
|
|
|
#
|
|
|
|
# Specify the list of directories which will automatically belong to the group
|
|
|
|
# with the optional ` + "`" + `paths` + "`" + ` property.
|
|
|
|
#
|
2021-03-07 16:00:09 +00:00
|
|
|
# Omitting ` + "`" + `paths` + "`" + ` is equivalent to providing a single path equal to the name of
|
2021-02-19 21:41:36 +00:00
|
|
|
# the group. This can be useful to quickly declare a group by the name of the
|
|
|
|
# directory it applies to.
|
2021-02-14 22:05:11 +00:00
|
|
|
|
2021-02-19 21:41:36 +00:00
|
|
|
#[dir."<NAME>"]
|
|
|
|
#paths = ["<DIR1>", "<DIR2>"]
|
|
|
|
#[dir."<NAME>".note]
|
2021-02-14 22:05:11 +00:00
|
|
|
#filename = "{{date now}}"
|
2021-02-19 21:41:36 +00:00
|
|
|
#[dir."<NAME>".extra]
|
2021-02-14 22:05:11 +00:00
|
|
|
#key = "value"
|
|
|
|
|
|
|
|
|
|
|
|
# EXTERNAL TOOLS
|
|
|
|
[tool]
|
|
|
|
|
|
|
|
# Default editor used to open notes. When not set, the EDITOR or VISUAL
|
|
|
|
# environment variables are used.
|
|
|
|
#editor = "vim"
|
|
|
|
|
|
|
|
# Pager used to scroll through long output. If you want to disable paging
|
|
|
|
# altogether, set it to an empty string "".
|
|
|
|
#pager = "less -FIRX"
|
|
|
|
|
|
|
|
# Command used to preview a note during interactive fzf mode.
|
|
|
|
# Set it to an empty string "" to disable preview.
|
|
|
|
|
|
|
|
# bat is a great tool to render Markdown document with syntax highlighting.
|
|
|
|
#https://github.com/sharkdp/bat
|
2021-02-24 20:49:56 +00:00
|
|
|
#fzf-preview = "bat -p --color always {-1}"
|
2021-02-14 22:05:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
# COMMAND ALIASES
|
|
|
|
#
|
|
|
|
# Aliases are user commands called with ` + "`" + `zk <alias> [<flags>] [<args>]` + "`" + `.
|
|
|
|
#
|
|
|
|
# The alias will be executed with ` + "`" + `$SHELL -c` + "`" + `, please refer to your shell's
|
|
|
|
# man page to see the available syntax. In most shells:
|
|
|
|
# * $@ can be used to expand all the provided flags and arguments
|
|
|
|
# * you can pipe commands together with the usual | character
|
|
|
|
#
|
|
|
|
[alias]
|
|
|
|
# Here are a few aliases to get you started.
|
|
|
|
|
|
|
|
# Shortcut to a command.
|
|
|
|
#ls = "zk list $@"
|
|
|
|
|
|
|
|
# Default flags for an existing command.
|
|
|
|
#list = "zk list --quiet $@"
|
|
|
|
|
|
|
|
# Edit the last modified note.
|
|
|
|
#editlast = "zk edit --limit 1 --sort modified- $@"
|
|
|
|
|
|
|
|
# Edit the notes selected interactively among the notes created the last two weeks.
|
|
|
|
# This alias doesn't take any argument, so we don't use $@.
|
|
|
|
#recent = "zk edit --sort created- --created-after 'last two weeks' --interactive"
|
|
|
|
|
|
|
|
# Print paths separated with colons for the notes found with the given
|
|
|
|
# arguments. This can be useful to expand a complex search query into a flag
|
|
|
|
# taking only paths. For example:
|
2021-03-07 16:14:07 +00:00
|
|
|
# zk list --link-to "` + "`" + `zk path -m potatoe` + "`" + `"
|
2021-02-14 22:05:11 +00:00
|
|
|
#path = "zk list --quiet --format {{path}} --delimiter , $@"
|
|
|
|
|
|
|
|
# Show a random note.
|
|
|
|
#lucky = "zk list --quiet --format full --sort random --limit 1"
|
|
|
|
|
|
|
|
# Returns the Git history for the notes found with the given arguments.
|
|
|
|
# Note the use of a pipe and the location of $@.
|
|
|
|
#hist = "zk list --format path --delimiter0 --quiet $@ | xargs -t -0 git log --patch --"
|
|
|
|
|
|
|
|
# Edit this configuration file.
|
|
|
|
#conf = '$EDITOR "$ZK_PATH/.zk/config.toml"'
|
|
|
|
`
|
2020-12-25 11:14:01 +00:00
|
|
|
|
2021-02-27 13:24:40 +00:00
|
|
|
const defaultTemplate = `# {{title}}
|
|
|
|
|
|
|
|
{{content}}
|
|
|
|
`
|
|
|
|
|
2021-02-15 21:44:31 +00:00
|
|
|
// Zk (Zettelkasten) represents an opened notebook.
|
2020-12-25 19:25:52 +00:00
|
|
|
type Zk struct {
|
2021-02-15 21:44:31 +00:00
|
|
|
// Notebook root path.
|
2020-12-28 12:15:07 +00:00
|
|
|
Path string
|
2020-12-31 12:44:17 +00:00
|
|
|
// Global user configuration.
|
|
|
|
Config Config
|
2020-12-25 19:25:52 +00:00
|
|
|
}
|
|
|
|
|
2021-02-15 21:44:31 +00:00
|
|
|
// Dir represents a directory inside a notebook.
|
2021-01-03 16:09:10 +00:00
|
|
|
type Dir struct {
|
2021-02-15 21:44:31 +00:00
|
|
|
// Name of the directory, which is the path relative to the notebook's root.
|
2021-01-03 16:09:10 +00:00
|
|
|
Name string
|
|
|
|
// Absolute path to the directory.
|
|
|
|
Path string
|
|
|
|
// User configuration for this directory.
|
2021-02-19 21:41:36 +00:00
|
|
|
Config GroupConfig
|
2020-12-28 12:15:07 +00:00
|
|
|
}
|
|
|
|
|
2021-02-15 21:44:31 +00:00
|
|
|
// Open locates a notebook at the given path and parses its configuration.
|
2020-12-25 19:25:52 +00:00
|
|
|
func Open(path string) (*Zk, error) {
|
2020-12-25 11:14:01 +00:00
|
|
|
wrap := errors.Wrapper("open failed")
|
|
|
|
|
|
|
|
path, err := filepath.Abs(path)
|
|
|
|
if err != nil {
|
2020-12-25 19:25:52 +00:00
|
|
|
return nil, wrap(err)
|
2020-12-25 11:14:01 +00:00
|
|
|
}
|
2020-12-25 19:25:52 +00:00
|
|
|
path, err = locateRoot(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, wrap(err)
|
|
|
|
}
|
|
|
|
|
2021-02-06 20:39:18 +00:00
|
|
|
configContent, err := ioutil.ReadFile(filepath.Join(path, ".zk/config.toml"))
|
2020-12-25 19:25:52 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, wrap(err)
|
|
|
|
}
|
|
|
|
|
2020-12-31 12:44:17 +00:00
|
|
|
templatesDir := filepath.Join(path, ".zk/templates")
|
|
|
|
config, err := ParseConfig(configContent, templatesDir)
|
2020-12-25 19:25:52 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, wrap(err)
|
|
|
|
}
|
|
|
|
|
2020-12-28 12:15:07 +00:00
|
|
|
return &Zk{
|
|
|
|
Path: path,
|
2020-12-31 12:44:17 +00:00
|
|
|
Config: *config,
|
2020-12-28 12:15:07 +00:00
|
|
|
}, nil
|
2020-12-25 11:14:01 +00:00
|
|
|
}
|
|
|
|
|
2021-02-15 21:44:31 +00:00
|
|
|
// Create initializes a new notebook at the given path.
|
2020-12-25 11:14:01 +00:00
|
|
|
func Create(path string) error {
|
|
|
|
wrap := errors.Wrapper("init failed")
|
|
|
|
|
|
|
|
path, err := filepath.Abs(path)
|
|
|
|
if err != nil {
|
|
|
|
return wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if existingPath, err := locateRoot(path); err == nil {
|
2021-02-15 21:44:31 +00:00
|
|
|
return wrap(fmt.Errorf("a notebook already exists in %v", existingPath))
|
2020-12-25 11:14:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Write default config.toml.
|
2021-02-27 13:24:40 +00:00
|
|
|
err = paths.WriteString(filepath.Join(path, ".zk/config.toml"), defaultConfig)
|
2020-12-25 11:14:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return wrap(err)
|
|
|
|
}
|
2021-02-27 13:24:40 +00:00
|
|
|
|
|
|
|
// Write default template.
|
|
|
|
err = paths.WriteString(filepath.Join(path, ".zk/templates/default.md"), defaultTemplate)
|
2020-12-25 11:14:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-02-15 21:44:31 +00:00
|
|
|
// locate finds the root of the notebook containing the given path.
|
2020-12-25 11:14:01 +00:00
|
|
|
func locateRoot(path string) (string, error) {
|
|
|
|
if !filepath.IsAbs(path) {
|
|
|
|
panic("absolute path expected")
|
|
|
|
}
|
|
|
|
|
|
|
|
var locate func(string) (string, error)
|
|
|
|
locate = func(currentPath string) (string, error) {
|
|
|
|
if currentPath == "/" || currentPath == "." {
|
2021-02-15 21:44:31 +00:00
|
|
|
return "", fmt.Errorf("no notebook found in %v or a parent directory", path)
|
2020-12-25 11:14:01 +00:00
|
|
|
}
|
2020-12-31 12:44:17 +00:00
|
|
|
exists, err := paths.DirExists(filepath.Join(currentPath, ".zk"))
|
|
|
|
switch {
|
|
|
|
case err != nil:
|
|
|
|
return "", err
|
|
|
|
case exists:
|
2020-12-25 11:14:01 +00:00
|
|
|
return currentPath, nil
|
2020-12-31 12:44:17 +00:00
|
|
|
default:
|
|
|
|
return locate(filepath.Dir(currentPath))
|
2020-12-25 11:14:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return locate(path)
|
|
|
|
}
|
|
|
|
|
2021-02-15 21:44:31 +00:00
|
|
|
// DBPath returns the path to the notebook database.
|
2021-01-02 11:29:21 +00:00
|
|
|
func (zk *Zk) DBPath() string {
|
2021-02-27 09:20:53 +00:00
|
|
|
return filepath.Join(zk.Path, ".zk/notebook.db")
|
2021-01-02 11:29:21 +00:00
|
|
|
}
|
|
|
|
|
2021-02-15 21:44:31 +00:00
|
|
|
// RelPath returns the path relative to the notebook root to the given path.
|
2021-02-20 17:15:47 +00:00
|
|
|
func (zk *Zk) RelPath(absPath string) (string, error) {
|
|
|
|
wrap := errors.Wrapperf("%v: not a valid notebook path", absPath)
|
2020-12-28 12:15:07 +00:00
|
|
|
|
2021-02-20 17:15:47 +00:00
|
|
|
path, err := filepath.Abs(absPath)
|
2020-12-28 12:15:07 +00:00
|
|
|
if err != nil {
|
2021-01-12 18:38:58 +00:00
|
|
|
return path, wrap(err)
|
2020-12-28 12:15:07 +00:00
|
|
|
}
|
2021-01-12 18:38:58 +00:00
|
|
|
path, err = filepath.Rel(zk.Path, path)
|
|
|
|
if err != nil {
|
|
|
|
return path, wrap(err)
|
|
|
|
}
|
2021-02-20 17:15:47 +00:00
|
|
|
if strings.HasPrefix(path, "..") {
|
|
|
|
return path, fmt.Errorf("%s: path is outside the notebook", absPath)
|
|
|
|
}
|
2021-01-12 18:38:58 +00:00
|
|
|
if path == "." {
|
|
|
|
path = ""
|
|
|
|
}
|
|
|
|
return path, nil
|
|
|
|
}
|
2020-12-28 12:15:07 +00:00
|
|
|
|
2021-02-15 21:44:31 +00:00
|
|
|
// RootDir returns the root Dir for this notebook.
|
2021-02-07 16:48:23 +00:00
|
|
|
func (zk *Zk) RootDir() Dir {
|
|
|
|
return Dir{
|
|
|
|
Name: "",
|
|
|
|
Path: zk.Path,
|
2021-02-19 21:41:36 +00:00
|
|
|
Config: zk.Config.RootGroupConfig(),
|
2021-02-07 16:48:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-15 21:44:31 +00:00
|
|
|
// DirAt returns a Dir representation of the notebook directory at the given path.
|
2021-01-12 18:38:58 +00:00
|
|
|
func (zk *Zk) DirAt(path string, overrides ...ConfigOverrides) (*Dir, error) {
|
|
|
|
path, err := filepath.Abs(path)
|
2020-12-28 12:15:07 +00:00
|
|
|
if err != nil {
|
2021-02-15 21:44:31 +00:00
|
|
|
return nil, errors.Wrapf(err, "%v: not a valid notebook directory", path)
|
2020-12-28 12:15:07 +00:00
|
|
|
}
|
2021-01-12 18:38:58 +00:00
|
|
|
|
|
|
|
name, err := zk.RelPath(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2021-01-02 21:25:39 +00:00
|
|
|
}
|
2020-12-28 12:15:07 +00:00
|
|
|
|
2021-02-19 22:02:35 +00:00
|
|
|
config, err := zk.findConfigForDirNamed(name, overrides)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
config = config.Clone()
|
2020-12-31 14:54:39 +00:00
|
|
|
for _, v := range overrides {
|
|
|
|
config.Override(v)
|
|
|
|
}
|
2020-12-28 13:25:35 +00:00
|
|
|
|
2020-12-31 12:44:17 +00:00
|
|
|
return &Dir{
|
|
|
|
Name: name,
|
|
|
|
Path: path,
|
|
|
|
Config: config,
|
|
|
|
}, nil
|
2020-12-28 13:25:35 +00:00
|
|
|
}
|
|
|
|
|
2021-02-19 22:02:35 +00:00
|
|
|
func (zk *Zk) findConfigForDirNamed(name string, overrides []ConfigOverrides) (GroupConfig, error) {
|
|
|
|
// If there's a Group overrides, attempt to find a matching group.
|
2021-03-07 16:00:09 +00:00
|
|
|
overriddenGroup := ""
|
2021-02-19 22:02:35 +00:00
|
|
|
for _, o := range overrides {
|
|
|
|
if !o.Group.IsNull() {
|
2021-03-07 16:00:09 +00:00
|
|
|
overriddenGroup = o.Group.Unwrap()
|
|
|
|
if group, ok := zk.Config.Groups[overriddenGroup]; ok {
|
2021-02-19 22:02:35 +00:00
|
|
|
return group, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-07 16:00:09 +00:00
|
|
|
if overriddenGroup != "" {
|
|
|
|
return GroupConfig{}, fmt.Errorf("%s: group not find in the config file", overriddenGroup)
|
2021-02-19 22:02:35 +00:00
|
|
|
}
|
|
|
|
|
2021-02-20 17:41:57 +00:00
|
|
|
for groupName, group := range zk.Config.Groups {
|
2021-02-19 21:41:36 +00:00
|
|
|
for _, path := range group.Paths {
|
2021-02-20 17:41:57 +00:00
|
|
|
matches, err := filepath.Match(path, name)
|
|
|
|
if err != nil {
|
|
|
|
return GroupConfig{}, errors.Wrapf(err, "failed to match group %s to %s", groupName, name)
|
|
|
|
} else if matches {
|
2021-02-19 22:02:35 +00:00
|
|
|
return group, nil
|
2021-02-19 21:41:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Fallback on root config.
|
2021-02-19 22:02:35 +00:00
|
|
|
return zk.Config.RootGroupConfig(), nil
|
2021-02-19 21:41:36 +00:00
|
|
|
}
|
|
|
|
|
2020-12-31 12:44:17 +00:00
|
|
|
// RequiredDirAt is the same as DirAt, but checks that the directory exists
|
|
|
|
// before returning the Dir.
|
2020-12-31 14:54:39 +00:00
|
|
|
func (zk *Zk) RequireDirAt(path string, overrides ...ConfigOverrides) (*Dir, error) {
|
|
|
|
dir, err := zk.DirAt(path, overrides...)
|
2020-12-31 12:44:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2020-12-28 13:25:35 +00:00
|
|
|
}
|
2020-12-31 12:44:17 +00:00
|
|
|
exists, err := paths.Exists(dir.Path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2020-12-28 13:25:35 +00:00
|
|
|
}
|
2020-12-31 12:44:17 +00:00
|
|
|
if !exists {
|
|
|
|
return nil, fmt.Errorf("%v: directory not found", path)
|
2020-12-28 13:25:35 +00:00
|
|
|
}
|
2020-12-31 12:44:17 +00:00
|
|
|
return dir, nil
|
2020-12-28 13:25:35 +00:00
|
|
|
}
|