From 1f91d0dfdbe30eab63af129698b3e957341e8b8a Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Fri, 4 Aug 2017 18:50:12 +0200 Subject: [PATCH] Handle ctrl+c --- CliClient/app/app.js | 20 +++++-- CliClient/app/cli-utils.js | 53 ++++++++++++++++- CliClient/app/command-edit.js | 6 +- CliClient/app/command-import-enex.js | 13 ++-- CliClient/app/command-rm.js | 5 +- CliClient/app/command-sync.js | 25 ++++++-- CliClient/app/main.js | 20 ++++++- CliClient/build-translation.sh | 3 + CliClient/locales/en_GB.po | 60 ++++++++++++------- CliClient/locales/fr_FR.po | 89 +++++++++++++++++++--------- CliClient/locales/joplin.pot | 60 ++++++++++++------- 11 files changed, 256 insertions(+), 98 deletions(-) create mode 100644 CliClient/build-translation.sh diff --git a/CliClient/app/app.js b/CliClient/app/app.js index cbd4af4e9d..614d5b1a5d 100644 --- a/CliClient/app/app.js +++ b/CliClient/app/app.js @@ -26,6 +26,7 @@ class Application { this.autocompletion_ = { active: false }; this.commands_ = {}; this.commandMetadata_ = null; + this.activeCommand_ = null; } currentFolder() { @@ -235,7 +236,7 @@ class Application { case 'FOLDERS_UPDATE_ONE': case 'FOLDER_DELETE': - reg.scheduleSync(); + //reg.scheduleSync(); break; } @@ -303,9 +304,13 @@ class Application { async execCommand(argv) { if (!argv.length) throw new Error('Empty command'); const commandName = argv[0]; - const command = this.findCommandByName(commandName); - const cmdArgs = cliUtils.makeCommandArgs(command, argv); - await command.action(cmdArgs); + this.activeCommand_ = this.findCommandByName(commandName); + const cmdArgs = cliUtils.makeCommandArgs(this.activeCommand_, argv); + await this.activeCommand_.action(cmdArgs); + } + + async cancelCurrentCommand() { + await this.activeCommand_.cancel(); } async start() { @@ -388,14 +393,17 @@ class Application { for (let i = 0; i < items.length; i++) { items[i] = items[i].replace(/ /g, '\\ '); } - //console.info(items); console.info(items.join("\n")); } return; } - this.execCommand(argv); + try { + await this.execCommand(argv); + } catch (error) { + console.info(error); + } } } diff --git a/CliClient/app/cli-utils.js b/CliClient/app/cli-utils.js index 66816ba158..6781dadf8d 100644 --- a/CliClient/app/cli-utils.js +++ b/CliClient/app/cli-utils.js @@ -1,4 +1,6 @@ import yargParser from 'yargs-parser'; +import { _ } from 'lib/locale.js'; +import { time } from 'lib/time-utils.js'; const stringPadding = require('string-padding'); const cliUtils = {}; @@ -104,7 +106,7 @@ cliUtils.makeCommandArgs = function(cmd, argv) { for (let i = 1; i < cmdUsage['_'].length; i++) { const a = cliUtils.parseCommandArg(cmdUsage['_'][i]); - if (a.required && !args['_'][i]) throw new Error('Missing required arg: ' + a.name); + if (a.required && !args['_'][i]) throw new Error(_('Missing required argument: %s', a.name)); if (i >= a.length) { output[a.name] = null; } else { @@ -124,4 +126,53 @@ cliUtils.makeCommandArgs = function(cmd, argv) { return output; } +cliUtils.promptConfirm = function(message, answers = null) { + if (!answers) answers = [_('Y'), _('n')]; + const readline = require('readline'); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + message += ' (' + answers.join('/') + ')'; + + return new Promise((resolve, reject) => { + rl.question(message + ' ', (answer) => { + const ok = !answer || answer.toLowerCase() == answers[0].toLowerCase(); + rl.close(); + resolve(ok); + }); + }); +} + +let redrawStarted_ = false; +let redrawLastLog_ = null; +let redrawLastUpdateTime_ = 0; + +cliUtils.redraw = function(s) { + const now = time.unixMs(); + + if (now - redrawLastUpdateTime_ > 4000) { + console.info(s); + redrawLastUpdateTime_ = now; + redrawLastLog_ = null; + } else { + redrawLastLog_ = s; + } + + redrawStarted_ = true; +} + +cliUtils.redrawDone = function() { + if (!redrawStarted_) return; + + if (redrawLastLog_) { + console.info(redrawLastLog_); + } + + redrawLastLog_ = null; + redrawStarted_ = false; +} + export { cliUtils }; \ No newline at end of file diff --git a/CliClient/app/command-edit.js b/CliClient/app/command-edit.js index fc416c8616..13f4bcc4a9 100644 --- a/CliClient/app/command-edit.js +++ b/CliClient/app/command-edit.js @@ -7,6 +7,7 @@ import { Note } from 'lib/models/note.js'; import { Setting } from 'lib/models/setting.js'; import { BaseModel } from 'lib/base-model.js'; import { autocompleteItems } from './autocomplete.js'; +import { cliUtils } from './cli-utils.js'; class Command extends BaseCommand { @@ -46,10 +47,7 @@ class Command extends BaseCommand { let note = await app().loadItem(BaseModel.TYPE_NOTE, title); if (!note) { - // TODO - throw new Error(_('Note does not exist.')); - - let ok = await vorpalUtils.cmdPromptConfirm(this, _('Note does not exist: "%s". Create it?', title)) + const ok = await cliUtils.promptConfirm(_('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); diff --git a/CliClient/app/command-import-enex.js b/CliClient/app/command-import-enex.js index a3264bdb3d..2665b905a2 100644 --- a/CliClient/app/command-import-enex.js +++ b/CliClient/app/command-import-enex.js @@ -4,6 +4,7 @@ import { _ } from 'lib/locale.js'; import { Folder } from 'lib/models/folder.js'; import { importEnex } from 'import-enex'; import { filename, basename } from 'lib/path-utils.js'; +import { cliUtils } from './cli-utils.js'; class Command extends BaseCommand { @@ -28,16 +29,10 @@ class Command extends BaseCommand { let folderTitle = args['notebook']; let force = args.options.force === true; - - - force = true; // TODO - - if (!folderTitle) folderTitle = filename(filePath); folder = await Folder.loadByField('title', folderTitle); const msg = folder ? _('File "%s" will be imported into existing notebook "%s". Continue?', basename(filePath), folderTitle) : _('New notebook "%s" will be created and file "%s" will be imported into it. Continue?', folderTitle, basename(filePath)); - const ok = force ? true : await vorpalUtils.cmdPromptConfirm(this, msg); - + const ok = force ? true : await cliUtils.promptConfirm(msg); if (!ok) return; let options = { @@ -50,8 +45,7 @@ class Command extends BaseCommand { if (progressState.skipped) line.push(_('Skipped: %d.', progressState.skipped)); if (progressState.resourcesCreated) line.push(_('Resources: %d.', progressState.resourcesCreated)); if (progressState.notesTagged) line.push(_('Tagged: %d.', progressState.notesTagged)); - this.log(line.join(' ')); // TODO - //vorpalUtils.redraw(line.join(' ')); + cliUtils.redraw(line.join(' ')); }, onError: (error) => { let s = error.trace ? error.trace : error.toString(); @@ -62,6 +56,7 @@ class Command extends BaseCommand { folder = !folder ? await Folder.save({ title: folderTitle }) : folder; this.log(_('Importing notes...')); await importEnex(folder.id, filePath, options); + cliUtils.redrawDone(); } } diff --git a/CliClient/app/command-rm.js b/CliClient/app/command-rm.js index b3f6af50d8..5f491cfd9f 100644 --- a/CliClient/app/command-rm.js +++ b/CliClient/app/command-rm.js @@ -6,6 +6,7 @@ import { Folder } from 'lib/models/folder.js'; import { Note } from 'lib/models/note.js'; import { BaseModel } from 'lib/base-model.js'; import { autocompleteItems } from './autocomplete.js'; +import { cliUtils } from './cli-utils.js'; class Command extends BaseCommand { @@ -30,7 +31,7 @@ class Command extends BaseCommand { async action(args) { const pattern = args['note-pattern']; const recursive = args.options && args.options.recursive === true; - const force = true || args.options && args.options.force === true; // TODO + const force = args.options && args.options.force === true; // if (recursive) { // const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern); @@ -43,7 +44,7 @@ class Command extends BaseCommand { const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern); if (!notes.length) throw new Error(_('Cannot find "%s".', pattern)); - const ok = force ? true : await vorpalUtils.cmdPromptConfirm(this, _('%d notes match this pattern. Delete them?', notes.length)); + const ok = force ? true : await cliUtils.promptConfirm(_('%d notes match this pattern. Delete them?', notes.length)); if (!ok) return; let ids = notes.map((n) => n.id); await Note.batchDelete(ids); diff --git a/CliClient/app/command-sync.js b/CliClient/app/command-sync.js index a7f9e4b8f3..f6c90f629c 100644 --- a/CliClient/app/command-sync.js +++ b/CliClient/app/command-sync.js @@ -6,6 +6,7 @@ import { Setting } from 'lib/models/setting.js'; import { BaseItem } from 'lib/models/base-item.js'; import { Synchronizer } from 'lib/synchronizer.js'; import { reg } from 'lib/registry.js'; +import { cliUtils } from './cli-utils.js'; import md5 from 'md5'; const locker = require('proper-lockfile'); const fs = require('fs-extra'); @@ -67,9 +68,18 @@ class Command extends BaseCommand { const lockFilePath = osTmpdir() + '/synclock_' + md5(Setting.value('profileDir')); if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock'); - if (await Command.isLocked(lockFilePath)) throw new Error(_('Synchronisation is already in progress.')); + try { + if (await Command.isLocked(lockFilePath)) throw new Error(_('Synchronisation is already in progress.')); - this.releaseLockFn_ = await Command.lockFile(lockFilePath); + this.releaseLockFn_ = await Command.lockFile(lockFilePath); + } catch (error) { + if (error.code == 'ELOCKED') { + const msg = _('Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at "%s" and resume the operation.', error.file); + this.log(msg); + return; + } + throw error; + } try { this.syncTarget_ = Setting.value('sync.target'); @@ -87,11 +97,10 @@ class Command extends BaseCommand { let options = { onProgress: (report) => { let lines = Synchronizer.reportToLines(report); - //if (lines.length) vorpalUtils.redraw(lines.join(' ')); - if (lines.length) this.log(lines.join(' ')); // TODO + if (lines.length) cliUtils.redraw(lines.join(' ')); }, onMessage: (msg) => { - vorpalUtils.redrawDone(); + cliUtils.redrawDone(); this.log(msg); }, randomFailures: args.options['random-failures'] === true, @@ -122,11 +131,13 @@ class Command extends BaseCommand { this.log(_('Done.')); } catch (error) { + cliUtils.redrawDone(); this.releaseLockFn_(); this.releaseLockFn_ = null; throw error; } + cliUtils.redrawDone(); this.releaseLockFn_(); this.releaseLockFn_ = null; } @@ -134,7 +145,9 @@ class Command extends BaseCommand { async cancel() { const target = this.syncTarget_ ? this.syncTarget_ : Setting.value('sync.target'); - this.log(_('Cancelling...')); + cliUtils.redrawDone(); + + this.log(_('Cancelling... Please wait.')); if (reg.syncHasAuth(target)) { let sync = await reg.synchronizer(target); diff --git a/CliClient/app/main.js b/CliClient/app/main.js index e282c8692c..2429e90e32 100644 --- a/CliClient/app/main.js +++ b/CliClient/app/main.js @@ -40,7 +40,25 @@ Setting.setConstant('appType', 'cli'); shimInit(); -app().start().catch((error) => { +const application = app(); + +if (process.platform === "win32") { + var rl = require("readline").createInterface({ + input: process.stdin, + output: process.stdout + }); + + rl.on("SIGINT", function () { + process.emit("SIGINT"); + }); +} + +process.on("SIGINT", async function() { + console.info(_('Received %s', 'SIGINT')); + await application.cancelCurrentCommand(); +}); + +application.start().catch((error) => { console.error(_('Fatal error:')); console.error(error); }); \ No newline at end of file diff --git a/CliClient/build-translation.sh b/CliClient/build-translation.sh new file mode 100644 index 0000000000..11a722a3b3 --- /dev/null +++ b/CliClient/build-translation.sh @@ -0,0 +1,3 @@ +#/bin/bash +CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +NODE_PATH="$CLIENT_DIR/build" node "$CLIENT_DIR/build/build-translation.js" --silent \ No newline at end of file diff --git a/CliClient/locales/en_GB.po b/CliClient/locales/en_GB.po index 3eecdc7e48..25df890821 100644 --- a/CliClient/locales/en_GB.po +++ b/CliClient/locales/en_GB.po @@ -41,6 +41,33 @@ msgstr "" msgid "Exits the application." msgstr "" +msgid "Only Bash is currently supported for autocompletion." +msgstr "" + +#, javascript-format +msgid "Adding autocompletion script to: \"%s\"" +msgstr "" + +msgid "Autocompletion script is already installed." +msgstr "" + +msgid "Autocompletion has been installed." +msgstr "" + +#, javascript-format +msgid "Sourcing \"%s\"..." +msgstr "" + +#, javascript-format +msgid "Missing required argument: %s" +msgstr "" + +msgid "Y" +msgstr "" + +msgid "n" +msgstr "" + msgid "Displays the given note." msgstr "" @@ -69,7 +96,7 @@ msgid "%s = %s" msgstr "" msgid "" -"Duplicates the notes matching to [notebook]. If no notebook is " +"Duplicates the notes matching to [notebook]. If no notebook is " "specified the note is duplicated in the current notebook." msgstr "" @@ -86,9 +113,6 @@ msgstr "" msgid "No active notebook." msgstr "" -msgid "Note does not exist." -msgstr "" - #, javascript-format msgid "Note does not exist: \"%s\". Create it?" msgstr "" @@ -143,8 +167,8 @@ msgid "Importing notes..." msgstr "" msgid "" -"Displays the notes in [notebook]. Use `ls /` to display the list of " -"notebooks." +"Displays the notes in the current notebook. Use `ls /` to display the list " +"of notebooks." msgstr "" msgid "Displays only the first top notes." @@ -185,21 +209,15 @@ msgstr "" msgid "Creates a new todo." msgstr "" -msgid "" -"Moves the notes matching to . If is a note, " -"it will be moved to the notebook . If is a notebook, " -"it will be renamed to ." +msgid "Moves the notes matching to [notebook]." msgstr "" -msgid "Deletes the items matching ." +msgid "Deletes the notes matching ." msgstr "" msgid "Deletes the items without asking for confirmation." msgstr "" -msgid "Deletes a notebook." -msgstr "" - #, javascript-format msgid "%d notes match this pattern. Delete them?" msgstr "" @@ -239,9 +257,9 @@ msgid "Cancelling..." msgstr "" msgid "" -" can be \"add\", \"remove\" or \"list\" to assign or remove [tag] " -"from [note], or to list the notes associated with [tag]. The command `tag " -"list` can be used to list all the tags." +" can be \"add\", \"remove\" or \"list\" to assign or remove " +"[tag] from [note], or to list the notes associated with [tag]. The command " +"`tag list` can be used to list all the tags." msgstr "" #, javascript-format @@ -249,10 +267,10 @@ msgid "Invalid command: \"%s\"" msgstr "" msgid "" -" can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the " -"given todo between completed and uncompleted state (If the target is a " -"regular note it will be converted to a todo). Use \"clear\" to convert the " -"todo back to a regular note." +" can either be \"toggle\" or \"clear\". Use \"toggle\" to " +"toggle the given todo between completed and uncompleted state (If the target " +"is a regular note it will be converted to a todo). Use \"clear\" to convert " +"the todo back to a regular note." msgstr "" msgid "" diff --git a/CliClient/locales/fr_FR.po b/CliClient/locales/fr_FR.po index 49de6afae0..961c6ad222 100644 --- a/CliClient/locales/fr_FR.po +++ b/CliClient/locales/fr_FR.po @@ -43,6 +43,33 @@ msgstr "Affiche l'aide pour la commande donnée." msgid "Exits the application." msgstr "Quitter le logiciel." +msgid "Only Bash is currently supported for autocompletion." +msgstr "" + +#, javascript-format +msgid "Adding autocompletion script to: \"%s\"" +msgstr "" + +msgid "Autocompletion script is already installed." +msgstr "" + +msgid "Autocompletion has been installed." +msgstr "" + +#, javascript-format +msgid "Sourcing \"%s\"..." +msgstr "" + +#, javascript-format +msgid "Missing required argument: %s" +msgstr "" + +msgid "Y" +msgstr "" + +msgid "n" +msgstr "" + msgid "Displays the given note." msgstr "Affiche la note." @@ -73,8 +100,9 @@ msgstr "%s = %s (%s)" msgid "%s = %s" msgstr "%s = %s" +#, fuzzy msgid "" -"Duplicates the notes matching to [notebook]. If no notebook is " +"Duplicates the notes matching to [notebook]. If no notebook is " "specified the note is duplicated in the current notebook." msgstr "" "Copie les notes correspondant à [nom] vers [carnet]. Si aucun carnet n'est " @@ -95,13 +123,9 @@ msgstr "" msgid "No active notebook." msgstr "Aucun carnet actif." -#, fuzzy -msgid "Note does not exist." -msgstr "Ce carnet n'existe pas : \"%s\". Le créer ?" - #, javascript-format msgid "Note does not exist: \"%s\". Create it?" -msgstr "Ce carnet n'existe pas : \"%s\". Le créer ?" +msgstr "Cette note n'existe pas : \"%s\". La créer ?" msgid "Starting to edit note. Close the editor to get back to the prompt." msgstr "" @@ -157,9 +181,10 @@ msgstr "Etiquettes : %d." msgid "Importing notes..." msgstr "Importation des notes..." +#, fuzzy msgid "" -"Displays the notes in [notebook]. Use `ls /` to display the list of " -"notebooks." +"Displays the notes in the current notebook. Use `ls /` to display the list " +"of notebooks." msgstr "" "Affiche les notes dans le carnet. Utilisez `ls /` pour afficher la liste des " "carnets." @@ -208,24 +233,17 @@ msgstr "Les notes ne peuvent être créées que dans un carnet." msgid "Creates a new todo." msgstr "Créer une nouvelle tâche." -msgid "" -"Moves the notes matching to . If is a note, " -"it will be moved to the notebook . If is a notebook, " -"it will be renamed to ." -msgstr "" -"Déplacer les notes correspondantes à vers . Si " -"est une note, elle sera déplacée vers le carnet . Si " -"est un carnet, il sera renommé ." +#, fuzzy +msgid "Moves the notes matching to [notebook]." +msgstr "Supprime les objets correspondants à ." -msgid "Deletes the items matching ." +#, fuzzy +msgid "Deletes the notes matching ." msgstr "Supprime les objets correspondants à ." msgid "Deletes the items without asking for confirmation." msgstr "Supprime les objets sans demander la confirmation." -msgid "Deletes a notebook." -msgstr "Supprime le carnet." - #, javascript-format msgid "%d notes match this pattern. Delete them?" msgstr "%d notes correspondent à ce motif. Les supprimer ?" @@ -266,10 +284,11 @@ msgstr "Terminé." msgid "Cancelling..." msgstr "Annulation..." +#, fuzzy msgid "" -" can be \"add\", \"remove\" or \"list\" to assign or remove [tag] " -"from [note], or to list the notes associated with [tag]. The command `tag " -"list` can be used to list all the tags." +" can be \"add\", \"remove\" or \"list\" to assign or remove " +"[tag] from [note], or to list the notes associated with [tag]. The command " +"`tag list` can be used to list all the tags." msgstr "" " peut être \"add\", \"remove\" ou \"list\" pour assigner ou enlever " "l'étiquette [tag] de la [note], our pour lister les notes associées avec " @@ -280,11 +299,12 @@ msgstr "" msgid "Invalid command: \"%s\"" msgstr "Commande invalide : \"%s\"" +#, fuzzy msgid "" -" can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the " -"given todo between completed and uncompleted state (If the target is a " -"regular note it will be converted to a todo). Use \"clear\" to convert the " -"todo back to a regular note." +" can either be \"toggle\" or \"clear\". Use \"toggle\" to " +"toggle the given todo between completed and uncompleted state (If the target " +"is a regular note it will be converted to a todo). Use \"clear\" to convert " +"the todo back to a regular note." msgstr "" "Gère le status des tâches. peut être \"toggle\" ou \"clear\". " "Utilisez \"toggle\" pour basculer la tâche entre le status terminé et non-" @@ -579,6 +599,21 @@ msgstr "" msgid "Welcome" msgstr "Bienvenue" +#~ msgid "Note does not exist." +#~ msgstr "Cette note n'existe pas." + +#~ msgid "" +#~ "Moves the notes matching to . If is a " +#~ "note, it will be moved to the notebook . If is a " +#~ "notebook, it will be renamed to ." +#~ msgstr "" +#~ "Déplacer les notes correspondantes à vers . Si " +#~ " est une note, elle sera déplacée vers le carnet . Si " +#~ " est un carnet, il sera renommé ." + +#~ msgid "Deletes a notebook." +#~ msgstr "Supprime le carnet." + #~ msgid "Delete notebook \"%s\"?" #~ msgstr "Supprimer le carnet \"%s\" ?" diff --git a/CliClient/locales/joplin.pot b/CliClient/locales/joplin.pot index 3eecdc7e48..25df890821 100644 --- a/CliClient/locales/joplin.pot +++ b/CliClient/locales/joplin.pot @@ -41,6 +41,33 @@ msgstr "" msgid "Exits the application." msgstr "" +msgid "Only Bash is currently supported for autocompletion." +msgstr "" + +#, javascript-format +msgid "Adding autocompletion script to: \"%s\"" +msgstr "" + +msgid "Autocompletion script is already installed." +msgstr "" + +msgid "Autocompletion has been installed." +msgstr "" + +#, javascript-format +msgid "Sourcing \"%s\"..." +msgstr "" + +#, javascript-format +msgid "Missing required argument: %s" +msgstr "" + +msgid "Y" +msgstr "" + +msgid "n" +msgstr "" + msgid "Displays the given note." msgstr "" @@ -69,7 +96,7 @@ msgid "%s = %s" msgstr "" msgid "" -"Duplicates the notes matching to [notebook]. If no notebook is " +"Duplicates the notes matching to [notebook]. If no notebook is " "specified the note is duplicated in the current notebook." msgstr "" @@ -86,9 +113,6 @@ msgstr "" msgid "No active notebook." msgstr "" -msgid "Note does not exist." -msgstr "" - #, javascript-format msgid "Note does not exist: \"%s\". Create it?" msgstr "" @@ -143,8 +167,8 @@ msgid "Importing notes..." msgstr "" msgid "" -"Displays the notes in [notebook]. Use `ls /` to display the list of " -"notebooks." +"Displays the notes in the current notebook. Use `ls /` to display the list " +"of notebooks." msgstr "" msgid "Displays only the first top notes." @@ -185,21 +209,15 @@ msgstr "" msgid "Creates a new todo." msgstr "" -msgid "" -"Moves the notes matching to . If is a note, " -"it will be moved to the notebook . If is a notebook, " -"it will be renamed to ." +msgid "Moves the notes matching to [notebook]." msgstr "" -msgid "Deletes the items matching ." +msgid "Deletes the notes matching ." msgstr "" msgid "Deletes the items without asking for confirmation." msgstr "" -msgid "Deletes a notebook." -msgstr "" - #, javascript-format msgid "%d notes match this pattern. Delete them?" msgstr "" @@ -239,9 +257,9 @@ msgid "Cancelling..." msgstr "" msgid "" -" can be \"add\", \"remove\" or \"list\" to assign or remove [tag] " -"from [note], or to list the notes associated with [tag]. The command `tag " -"list` can be used to list all the tags." +" can be \"add\", \"remove\" or \"list\" to assign or remove " +"[tag] from [note], or to list the notes associated with [tag]. The command " +"`tag list` can be used to list all the tags." msgstr "" #, javascript-format @@ -249,10 +267,10 @@ msgid "Invalid command: \"%s\"" msgstr "" msgid "" -" can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the " -"given todo between completed and uncompleted state (If the target is a " -"regular note it will be converted to a todo). Use \"clear\" to convert the " -"todo back to a regular note." +" can either be \"toggle\" or \"clear\". Use \"toggle\" to " +"toggle the given todo between completed and uncompleted state (If the target " +"is a regular note it will be converted to a todo). Use \"clear\" to convert " +"the todo back to a regular note." msgstr "" msgid ""