mirror of
https://github.com/dnote/dnote
synced 2026-03-17 07:55:50 +01:00
Allow multiple editor sessions (#221)
* Allow to get editor input from multiple sessions at a given time * Add license
This commit is contained in:
parent
92c3d8376e
commit
ddabdd9732
12 changed files with 219 additions and 27 deletions
2
pkg/cli/.gitignore
vendored
2
pkg/cli/.gitignore
vendored
|
|
@ -1,6 +1,6 @@
|
|||
main
|
||||
*.swo
|
||||
tmp/
|
||||
tmp*/
|
||||
/vendor
|
||||
test-dnote
|
||||
/dist
|
||||
|
|
|
|||
|
|
@ -115,8 +115,12 @@ func newRun(ctx context.DnoteCtx) infra.RunEFunc {
|
|||
}
|
||||
|
||||
if content == "" {
|
||||
fpath := ui.GetTmpContentPath(ctx)
|
||||
err := ui.GetEditorInput(ctx, fpath, &content)
|
||||
fpath, err := ui.GetTmpContentPath(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting temporarily content file path")
|
||||
}
|
||||
|
||||
err = ui.GetEditorInput(ctx, fpath, &content)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get editor input")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,10 @@ func newRun(ctx context.DnoteCtx) infra.RunEFunc {
|
|||
}
|
||||
|
||||
if newContent == "" {
|
||||
fpath := ui.GetTmpContentPath(ctx)
|
||||
fpath, err := ui.GetTmpContentPath(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting temporarily content file path")
|
||||
}
|
||||
|
||||
e := ioutil.WriteFile(fpath, []byte(oldContent), 0644)
|
||||
if e != nil {
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ var (
|
|||
DnoteDirName = ".dnote"
|
||||
// DnoteDBFileName is a filename for the Dnote SQLite database
|
||||
DnoteDBFileName = "dnote.db"
|
||||
// TmpContentFilename is the filename for a temporary content
|
||||
TmpContentFilename = "DNOTE_TMPCONTENT.md"
|
||||
// TmpContentFileBase is the base for the filename for a temporary content
|
||||
TmpContentFileBase = "DNOTE_TMPCONTENT"
|
||||
// TmpContentFileExt is the extension for the temporary content file
|
||||
TmpContentFileExt = "md"
|
||||
// ConfigFilename is the name of the config file
|
||||
ConfigFilename = "dnoterc"
|
||||
|
||||
|
|
|
|||
|
|
@ -315,7 +315,11 @@ func getEditorCommand() string {
|
|||
func initDnoteDir(ctx context.DnoteCtx) error {
|
||||
path := ctx.DnoteDir
|
||||
|
||||
if utils.FileExists(path) {
|
||||
ok, err := utils.FileExists(path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "checking if dnote dir exists")
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -329,7 +333,11 @@ func initDnoteDir(ctx context.DnoteCtx) error {
|
|||
// initConfigFile populates a new config file if it does not exist yet
|
||||
func initConfigFile(ctx context.DnoteCtx, apiEndpoint string) error {
|
||||
path := config.GetPath(ctx)
|
||||
if utils.FileExists(path) {
|
||||
ok, err := utils.FileExists(path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "checking if config exists")
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,10 +57,19 @@ func TestInit(t *testing.T) {
|
|||
db := database.OpenTestDB(t, opts.DnoteDir)
|
||||
|
||||
// Test
|
||||
if !utils.FileExists(opts.DnoteDir) {
|
||||
ok, err := utils.FileExists(opts.DnoteDir)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "checking if dnote dir exists"))
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("dnote directory was not initialized")
|
||||
}
|
||||
if !utils.FileExists(fmt.Sprintf("%s/%s", opts.DnoteDir, consts.ConfigFilename)) {
|
||||
|
||||
ok, err = utils.FileExists(fmt.Sprintf("%s/%s", opts.DnoteDir, consts.ConfigFilename))
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "checking if dnote config exists"))
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("config file was not initialized")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,11 @@ func makeSchema(complete bool) schema {
|
|||
func Legacy(ctx context.DnoteCtx) error {
|
||||
// If schema does not exist, no need run a legacy migration
|
||||
schemaPath := getSchemaPath(ctx)
|
||||
if ok := utils.FileExists(schemaPath); !ok {
|
||||
ok, err := utils.FileExists(schemaPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "checking if schema exists")
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -472,7 +476,11 @@ var migrateToV8SystemKeyBookMark = "bookmark"
|
|||
// migrateToV1 deletes YAML archive if exists
|
||||
func migrateToV1(ctx context.DnoteCtx) error {
|
||||
yamlPath := fmt.Sprintf("%s/%s", ctx.HomeDir, ".dnote-yaml-archived")
|
||||
if !utils.FileExists(yamlPath) {
|
||||
ok, err := utils.FileExists(yamlPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "checking if yaml file exists")
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,11 @@ func TestMigrateToV1(t *testing.T) {
|
|||
}
|
||||
|
||||
// test
|
||||
if utils.FileExists(yamlPath) {
|
||||
ok, err := utils.FileExists(yamlPath)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "checking if yaml file exists"))
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("YAML archive file has not been deleted")
|
||||
}
|
||||
})
|
||||
|
|
@ -93,7 +97,11 @@ func TestMigrateToV1(t *testing.T) {
|
|||
}
|
||||
|
||||
// test
|
||||
if utils.FileExists(yamlPath) {
|
||||
ok, err := utils.FileExists(yamlPath)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "checking if yaml file exists"))
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("YAML archive file must not exist")
|
||||
}
|
||||
})
|
||||
|
|
@ -372,23 +380,43 @@ func TestMigrateToV8(t *testing.T) {
|
|||
dnotercPath := fmt.Sprintf("%s/dnoterc", ctx.DnoteDir)
|
||||
schemaFilePath := fmt.Sprintf("%s/schema", ctx.DnoteDir)
|
||||
timestampFilePath := fmt.Sprintf("%s/timestamps", ctx.DnoteDir)
|
||||
if ok := utils.FileExists(dnoteFilePath); ok {
|
||||
|
||||
ok, err := utils.FileExists(dnoteFilePath)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "checking if file exists"))
|
||||
}
|
||||
if ok {
|
||||
t.Errorf("%s still exists", dnoteFilePath)
|
||||
}
|
||||
if ok := utils.FileExists(schemaFilePath); ok {
|
||||
t.Errorf("%s still exists", dnoteFilePath)
|
||||
|
||||
ok, err = utils.FileExists(schemaFilePath)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "checking if file exists"))
|
||||
}
|
||||
if ok := utils.FileExists(timestampFilePath); ok {
|
||||
t.Errorf("%s still exists", dnoteFilePath)
|
||||
if ok {
|
||||
t.Errorf("%s still exists", schemaFilePath)
|
||||
}
|
||||
if ok := utils.FileExists(dnotercPath); !ok {
|
||||
t.Errorf("%s should exist", dnotercPath)
|
||||
|
||||
ok, err = utils.FileExists(timestampFilePath)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "checking if file exists"))
|
||||
}
|
||||
if ok {
|
||||
t.Errorf("%s still exists", timestampFilePath)
|
||||
}
|
||||
|
||||
ok, err = utils.FileExists(dnotercPath)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "checking if file exists"))
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("%s still exists", dnotercPath)
|
||||
}
|
||||
|
||||
// 2. test if notes and books are migrated
|
||||
|
||||
var bookCount, noteCount int
|
||||
err := db.QueryRow("SELECT count(*) FROM books").Scan(&bookCount)
|
||||
err = db.QueryRow("SELECT count(*) FROM books").Scan(&bookCount)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "counting books"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,19 @@ import (
|
|||
|
||||
// GetTmpContentPath returns the path to the temporary file containing
|
||||
// content being added or edited
|
||||
func GetTmpContentPath(ctx context.DnoteCtx) string {
|
||||
return fmt.Sprintf("%s/%s", ctx.DnoteDir, consts.TmpContentFilename)
|
||||
func GetTmpContentPath(ctx context.DnoteCtx) (string, error) {
|
||||
for i := 0; ; i++ {
|
||||
filename := fmt.Sprintf("%s_%d.%s", consts.TmpContentFileBase, i, consts.TmpContentFileExt)
|
||||
candidate := fmt.Sprintf("%s/%s", ctx.DnoteDir, filename)
|
||||
|
||||
ok, err := utils.FileExists(candidate)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "checking if file exists at %s", candidate)
|
||||
}
|
||||
if !ok {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getEditorCommand returns the system's editor command with appropriate flags,
|
||||
|
|
@ -91,7 +102,11 @@ func newEditorCmd(ctx context.DnoteCtx, fpath string) (*exec.Cmd, error) {
|
|||
// GetEditorInput gets the user input by launching a text editor and waiting for
|
||||
// it to exit
|
||||
func GetEditorInput(ctx context.DnoteCtx, fpath string, content *string) error {
|
||||
if !utils.FileExists(fpath) {
|
||||
ok, err := utils.FileExists(fpath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "checking if the file exists at %s", fpath)
|
||||
}
|
||||
if !ok {
|
||||
f, err := os.Create(fpath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating a temporary content file")
|
||||
|
|
|
|||
90
pkg/cli/ui/editor_test.go
Normal file
90
pkg/cli/ui/editor_test.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/* Copyright (C) 2019 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote CLI.
|
||||
*
|
||||
* Dnote CLI is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote CLI is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Dnote CLI. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/dnote/pkg/assert"
|
||||
"github.com/dnote/dnote/pkg/cli/context"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestGetTmpContentPath(t *testing.T) {
|
||||
t.Run("no collision", func(t *testing.T) {
|
||||
ctx := context.InitTestCtx(t, "../tmp1", nil)
|
||||
defer context.TeardownTestCtx(t, ctx)
|
||||
|
||||
res, err := GetTmpContentPath(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "executing"))
|
||||
}
|
||||
|
||||
expected := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_0.md")
|
||||
assert.Equal(t, res, expected, "filename did not match")
|
||||
})
|
||||
|
||||
t.Run("one existing session", func(t *testing.T) {
|
||||
// set up
|
||||
ctx := context.InitTestCtx(t, "../tmp2", nil)
|
||||
defer context.TeardownTestCtx(t, ctx)
|
||||
|
||||
p := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_0.md")
|
||||
if _, err := os.Create(p); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "preparing the conflicting file"))
|
||||
}
|
||||
|
||||
// execute
|
||||
res, err := GetTmpContentPath(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "executing"))
|
||||
}
|
||||
|
||||
// test
|
||||
expected := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_1.md")
|
||||
assert.Equal(t, res, expected, "filename did not match")
|
||||
})
|
||||
|
||||
t.Run("two existing sessions", func(t *testing.T) {
|
||||
// set up
|
||||
ctx := context.InitTestCtx(t, "../tmp3", nil)
|
||||
defer context.TeardownTestCtx(t, ctx)
|
||||
|
||||
p1 := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_0.md")
|
||||
if _, err := os.Create(p1); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "preparing the conflicting file"))
|
||||
}
|
||||
p2 := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_1.md")
|
||||
if _, err := os.Create(p2); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "preparing the conflicting file"))
|
||||
}
|
||||
|
||||
// execute
|
||||
res, err := GetTmpContentPath(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "executing"))
|
||||
}
|
||||
|
||||
// test
|
||||
expected := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_2.md")
|
||||
assert.Equal(t, res, expected, "filename did not match")
|
||||
})
|
||||
}
|
||||
|
|
@ -44,9 +44,16 @@ func ReadFileAbs(relpath string) []byte {
|
|||
}
|
||||
|
||||
// FileExists checks if the file exists at the given path
|
||||
func FileExists(filepath string) bool {
|
||||
func FileExists(filepath string) (bool, error) {
|
||||
_, err := os.Stat(filepath)
|
||||
return !os.IsNotExist(err)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, errors.Wrap(err, "getting file info")
|
||||
}
|
||||
|
||||
// CopyDir copies a directory from src to dest, recursively copying nested
|
||||
|
|
|
|||
|
|
@ -1,3 +1,21 @@
|
|||
/* Copyright (C) 2019 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue