From 6ba92a03b70fcc36b0b9e30ca24e05b3bcf7057f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Sat, 17 Apr 2021 11:28:38 +0200 Subject: [PATCH] Add --working-dir option (#29) --- CHANGELOG.md | 4 +- internal/adapter/fs/fs.go | 14 ++++- internal/adapter/fzf/note_filter.go | 24 +++----- internal/cli/cmd/edit.go | 1 - internal/cli/cmd/list.go | 1 - internal/cli/container.go | 28 ++++++--- internal/core/fs.go | 3 + internal/core/fs_test.go | 28 +++++---- internal/core/note_new_test.go | 16 ++--- internal/core/notebook.go | 2 +- main.go | 93 +++++++++++++++++++---------- 11 files changed, 133 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05b3d78..a0c6203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/internal/adapter/fs/fs.go b/internal/adapter/fs/fs.go index c46749d..2c651f4 100644 --- a/internal/adapter/fs/fs.go +++ b/internal/adapter/fs/fs.go @@ -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 { diff --git a/internal/adapter/fzf/note_filter.go b/internal/adapter/fzf/note_filter.go index a841347..d7b9a48 100644 --- a/internal/adapter/fzf/note_filter.go +++ b/internal/adapter/fzf/note_filter.go @@ -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) } } diff --git a/internal/cli/cmd/edit.go b/internal/cli/cmd/edit.go index 667d893..2d2bfc1 100644 --- a/internal/cli/cmd/edit.go +++ b/internal/cli/cmd/edit.go @@ -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) diff --git a/internal/cli/cmd/list.go b/internal/cli/cmd/list.go index 85fc635..d8392e2 100644 --- a/internal/cli/cmd/list.go +++ b/internal/cli/cmd/list.go @@ -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) diff --git a/internal/cli/container.go b/internal/cli/container.go index 432e029..11500d9 100644 --- a/internal/cli/container.go +++ b/internal/cli/container.go @@ -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) { diff --git a/internal/core/fs.go b/internal/core/fs.go index 4810edc..ee540e4 100644 --- a/internal/core/fs.go +++ b/internal/core/fs.go @@ -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) diff --git a/internal/core/fs_test.go b/internal/core/fs_test.go index bdf9980..69f2479 100644 --- a/internal/core/fs_test.go +++ b/internal/core/fs_test.go @@ -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 } diff --git a/internal/core/note_new_test.go b/internal/core/note_new_test.go index 89baa3a..a005053 100644 --- a/internal/core/note_new_test.go +++ b/internal/core/note_new_test.go @@ -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() diff --git a/internal/core/notebook.go b/internal/core/notebook.go index 31fa456..006546d 100644 --- a/internal/core/notebook.go +++ b/internal/core/notebook.go @@ -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 = "" diff --git a/main.go b/main.go index cc292b0..b65f0e2 100644 --- a/main.go +++ b/main.go @@ -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 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 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 }