Add --working-dir option (#29)

pull/30/head
Mickaël Menu 3 years ago committed by GitHub
parent 50855154e2
commit 6ba92a03b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,7 +7,9 @@ All notable changes to this project will be documented in this file.
### Changed
* The local configuration is not required anymore in a notebook's `.zk` directory.
* `--notebook-dir` does not change the working directory anymore, instead it sets manually the current notebook and disable auto-discovery. Use the new `--working-dir`/`-W` flag to run `zk` as if it was started from this path instead of the current working directory.
* For convenience, `ZK_NOTEBOOK_DIR` behaves like setting a `--working-dir` fallback, instead of `--notebook-dir`. This way, paths will be relative to the root of the notebook.
* A practical use case is to use `zk list -W .` when outside a notebook. This will list the notes in `ZK_NOTEBOOK_DIR` but print paths relative to the current directory, making them actionable from your terminal emulator.
## 0.3.0

@ -12,7 +12,7 @@ import (
// FileStorage implements the port core.FileStorage.
type FileStorage struct {
// Current working directory.
WorkingDir string
workingDir string
logger util.Logger
}
@ -30,10 +30,18 @@ func NewFileStorage(workingDir string, logger util.Logger) (*FileStorage, error)
return &FileStorage{workingDir, logger}, nil
}
func (fs *FileStorage) WorkingDir() string {
return fs.workingDir
}
func (fs *FileStorage) SetWorkingDir(path string) {
fs.workingDir = path
}
func (fs *FileStorage) Abs(path string) (string, error) {
var err error
if !filepath.IsAbs(path) {
path = filepath.Join(fs.WorkingDir, path)
path = filepath.Join(fs.workingDir, path)
path, err = filepath.Abs(path)
if err != nil {
return path, err
@ -44,7 +52,7 @@ func (fs *FileStorage) Abs(path string) (string, error) {
}
func (fs *FileStorage) Rel(path string) (string, error) {
return filepath.Rel(fs.WorkingDir, path)
return filepath.Rel(fs.workingDir, path)
}
func (fs *FileStorage) Canonical(path string) string {

@ -14,6 +14,7 @@ import (
// NoteFilter uses fzf to filter interactively a set of notes.
type NoteFilter struct {
opts NoteFilterOpts
fs core.FileStorage
terminal *term.Terminal
}
@ -34,13 +35,12 @@ type NoteFilterOpts struct {
NewNoteDir *core.Dir
// Absolute path to the notebook.
NotebookDir string
// Absolute path to the working directory.
WorkingDir string
}
func NewNoteFilter(opts NoteFilterOpts, terminal *term.Terminal) *NoteFilter {
func NewNoteFilter(opts NoteFilterOpts, fs core.FileStorage, terminal *term.Terminal) *NoteFilter {
return &NoteFilter{
opts: opts,
fs: fs,
terminal: terminal,
}
}
@ -49,18 +49,15 @@ func NewNoteFilter(opts NoteFilterOpts, terminal *term.Terminal) *NoteFilter {
func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote, error) {
selectedNotes := make([]core.ContextualNote, 0)
relPaths := []string{}
absPaths := []string{}
if !f.opts.Interactive || !f.terminal.IsInteractive() || (!f.opts.AlwaysFilter && len(notes) == 0) {
return notes, nil
}
for _, note := range notes {
absPath := filepath.Join(f.opts.NotebookDir, note.Path)
relPath, err := filepath.Rel(f.opts.WorkingDir, absPath)
if err != nil {
return selectedNotes, err
}
relPaths = append(relPaths, relPath)
absPaths = append(absPaths, filepath.Join(f.opts.NotebookDir, note.Path))
relPaths = append(relPaths, note.Path)
}
zkBin, err := os.Executable()
@ -84,11 +81,6 @@ func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote,
}
previewCmd := f.opts.PreviewCmd.OrString("cat {-1}").Unwrap()
if previewCmd != "" {
// The note paths will be relative to the current path, so we need to
// move there otherwise the preview command will fail.
previewCmd = `cd "` + f.opts.WorkingDir + `" && ` + previewCmd
}
fzf, err := New(Opts{
PreviewCmd: opt.NewNotEmptyString(previewCmd),
@ -107,7 +99,7 @@ func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote,
fzf.Add([]string{
f.terminal.MustStyle(title, core.StyleYellow),
f.terminal.MustStyle(stringsutil.JoinLines(note.Body), core.StyleUnderstate),
f.terminal.MustStyle(relPaths[i], core.StyleUnderstate),
f.terminal.MustStyle(absPaths[i], core.StyleUnderstate),
})
}
@ -119,7 +111,7 @@ func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote,
for _, s := range selection {
path := s[len(s)-1]
for i, m := range notes {
if relPaths[i] == path {
if absPaths[i] == path {
selectedNotes = append(selectedNotes, m)
}
}

@ -38,7 +38,6 @@ func (cmd *Edit) Run(container *cli.Container) error {
PreviewCmd: container.Config.Tool.FzfPreview,
NewNoteDir: cmd.newNoteDir(notebook),
NotebookDir: notebook.Path,
WorkingDir: container.WorkingDir,
})
notes, err = filter.Apply(notes)

@ -52,7 +52,6 @@ func (cmd *List) Run(container *cli.Container) error {
AlwaysFilter: false,
PreviewCmd: container.Config.Tool.FzfPreview,
NotebookDir: notebook.Path,
WorkingDir: container.WorkingDir,
})
notes, err = filter.Apply(notes)

@ -21,6 +21,11 @@ import (
"github.com/mickael-menu/zk/internal/util/rand"
)
type Dirs struct {
NotebookDir string
WorkingDir string
}
type Container struct {
Version string
Config core.Config
@ -136,16 +141,18 @@ func globalConfigDir() string {
// SetCurrentNotebook sets the first notebook found in the given search paths
// as the current default one.
func (c *Container) SetCurrentNotebook(searchPaths []string) {
if len(searchPaths) == 0 {
func (c *Container) SetCurrentNotebook(searchDirs []Dirs) {
if len(searchDirs) == 0 {
return
}
for _, path := range searchPaths {
c.currentNotebook, c.currentNotebookErr = c.Notebooks.Open(path)
for _, dirs := range searchDirs {
notebookDir := c.FS.Canonical(dirs.NotebookDir)
workingDir := c.FS.Canonical(dirs.WorkingDir)
c.currentNotebook, c.currentNotebookErr = c.Notebooks.Open(notebookDir)
if c.currentNotebookErr == nil {
c.WorkingDir = path
c.FS.WorkingDir = path
c.setWorkingDir(workingDir)
c.Config = c.currentNotebook.Config
// FIXME: Is there something to do to support multiple notebooks here?
os.Setenv("ZK_NOTEBOOK_DIR", c.currentNotebook.Path)
@ -154,13 +161,20 @@ func (c *Container) SetCurrentNotebook(searchPaths []string) {
}
}
// SetWorkingDir resets the current working directory.
func (c *Container) setWorkingDir(path string) {
path = c.FS.Canonical(path)
c.WorkingDir = path
c.FS.SetWorkingDir(path)
}
// CurrentNotebook returns the current default notebook.
func (c *Container) CurrentNotebook() (*core.Notebook, error) {
return c.currentNotebook, c.currentNotebookErr
}
func (c *Container) NewNoteFilter(opts fzf.NoteFilterOpts) *fzf.NoteFilter {
return fzf.NewNoteFilter(opts, c.Terminal)
return fzf.NewNoteFilter(opts, c.FS, c.Terminal)
}
func (c *Container) NewNoteEditor(notebook *core.Notebook) (*editor.Editor, error) {

@ -3,6 +3,9 @@ package core
// FileStorage is a port providing read and write access to a file storage.
type FileStorage interface {
// WorkingDir returns the current working directory.
WorkingDir() string
// Abs makes the given file path absolute if needed, using the FileStorage
// working directory.
Abs(path string) (string, error)

@ -8,25 +8,29 @@ import (
// fileStorageMock implements an in-memory FileStorage for testing purposes.
type fileStorageMock struct {
// Working directory used to calculate relative paths.
WorkingDir string
workingDir string
// File content indexed by their path in this file storage.
Files map[string]string
files map[string]string
// Existing directories
Dirs []string
dirs []string
}
func newFileStorageMock(workingDir string, dirs []string) *fileStorageMock {
return &fileStorageMock{
WorkingDir: workingDir,
Files: map[string]string{},
Dirs: dirs,
workingDir: workingDir,
files: map[string]string{},
dirs: dirs,
}
}
func (fs *fileStorageMock) WorkingDir() string {
return fs.workingDir
}
func (fs *fileStorageMock) Abs(path string) (string, error) {
var err error
if !filepath.IsAbs(path) {
path = filepath.Join(fs.WorkingDir, path)
path = filepath.Join(fs.workingDir, path)
path, err = filepath.Abs(path)
if err != nil {
return path, err
@ -37,7 +41,7 @@ func (fs *fileStorageMock) Abs(path string) (string, error) {
}
func (fs *fileStorageMock) Rel(path string) (string, error) {
return filepath.Rel(fs.WorkingDir, path)
return filepath.Rel(fs.workingDir, path)
}
func (fs *fileStorageMock) Canonical(path string) string {
@ -45,12 +49,12 @@ func (fs *fileStorageMock) Canonical(path string) string {
}
func (fs *fileStorageMock) FileExists(path string) (bool, error) {
_, ok := fs.Files[path]
_, ok := fs.files[path]
return ok, nil
}
func (fs *fileStorageMock) DirExists(path string) (bool, error) {
for _, dir := range fs.Dirs {
for _, dir := range fs.dirs {
if dir == path {
return true, nil
}
@ -67,11 +71,11 @@ func (fs *fileStorageMock) IsDescendantOf(dir string, path string) (bool, error)
}
func (fs *fileStorageMock) Read(path string) ([]byte, error) {
content, _ := fs.Files[path]
content, _ := fs.files[path]
return []byte(content), nil
}
func (fs *fileStorageMock) Write(path string, content []byte) error {
fs.Files[path] = string(content)
fs.files[path] = string(content)
return nil
}

@ -29,7 +29,7 @@ func TestNotebookNewNote(t *testing.T) {
assert.Equal(t, path, "/notebook/filename.ext")
// Check created note.
assert.Equal(t, test.fs.Files[path], "body")
assert.Equal(t, test.fs.files[path], "body")
assert.Equal(t, test.receivedLang, test.config.Note.Lang)
assert.Equal(t, test.receivedIDOpts, test.config.Note.IDOptions)
@ -115,7 +115,7 @@ func TestNotebookNewNoteInDir(t *testing.T) {
assert.Equal(t, path, "/notebook/a-dir/filename.ext")
// Check created note.
assert.Equal(t, test.fs.Files[path], "body")
assert.Equal(t, test.fs.files[path], "body")
// Check that the templates received the proper render contexts.
assert.Equal(t, test.filenameTemplate.Contexts, []interface{}{
@ -189,7 +189,7 @@ func TestNotebookNewNoteInDirWithGroup(t *testing.T) {
assert.Equal(t, path, "/notebook/a-dir/group-filename.group-ext")
// Check created note.
assert.Equal(t, test.fs.Files[path], "group template body")
assert.Equal(t, test.fs.files[path], "group template body")
assert.Equal(t, test.receivedLang, groupConfig.Note.Lang)
assert.Equal(t, test.receivedIDOpts, groupConfig.Note.IDOptions)
@ -264,7 +264,7 @@ func TestNotebookNewNoteWithGroup(t *testing.T) {
assert.Equal(t, path, "/notebook/group-filename.group-ext")
// Check created note.
assert.Equal(t, test.fs.Files[path], "group template body")
assert.Equal(t, test.fs.files[path], "group template body")
assert.Equal(t, test.receivedLang, groupConfig.Note.Lang)
assert.Equal(t, test.receivedIDOpts, groupConfig.Note.IDOptions)
@ -325,7 +325,7 @@ func TestNotebookNewNoteWithCustomTemplate(t *testing.T) {
})
assert.Nil(t, err)
assert.Equal(t, test.fs.Files[path], "custom body template")
assert.Equal(t, test.fs.files[path], "custom body template")
}
// Tries to generate a filename until one is free.
@ -352,7 +352,7 @@ func TestNotebookNewNoteTriesUntilFreePath(t *testing.T) {
assert.Equal(t, path, "/notebook/filename4.ext")
// Check created note.
assert.Equal(t, test.fs.Files[path], "body")
assert.Equal(t, test.fs.files[path], "body")
}
func TestNotebookNewNoteErrorWhenNoFreePath(t *testing.T) {
@ -375,7 +375,7 @@ func TestNotebookNewNoteErrorWhenNoFreePath(t *testing.T) {
})
assert.Err(t, err, "/notebook/filename50.ext: note already exists")
assert.Equal(t, test.fs.Files, files)
assert.Equal(t, test.fs.files, files)
}
var now = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
@ -409,7 +409,7 @@ func (t *newNoteTest) setup() {
t.dirs = append(t.dirs, t.rootDir)
t.fs = newFileStorageMock(t.rootDir, t.dirs)
if t.files != nil {
t.fs.Files = t.files
t.fs.files = t.files
}
t.templateLoader = newTemplateLoaderMock()

@ -212,7 +212,7 @@ func (n *Notebook) RelPath(originalPath string) (string, error) {
return path, wrap(err)
}
if strings.HasPrefix(path, "..") {
return path, fmt.Errorf("%s: path is outside the notebook", originalPath)
return path, fmt.Errorf("%s: path is outside the notebook at %s", originalPath, n.Path)
}
if path == "." {
path = ""

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/alecthomas/kong"
@ -25,8 +26,9 @@ var root struct {
List cmd.List `cmd group:"notes" help:"List notes matching the given criteria."`
Edit cmd.Edit `cmd group:"notes" help:"Edit notes matching the given criteria."`
NotebookDir string `type:path placeholder:PATH help:"Turn off notebook auto-discovery and set manually the notebook where commands are run."`
WorkingDir string `short:W type:path placeholder:PATH help:"Run as if zk was started in <PATH> instead of the current working directory."`
NoInput NoInput `help:"Never prompt or ask for confirmation."`
NotebookDir string `placeholder:"PATH" help:"Run as if zk was started in <PATH> instead of the current working directory."`
ShowHelp ShowHelp `cmd hidden default:"1"`
LSP cmd.LSP `cmd hidden`
@ -62,9 +64,11 @@ func main() {
fatalIfError(err)
// Open the notebook if there's any.
searchPaths, err := notebookSearchPaths()
dirs, err := parseDirs()
fatalIfError(err)
container.SetCurrentNotebook(searchPaths)
searchDirs, err := notebookSearchDirs(dirs)
fatalIfError(err)
container.SetCurrentNotebook(searchDirs)
// Run the alias or command.
if isAlias, err := runAlias(container, os.Args[1:]); isAlias {
@ -155,7 +159,7 @@ func runAlias(container *cli.Container, args []string) (bool, error) {
return false, nil
}
// notebookSearchPaths returns the places where zk will look for a notebook.
// notebookSearchDirs returns the places where zk will look for a notebook.
// The first successful candidate will be used as the working directory from
// which path arguments are relative from.
//
@ -163,50 +167,77 @@ func runAlias(container *cli.Container, args []string) (bool, error) {
// 1. --notebook-dir flag
// 2. current working directory
// 3. ZK_NOTEBOOK_DIR environment variable
func notebookSearchPaths() ([]string, error) {
// 1. --notebook-dir flag
notebookDir, err := parseNotebookDirFlag()
func notebookSearchDirs(dirs cli.Dirs) ([]cli.Dirs, error) {
wd, err := os.Getwd()
if err != nil {
return []string{}, err
return nil, err
}
if notebookDir != "" {
// If --notebook-dir is used, we want to only check there to report errors.
return []string{notebookDir}, nil
// 1. --notebook-dir flag
if dirs.NotebookDir != "" {
// If --notebook-dir is used, we want to only check there to report
// "notebook not found" errors.
if dirs.WorkingDir == "" {
dirs.WorkingDir = wd
}
return []cli.Dirs{dirs}, nil
}
candidates := []string{}
candidates := []cli.Dirs{}
// 2. current working directory
wd, err := os.Getwd()
if err != nil {
return nil, err
wdDirs := dirs
if wdDirs.WorkingDir == "" {
wdDirs.WorkingDir = wd
}
candidates = append(candidates, wd)
wdDirs.NotebookDir = wdDirs.WorkingDir
candidates = append(candidates, wdDirs)
// 3. ZK_NOTEBOOK_DIR environment variable
if notebookDir, ok := os.LookupEnv("ZK_NOTEBOOK_DIR"); ok {
candidates = append(candidates, notebookDir)
dirs := dirs
dirs.NotebookDir = notebookDir
if dirs.WorkingDir == "" {
dirs.WorkingDir = notebookDir
}
candidates = append(candidates, dirs)
}
return candidates, nil
}
// parseNotebookDir returns the path to the notebook specified with the
// --notebook-dir flag.
// parseDirs returns the paths specified with the --notebook-dir and
// --working-dir flags.
//
// We need to parse the --notebook-dir flag before Kong, because we might need
// it to resolve zk command aliases before parsing the CLI.
func parseNotebookDirFlag() (string, error) {
foundFlag := false
for _, arg := range os.Args {
if arg == "--notebook-dir" {
foundFlag = true
} else if foundFlag {
return arg, nil
// We need to parse these flags before Kong, because we might need it to
// resolve zk command aliases before parsing the CLI.
func parseDirs() (cli.Dirs, error) {
var d cli.Dirs
var err error
findFlag := func(long string, short string) (string, error) {
foundFlag := ""
for _, arg := range os.Args {
if arg == long || (short != "" && arg == short) {
foundFlag = arg
} else if foundFlag != "" {
return filepath.Abs(arg)
}
}
if foundFlag != "" {
return "", errors.New(foundFlag + " requires a path argument")
}
return "", nil
}
if foundFlag {
return "", errors.New("--notebook-dir requires an argument")
d.NotebookDir, err = findFlag("--notebook-dir", "")
if err != nil {
return d, err
}
d.WorkingDir, err = findFlag("--working-dir", "-W")
if err != nil {
return d, err
}
return "", nil
return d, nil
}

Loading…
Cancel
Save