From dcacebf216a53abde7050e0fc4e660adc6da6f62 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Sat, 14 Oct 2017 19:03:23 +0100 Subject: [PATCH] Fixed editing notes --- CliClient/app/app-gui.js | 68 +++++++++++++++++++++------ CliClient/app/app.js | 6 +++ CliClient/app/base-command.js | 8 ++++ CliClient/app/cli-utils.js | 10 ++-- CliClient/app/command-edit.js | 67 +++++++++++++++----------- CliClient/app/command-sync.js | 2 +- CliClient/app/main.js | 20 ++++---- CliClient/locales/en_GB.po | 6 +-- CliClient/locales/fr_FR.po | 9 ++-- CliClient/locales/joplin.pot | 6 +-- ReactNativeClient/lib/synchronizer.js | 11 ++++- 11 files changed, 147 insertions(+), 66 deletions(-) diff --git a/CliClient/app/app-gui.js b/CliClient/app/app-gui.js index f029f2101d..0dbde30c2b 100644 --- a/CliClient/app/app-gui.js +++ b/CliClient/app/app-gui.js @@ -52,8 +52,20 @@ class AppGui { this.inputMode_ = AppGui.INPUT_MODE_NORMAL; + this.commandCancelCalled_ = false; + this.currentShortcutKeys_ = []; - this.lastShortcutKeyTime_ = 0; + this.lastShortcutKeyTime_ = 0; + + cliUtils.setStdout((...object) => { + for (let i = 0; i < object.length; i++) { + this.widget('console').bufferPush(object[i]); + } + }); + } + + renderer() { + return this.renderer_; } buildUi() { @@ -112,8 +124,10 @@ class AppGui { consoleWidget.hStretch = true; consoleWidget.name = 'console'; consoleWidget.prompt = chalk.green('Joplin') + ' ' + chalk.magenta('>') + ' '; - consoleWidget.on('accept', (event) => { - this.processCommand(event.input, 'console'); + consoleWidget.on('accept', async (event) => { + consoleWidget.promptVisible = false; + await this.processCommand(event.input, 'console'); + consoleWidget.promptVisible = true; }); const hLayout = new HLayoutWidget(); @@ -330,11 +344,21 @@ class AppGui { return ['ENTER', 'DOWN', 'UP', 'LEFT', 'RIGHT', 'DELETE', 'BACKSPACE', 'ESCAPE', 'TAB', 'SHIFT_TAB'].indexOf(name) >= 0; } + fullScreen(enable = true) { + if (enable) { + this.term().fullscreen(); + this.term().hideCursor(); + this.widget('root').invalidate(); + } else { + this.term().fullscreen(false); + this.term().showCursor(); + } + } + async start() { const term = this.term(); - term.fullscreen(); - term.hideCursor(); + this.fullScreen(); try { this.renderer_.start(); @@ -344,12 +368,32 @@ class AppGui { term.grabInput(); term.on('key', async (name, matches, data) => { - if (name === 'CTRL_C' ) { - term.showCursor(); - term.fullscreen(false); + + if (name === 'CTRL_D') { + const cmd = this.app().currentCommand(); + + if (cmd && cmd.cancellable() && !this.commandCancelCalled_) { + this.commandCancelCalled_ = true; + await cmd.cancel(); + this.commandCancelCalled_ = false; + } + + this.fullScreen(false); await this.app().exit(); return; } + + if (name === 'CTRL_C' ) { + const cmd = this.app().currentCommand(); + if (!cmd || !cmd.cancellable() || this.commandCancelCalled_) { + consoleWidget.bufferPush(_('Press Ctrl+D or type "exit" to exit the application')); + } else { + this.commandCancelCalled_ = true; + await cmd.cancel(); + this.commandCancelCalled_ = false; + } + return; + } const now = (new Date()).getTime(); @@ -381,24 +425,20 @@ class AppGui { cmd(); } else { consoleWidget.bufferPush(cmd); - consoleWidget.pause(); await this.processCommand(cmd); - consoleWidget.resume(); } } } } }); } catch (error) { + this.fullScreen(false); this.logger().error(error); - term.fullscreen(false); - this.term.showCursor(); console.error(error); } process.on('unhandledRejection', (reason, p) => { - term.fullscreen(false); - this.term.showCursor(); + this.fullScreen(false); console.error('Unhandled promise rejection', p, 'reason:', reason); process.exit(1); }); diff --git a/CliClient/app/app.js b/CliClient/app/app.js index 880acc7b92..3cfd3d9002 100644 --- a/CliClient/app/app.js +++ b/CliClient/app/app.js @@ -278,6 +278,11 @@ class Application { } }); + cmd.setForceRender(async () => { + this.gui_.widget('root').invalidate(); + await this.gui_.renderer().forceRender(); + }); + cmd.setPrompt(async (message, options) => { consoleWidget.focus(); @@ -384,6 +389,7 @@ class Application { this.activeCommand_ = this.findCommandByName(commandName); const cmdArgs = cliUtils.makeCommandArgs(this.activeCommand_, argv); await this.activeCommand_.action(cmdArgs); + this.activeCommand_ = null; } currentCommand() { diff --git a/CliClient/app/base-command.js b/CliClient/app/base-command.js index c4f27ca0f1..c9b8147e4a 100644 --- a/CliClient/app/base-command.js +++ b/CliClient/app/base-command.js @@ -55,6 +55,14 @@ class BaseCommand { if (this.stdout_) this.stdout_(...object); } + setForceRender(fn) { + this.forceRender_ = fn; + } + + async forceRender() { + if (this.forceRender_) await this.forceRender_(); + } + setPrompt(fn) { this.prompt_ = fn; } diff --git a/CliClient/app/cli-utils.js b/CliClient/app/cli-utils.js index f2a6e6128f..e5bb07848e 100644 --- a/CliClient/app/cli-utils.js +++ b/CliClient/app/cli-utils.js @@ -12,7 +12,7 @@ cliUtils.splitCommandString = function(s) { let r = yargParser(s); let output = []; for (let i = 0; i < r._.length; i++) { - let a = r._[i]; + let a = r._[i].toString(); a = a.replace(/__JOP_DASH_JOP_DASH__/g, '--'); a = a.replace(/__JOP_DASH__/g, '-'); output.push(a); @@ -213,11 +213,15 @@ let redrawStarted_ = false; let redrawLastLog_ = null; let redrawLastUpdateTime_ = 0; +cliUtils.setStdout = function(v) { + this.stdout_ = v; +} + cliUtils.redraw = function(s) { const now = time.unixMs(); if (now - redrawLastUpdateTime_ > 4000) { - console.info(s); + this.stdout_ (s); redrawLastUpdateTime_ = now; redrawLastLog_ = null; } else { @@ -231,7 +235,7 @@ cliUtils.redrawDone = function() { if (!redrawStarted_) return; if (redrawLastLog_) { - console.info(redrawLastLog_); + this.stdout_(redrawLastLog_); } redrawLastLog_ = null; diff --git a/CliClient/app/command-edit.js b/CliClient/app/command-edit.js index d6276414ca..0d01340845 100644 --- a/CliClient/app/command-edit.js +++ b/CliClient/app/command-edit.js @@ -1,5 +1,6 @@ import fs from 'fs-extra'; import { BaseCommand } from './base-command.js'; +import { uuid } from 'lib/uuid.js'; import { app } from './app.js'; import { _ } from 'lib/locale.js'; import { Folder } from 'lib/models/folder.js'; @@ -7,6 +8,7 @@ import { Note } from 'lib/models/note.js'; import { Setting } from 'lib/models/setting.js'; import { BaseModel } from 'lib/base-model.js'; import { cliUtils } from './cli-utils.js'; +import { time } from 'lib/time-utils.js'; class Command extends BaseCommand { @@ -20,13 +22,10 @@ class Command extends BaseCommand { async action(args) { let watcher = null; - let newNote = null; + let tempFilePath = null; const onFinishedEditing = async () => { - if (watcher) watcher.close(); - //app().vorpal().show(); - newNote = null; - this.stdout(_('Done editing.')); + if (tempFilePath) fs.removeSync(tempFilePath); } const textEditorPath = () => { @@ -36,6 +35,10 @@ class Command extends BaseCommand { } try { + // ------------------------------------------------------------------------- + // Load note or create it if it doesn't exist + // ------------------------------------------------------------------------- + let title = args['note']; if (!app().currentFolder()) throw new Error(_('No active notebook.')); @@ -44,47 +47,55 @@ class Command extends BaseCommand { if (!note) { const ok = await this.prompt(_('Note does not exist: "%s". Create it?', title)); if (!ok) return; - newNote = await Note.save({ title: title, parent_id: app().currentFolder().id }); - note = await Note.load(newNote.id); + note = await Note.save({ title: title, parent_id: app().currentFolder().id }); + note = await Note.load(note.id); } + // ------------------------------------------------------------------------- + // Create the file to be edited and prepare the editor program arguments + // ------------------------------------------------------------------------- + let editorPath = textEditorPath(); let editorArgs = editorPath.split(' '); editorPath = editorArgs[0]; editorArgs = editorArgs.splice(1); - let content = await Note.serializeForEdit(note); + const originalContent = await Note.serializeForEdit(note); - let tempFilePath = Setting.value('tempDir') + '/' + Note.systemPath(note); + tempFilePath = Setting.value('tempDir') + '/' + uuid.create() + '.md'; editorArgs.push(tempFilePath); - const spawn = require('child_process').spawn; + await fs.writeFile(tempFilePath, originalContent); + + // ------------------------------------------------------------------------- + // Start editing the file + // ------------------------------------------------------------------------- + + this.logger().info('Disabling fullscreen...'); this.stdout(_('Starting to edit note. Close the editor to get back to the prompt.')); + await this.forceRender(); - await fs.writeFile(tempFilePath, content); + const spawnSync = require('child_process').spawnSync; + spawnSync(editorPath, editorArgs, { stdio: 'inherit' }); - let watchTimeout = null; - watcher = fs.watch(tempFilePath, (eventType, filename) => { - // We need a timeout because for each change to the file, multiple events are generated. + await this.forceRender(); - if (watchTimeout) return; + // ------------------------------------------------------------------------- + // Save the note and clean up + // ------------------------------------------------------------------------- - watchTimeout = setTimeout(async () => { - let updatedNote = await fs.readFile(tempFilePath, 'utf8'); - updatedNote = await Note.unserializeForEdit(updatedNote); - updatedNote.id = note.id; - await Note.save(updatedNote); - //process.stdout.write('.'); - watchTimeout = null; - }, 200); - }); + const updatedContent = await fs.readFile(tempFilePath, 'utf8'); + if (updatedContent !== originalContent) { + let updatedNote = await Note.unserializeForEdit(updatedContent); + updatedNote.id = note.id; + await Note.save(updatedNote); + this.logger().info('Note has been saved'); + } + + await onFinishedEditing(); - const childProcess = spawn(editorPath, editorArgs, { stdio: 'inherit' }); - childProcess.on('exit', async (error, code) => { - await onFinishedEditing(); - }); } catch(error) { await onFinishedEditing(); throw error; diff --git a/CliClient/app/command-sync.js b/CliClient/app/command-sync.js index 6eaf9cd735..2ba8f89c90 100644 --- a/CliClient/app/command-sync.js +++ b/CliClient/app/command-sync.js @@ -153,7 +153,7 @@ class Command extends BaseCommand { if (reg.syncHasAuth(target)) { let sync = await reg.synchronizer(target); - if (sync) sync.cancel(); + if (sync) await sync.cancel(); } else { if (this.releaseLockFn_) this.releaseLockFn_(); this.releaseLockFn_ = null; diff --git a/CliClient/app/main.js b/CliClient/app/main.js index 2bd4c6fcc4..4e495d5bf0 100644 --- a/CliClient/app/main.js +++ b/CliClient/app/main.js @@ -49,18 +49,18 @@ if (process.platform === "win32") { }); } -let commandCancelCalled_ = false; +// let commandCancelCalled_ = false; -process.on("SIGINT", async function() { - const cmd = application.currentCommand(); +// process.on("SIGINT", async function() { +// const cmd = application.currentCommand(); - if (!cmd || !cmd.cancellable() || commandCancelCalled_) { - process.exit(0); - } else { - commandCancelCalled_ = true; - await cmd.cancel(); - } -}); +// if (!cmd || !cmd.cancellable() || commandCancelCalled_) { +// process.exit(0); +// } else { +// commandCancelCalled_ = true; +// await cmd.cancel(); +// } +// }); process.stdout.on('error', function( err ) { // https://stackoverflow.com/questions/12329816/error-write-epipe-when-piping-node-output-to-head#15884508 diff --git a/CliClient/locales/en_GB.po b/CliClient/locales/en_GB.po index 7d6df17fe5..44a01e8a26 100644 --- a/CliClient/locales/en_GB.po +++ b/CliClient/locales/en_GB.po @@ -39,6 +39,9 @@ msgstr "" msgid "Maximise/minimise the console" msgstr "" +msgid "Press Ctrl+D or type \"exit\" to exit the application" +msgstr "" + msgid "[Cancel]" msgstr "" @@ -172,9 +175,6 @@ msgstr "" msgid "Edit note." msgstr "" -msgid "Done editing." -msgstr "" - msgid "" "No text editor is defined. Please set it using `config editor `" msgstr "" diff --git a/CliClient/locales/fr_FR.po b/CliClient/locales/fr_FR.po index 99077dfb18..3db1ac88aa 100644 --- a/CliClient/locales/fr_FR.po +++ b/CliClient/locales/fr_FR.po @@ -46,6 +46,9 @@ msgstr "Créer un carnet." msgid "Maximise/minimise the console" msgstr "Quitter le logiciel." +msgid "Press Ctrl+D or type \"exit\" to exit the application" +msgstr "" + #, fuzzy msgid "[Cancel]" msgstr "Annulation..." @@ -189,9 +192,6 @@ msgstr "" msgid "Edit note." msgstr "Editer la note." -msgid "Done editing." -msgstr "Edition terminée." - msgid "" "No text editor is defined. Please set it using `config editor `" msgstr "" @@ -744,6 +744,9 @@ msgstr "" msgid "Welcome" msgstr "Bienvenue" +#~ msgid "Done editing." +#~ msgstr "Edition terminée." + #, fuzzy #~ msgid "Confirm" #~ msgstr "Conflits" diff --git a/CliClient/locales/joplin.pot b/CliClient/locales/joplin.pot index 7d6df17fe5..44a01e8a26 100644 --- a/CliClient/locales/joplin.pot +++ b/CliClient/locales/joplin.pot @@ -39,6 +39,9 @@ msgstr "" msgid "Maximise/minimise the console" msgstr "" +msgid "Press Ctrl+D or type \"exit\" to exit the application" +msgstr "" + msgid "[Cancel]" msgstr "" @@ -172,9 +175,6 @@ msgstr "" msgid "Edit note." msgstr "" -msgid "Done editing." -msgstr "" - msgid "" "No text editor is defined. Please set it using `config editor `" msgstr "" diff --git a/ReactNativeClient/lib/synchronizer.js b/ReactNativeClient/lib/synchronizer.js index 5410c1e23a..83f1d900f8 100644 --- a/ReactNativeClient/lib/synchronizer.js +++ b/ReactNativeClient/lib/synchronizer.js @@ -136,11 +136,20 @@ class Synchronizer { return false; } - cancel() { + async cancel() { if (this.cancelling_ || this.state() == 'idle') return; this.logSyncOperation('cancelling', null, null, ''); this.cancelling_ = true; + + return new Promise((resolve, reject) => { + const iid = setInterval(() => { + if (this.state() == 'idle') { + clearInterval(iid); + resolve(); + } + }, 100); + }); } cancelling() {