1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Merge branch 'master' into alarm-support

This commit is contained in:
Laurent Cozic 2017-11-24 17:09:24 +00:00
commit 60d2b0c763
379 changed files with 30742 additions and 10859 deletions

5
.gitignore vendored
View File

@ -32,4 +32,7 @@ INFO.md
sync_staging.sh
*.swp
_vieux/
README.md
_mydocs
.DS_Store
Assets/DownloadBadges*.psd
node_modules

49
.travis.yml Normal file
View File

@ -0,0 +1,49 @@
rvm: 2.3.3
matrix:
include:
- os: osx
osx_image: xcode9.0
language: node_js
node_js: "8"
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
- os: linux
sudo: required
dist: trusty
language: node_js
node_js: "8"
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
# cache:
# directories:
# - node_modules
# - $HOME/.cache/electron
# - $HOME/.cache/electron-builder
before_install:
# HOMEBREW_NO_AUTO_UPDATE needed so that Homebrew doesn't upgrade to the next
# version, which requires Ruby 2.3, which is not available on the Travis VM.
# Silence apt-get update errors (for example when a module doesn't exist) since
# otherwise it will make the whole build fails, even though all we need is yarn.
- |
if [ "$TRAVIS_OS_NAME" == "osx" ]; then
HOMEBREW_NO_AUTO_UPDATE=1 brew install yarn
else
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update || true
sudo apt-get install -y yarn
fi
script:
- |
cd ElectronClient/app
rsync -aP ../../ReactNativeClient/lib/ lib/
npm install
yarn dist

BIN
Assets/All.psd Normal file

Binary file not shown.

Binary file not shown.

BIN
Assets/DemoDesktop.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
Assets/DemoDesktop.psd Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
Assets/Icon-Android-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
Assets/Icon-Android.psd Normal file

Binary file not shown.

BIN
Assets/Icon-ios-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
Assets/Icon-ios.psd Normal file

Binary file not shown.

BIN
Assets/Joplin.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
Assets/Laptop-Terminal.psd Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
Assets/LinuxIcons/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

BIN
Assets/LinuxIcons/24x24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
Assets/LinuxIcons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
Assets/LinuxIcons/48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
Assets/LinuxIcons/72x72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
Assets/LinuxIcons/96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 151 KiB

View File

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

BIN
Assets/Tablet.psd Normal file

Binary file not shown.

View File

@ -0,0 +1,2 @@
#!/bin/bash
iconutil --convert icns macOs.iconset

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1472 930v318q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q63 0 117 25 15 7 18 23 3 17-9 29l-49 49q-10 10-23 10-3 0-9-2-23-6-45-6h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-254q0-13 9-22l64-64q10-10 23-10 6 0 12 3 20 8 20 29zm231-489l-814 814q-24 24-57 24t-57-24l-430-430q-24-24-24-57t24-57l110-110q24-24 57-24t57 24l263 263 647-647q24-24 57-24t57 24l110 110q24 24 24 57t-24 57z"/></svg>

After

Width:  |  Height:  |  Size: 612 B

2
Assets/check-square.svg Normal file
View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M813 1299l614-614q19-19 19-45t-19-45l-102-102q-19-19-45-19t-45 19l-467 467-211-211q-19-19-45-19t-45 19l-102 102q-19 19-19 45t19 45l358 358q19 19 45 19t45-19zm851-883v960q0 119-84.5 203.5t-203.5 84.5h-960q-119 0-203.5-84.5t-84.5-203.5v-960q0-119 84.5-203.5t203.5-84.5h960q119 0 203.5 84.5t84.5 203.5z"/></svg>

After

Width:  |  Height:  |  Size: 447 B

BIN
Assets/iOSIcons/29.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
Assets/iOSIcons/29x2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
Assets/iOSIcons/29x3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
Assets/iOSIcons/40.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
Assets/iOSIcons/40x2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
Assets/iOSIcons/40x3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
Assets/iOSIcons/57.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
Assets/iOSIcons/57x2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
Assets/iOSIcons/60x2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
Assets/iOSIcons/60x3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
Assets/macOs.icns Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

2
Assets/square-o.svg Normal file
View File

@ -0,0 +1,2 @@
<?xml version='1.0' encoding='utf-8'?>
<svg viewBox='0 0 1792 1792' xmlns='http://www.w3.org/2000/svg'><path d='M1312 256h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-832q0-66-47-113t-113-47zm288 160v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z'/></svg>

After

Width:  |  Height:  |  Size: 370 B

View File

@ -1,4 +0,0 @@
{
"presets": ["env", "react"],
"plugins": ["syntax-async-functions","transform-runtime"]
}

View File

@ -17,3 +17,5 @@ tests/cli-integration/
*.*~
tests/sync
out.txt
linkToLocal.sh
yarn-error.log

View File

@ -0,0 +1,97 @@
const { _ } = require('lib/locale.js');
const { Logger } = require('lib/logger.js');
const { Resource } = require('lib/models/resource.js');
const { netUtils } = require('lib/net-utils.js');
const http = require("http");
const urlParser = require("url");
const enableServerDestroy = require('server-destroy');
class ResourceServer {
constructor() {
this.server_ = null;
this.logger_ = new Logger();
this.port_ = null;
this.linkHandler_ = null;
this.started_ = false;
}
setLogger(logger) {
this.logger_ = logger;
}
logger() {
return this.logger_;
}
started() {
return this.started_;
}
baseUrl() {
if (!this.port_) return '';
return 'http://127.0.0.1:' + this.port_;
}
setLinkHandler(handler) {
this.linkHandler_ = handler;
}
async start() {
this.port_ = await netUtils.findAvailablePort([9167, 9267, 8167, 8267]);
if (!this.port_) {
this.logger().error('Could not find available port to start resource server. Please report the error at https://github.com/laurent22/joplin');
return;
}
this.server_ = http.createServer();
this.server_.on('request', async (request, response) => {
const writeResponse = (message) => {
response.write(message);
response.end();
}
const url = urlParser.parse(request.url, true);
let resourceId = url.pathname.split('/');
if (resourceId.length < 2) {
writeResponse('Error: could not get resource ID from path name: ' + url.pathname);
return;
}
resourceId = resourceId[1];
if (!this.linkHandler_) throw new Error('No link handler is defined');
try {
const done = await this.linkHandler_(resourceId, response);
if (!done) throw new Error('Unhandled resource: ' + resourceId);
} catch (error) {
response.setHeader('Content-Type', 'text/plain');
response.statusCode = 400;
response.write(error.message);
}
response.end();
});
this.server_.on('error', (error) => {
this.logger().error('Resource server:', error);
});
this.server_.listen(this.port_);
enableServerDestroy(this.server_);
this.started_ = true;
}
stop() {
if (this.server_) this.server_.destroy();
this.server_ = null;
}
}
module.exports = ResourceServer;

819
CliClient/app/app-gui.js Normal file
View File

@ -0,0 +1,819 @@
const { Logger } = require('lib/logger.js');
const { Folder } = require('lib/models/folder.js');
const { Tag } = require('lib/models/tag.js');
const { BaseModel } = require('lib/base-model.js');
const { Note } = require('lib/models/note.js');
const { Resource } = require('lib/models/resource.js');
const { cliUtils } = require('./cli-utils.js');
const { reducer, defaultState } = require('lib/reducer.js');
const { splitCommandString } = require('lib/string-utils.js');
const { reg } = require('lib/registry.js');
const { _ } = require('lib/locale.js');
const chalk = require('chalk');
const tk = require('terminal-kit');
const TermWrapper = require('tkwidgets/framework/TermWrapper.js');
const Renderer = require('tkwidgets/framework/Renderer.js');
const BaseWidget = require('tkwidgets/BaseWidget.js');
const ListWidget = require('tkwidgets/ListWidget.js');
const TextWidget = require('tkwidgets/TextWidget.js');
const HLayoutWidget = require('tkwidgets/HLayoutWidget.js');
const VLayoutWidget = require('tkwidgets/VLayoutWidget.js');
const ReduxRootWidget = require('tkwidgets/ReduxRootWidget.js');
const RootWidget = require('tkwidgets/RootWidget.js');
const WindowWidget = require('tkwidgets/WindowWidget.js');
const NoteWidget = require('./gui/NoteWidget.js');
const ResourceServer = require('./ResourceServer.js');
const NoteMetadataWidget = require('./gui/NoteMetadataWidget.js');
const FolderListWidget = require('./gui/FolderListWidget.js');
const NoteListWidget = require('./gui/NoteListWidget.js');
const StatusBarWidget = require('./gui/StatusBarWidget.js');
const ConsoleWidget = require('./gui/ConsoleWidget.js');
class AppGui {
constructor(app, store) {
this.app_ = app;
this.store_ = store;
BaseWidget.setLogger(app.logger());
this.term_ = new TermWrapper(tk.terminal);
this.renderer_ = null;
this.logger_ = new Logger();
this.buildUi();
this.renderer_ = new Renderer(this.term(), this.rootWidget_);
this.app_.on('modelAction', async (event) => {
await this.handleModelAction(event.action);
});
this.shortcuts_ = this.setupShortcuts();
this.inputMode_ = AppGui.INPUT_MODE_NORMAL;
this.commandCancelCalled_ = false;
this.currentShortcutKeys_ = [];
this.lastShortcutKeyTime_ = 0;
// Recurrent sync is setup only when the GUI is started. In
// a regular command it's not necessary since the process
// exits right away.
reg.setupRecurrentSync();
}
store() {
return this.store_;
}
renderer() {
return this.renderer_;
}
async forceRender() {
this.widget('root').invalidate();
await this.renderer_.renderRoot();
}
prompt(initialText = '', promptString = ':') {
return this.widget('statusBar').prompt(initialText, promptString);
}
stdoutMaxWidth() {
return this.widget('console').innerWidth - 1;
}
isDummy() {
return false;
}
buildUi() {
this.rootWidget_ = new ReduxRootWidget(this.store_);
this.rootWidget_.name = 'root';
const folderList = new FolderListWidget();
folderList.style = {
borderBottomWidth: 1,
borderRightWidth: 1,
};
folderList.name = 'folderList';
folderList.vStretch = true;
folderList.on('currentItemChange', async (event) => {
const item = folderList.currentItem;
if (item === '-') {
let newIndex = event.currentIndex + (event.previousIndex < event.currentIndex ? +1 : -1);
let nextItem = folderList.itemAt(newIndex);
if (!nextItem) nextItem = folderList.itemAt(event.previousIndex);
if (!nextItem) return; // Normally not possible
let actionType = 'FOLDER_SELECT';
if (nextItem.type_ === BaseModel.TYPE_TAG) actionType = 'TAG_SELECT';
if (nextItem.type_ === BaseModel.TYPE_SEARCH) actionType = 'SEARCH_SELECT';
this.store_.dispatch({
type: actionType,
id: nextItem.id,
});
} else if (item.type_ === Folder.modelType()) {
this.store_.dispatch({
type: 'FOLDER_SELECT',
id: item ? item.id : null,
});
} else if (item.type_ === Tag.modelType()) {
this.store_.dispatch({
type: 'TAG_SELECT',
id: item ? item.id : null,
});
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
this.store_.dispatch({
type: 'SEARCH_SELECT',
id: item ? item.id : null,
});
}
});
this.rootWidget_.connect(folderList, (state) => {
return {
selectedFolderId: state.selectedFolderId,
selectedTagId: state.selectedTagId,
selectedSearchId: state.selectedSearchId,
notesParentType: state.notesParentType,
folders: state.folders,
tags: state.tags,
searches: state.searches,
};
});
const noteList = new NoteListWidget();
noteList.name = 'noteList';
noteList.vStretch = true;
noteList.style = {
borderBottomWidth: 1,
borderLeftWidth: 1,
borderRightWidth: 1,
};
noteList.on('currentItemChange', async () => {
let note = noteList.currentItem;
this.store_.dispatch({
type: 'NOTE_SELECT',
id: note ? note.id : null,
});
});
this.rootWidget_.connect(noteList, (state) => {
return {
selectedNoteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
items: state.notes,
};
});
const noteText = new NoteWidget();
noteText.hStretch = true;
noteText.name = 'noteText';
noteText.style = {
borderBottomWidth: 1,
borderLeftWidth: 1,
};
this.rootWidget_.connect(noteText, (state) => {
return {
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
notes: state.notes,
};
});
const noteMetadata = new NoteMetadataWidget();
noteMetadata.hStretch = true;
noteMetadata.name = 'noteMetadata';
noteMetadata.style = {
borderBottomWidth: 1,
borderLeftWidth: 1,
borderRightWidth: 1,
};
this.rootWidget_.connect(noteMetadata, (state) => {
return { noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null };
});
noteMetadata.hide();
const consoleWidget = new ConsoleWidget();
consoleWidget.hStretch = true;
consoleWidget.style = {
borderBottomWidth: 1,
};
consoleWidget.hide();
const statusBar = new StatusBarWidget();
statusBar.hStretch = true;
const noteLayout = new VLayoutWidget();
noteLayout.name = 'noteLayout';
noteLayout.addChild(noteText, { type: 'stretch', factor: 1 });
noteLayout.addChild(noteMetadata, { type: 'stretch', factor: 1 });
const hLayout = new HLayoutWidget();
hLayout.name = 'hLayout';
hLayout.addChild(folderList, { type: 'stretch', factor: 1 });
hLayout.addChild(noteList, { type: 'stretch', factor: 1 });
hLayout.addChild(noteLayout, { type: 'stretch', factor: 2 });
const vLayout = new VLayoutWidget();
vLayout.name = 'vLayout';
vLayout.addChild(hLayout, { type: 'stretch', factor: 2 });
vLayout.addChild(consoleWidget, { type: 'stretch', factor: 1 });
vLayout.addChild(statusBar, { type: 'fixed', factor: 1 });
const win1 = new WindowWidget();
win1.addChild(vLayout);
win1.name = 'mainWindow';
this.rootWidget_.addChild(win1);
}
showModalOverlay(text) {
if (!this.widget('overlayWindow')) {
const textWidget = new TextWidget();
textWidget.hStretch = true;
textWidget.vStretch = true;
textWidget.text = 'testing';
textWidget.name = 'overlayText';
const win = new WindowWidget();
win.name = 'overlayWindow';
win.addChild(textWidget);
this.rootWidget_.addChild(win);
}
this.widget('overlayWindow').activate();
this.widget('overlayText').text = text;
}
hideModalOverlay() {
if (this.widget('overlayWindow')) this.widget('overlayWindow').hide();
this.widget('mainWindow').activate();
}
addCommandToConsole(cmd) {
if (!cmd) return;
this.stdout(chalk.cyan.bold('> ' + cmd));
}
setupShortcuts() {
const shortcuts = {};
shortcuts['TAB'] = {
friendlyName: 'Tab',
description: () => _('Give focus to next pane'),
isDocOnly: true,
}
shortcuts['SHIFT_TAB'] = {
friendlyName: 'Shift+Tab',
description: () => _('Give focus to previous pane'),
isDocOnly: true,
}
shortcuts[':'] = {
description: () => _('Enter command line mode'),
action: async () => {
const cmd = await this.widget('statusBar').prompt();
if (!cmd) return;
this.addCommandToConsole(cmd);
await this.processCommand(cmd);
},
};
shortcuts['ESC'] = { // Built into terminal-kit inputField
description: () => _('Exit command line mode'),
isDocOnly: true,
};
shortcuts['ENTER'] = {
description: () => _('Edit the selected note'),
action: () => {
const w = this.widget('mainWindow').focusedWidget;
if (w.name === 'folderList') {
this.widget('noteList').focus();
} else if (w.name === 'noteList' || w.name === 'noteText') {
this.processCommand('edit $n');
}
},
}
shortcuts['CTRL_C'] = {
description: () => _('Cancel the current command.'),
friendlyName: 'Ctrl+C',
isDocOnly: true,
}
shortcuts['CTRL_D'] = {
description: () => _('Exit the application.'),
friendlyName: 'Ctrl+D',
isDocOnly: true,
}
shortcuts['DELETE'] = {
description: () => _('Delete the currently selected note or notebook.'),
action: async () => {
if (this.widget('folderList').hasFocus) {
const item = this.widget('folderList').selectedJoplinItem;
if (item.type_ === BaseModel.TYPE_FOLDER) {
await this.processCommand('rmbook ' + item.id);
} else if (item.type_ === BaseModel.TYPE_TAG) {
this.stdout(_('To delete a tag, untag the associated notes.'));
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
this.store().dispatch({
type: 'SEARCH_DELETE',
id: item.id,
});
}
} else if (this.widget('noteList').hasFocus) {
await this.processCommand('rmnote $n');
} else {
this.stdout(_('Please select the note or notebook to be deleted first.'));
}
}
};
shortcuts[' '] = {
friendlyName: 'SPACE',
description: () => _('Set a to-do as completed / not completed'),
action: 'todo toggle $n',
}
shortcuts['tc'] = {
description: () => _('[t]oggle [c]onsole between maximized/minimized/hidden/visible.'),
action: () => {
if (!this.consoleIsShown()) {
this.showConsole();
this.minimizeConsole();
} else {
if (this.consoleIsMaximized()) {
this.hideConsole();
} else {
this.maximizeConsole();
}
}
},
canRunAlongOtherCommands: true,
}
shortcuts['/'] = {
description: () => _('Search'),
action: { type: 'prompt', initialText: 'search ""', cursorPosition: -2 },
};
shortcuts['tm'] = {
description: () => _('[t]oggle note [m]etadata.'),
action: () => {
this.toggleNoteMetadata();
},
canRunAlongOtherCommands: true,
}
shortcuts['mn'] = {
description: () => _('[M]ake a new [n]ote'),
action: { type: 'prompt', initialText: 'mknote ""', cursorPosition: -2 },
}
shortcuts['mt'] = {
description: () => _('[M]ake a new [t]odo'),
action: { type: 'prompt', initialText: 'mktodo ""', cursorPosition: -2 },
}
shortcuts['mb'] = {
description: () => _('[M]ake a new note[b]ook'),
action: { type: 'prompt', initialText: 'mkbook ""', cursorPosition: -2 },
}
shortcuts['yn'] = {
description: () => _('Copy ([Y]ank) the [n]ote to a notebook.'),
action: { type: 'prompt', initialText: 'cp $n ""', cursorPosition: -2 },
}
shortcuts['dn'] = {
description: () => _('Move the note to a notebook.'),
action: { type: 'prompt', initialText: 'mv $n ""', cursorPosition: -2 },
}
return shortcuts;
}
toggleConsole() {
this.showConsole(!this.consoleIsShown());
}
showConsole(doShow = true) {
this.widget('console').show(doShow);
}
hideConsole() {
this.showConsole(false);
}
consoleIsShown() {
return this.widget('console').shown;
}
maximizeConsole(doMaximize = true) {
const consoleWidget = this.widget('console');
if (consoleWidget.isMaximized__ === undefined) {
consoleWidget.isMaximized__ = false;
}
if (consoleWidget.isMaximized__ === doMaximize) return;
let constraints = {
type: 'stretch',
factor: !doMaximize ? 1 : 4,
};
consoleWidget.isMaximized__ = doMaximize;
this.widget('vLayout').setWidgetConstraints(consoleWidget, constraints);
}
minimizeConsole() {
this.maximizeConsole(false);
}
consoleIsMaximized() {
return this.widget('console').isMaximized__ === true;
}
showNoteMetadata(show = true) {
this.widget('noteMetadata').show(show);
}
hideNoteMetadata() {
this.showNoteMetadata(false);
}
toggleNoteMetadata() {
this.showNoteMetadata(!this.widget('noteMetadata').shown);
}
widget(name) {
if (name === 'root') return this.rootWidget_;
return this.rootWidget_.childByName(name);
}
app() {
return this.app_;
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
shortcuts() {
return this.shortcuts_;
}
term() {
return this.term_;
}
activeListItem() {
const widget = this.widget('mainWindow').focusedWidget;
if (!widget) return null;
if (widget.name == 'noteList' || widget.name == 'folderList') {
return widget.currentItem;
}
return null;
}
async handleModelAction(action) {
this.logger().info('Action:', action);
let state = Object.assign({}, defaultState);
state.notes = this.widget('noteList').items;
let newState = reducer(state, action);
if (newState !== state) {
this.widget('noteList').items = newState.notes;
}
}
async processCommand(cmd) {
if (!cmd) return;
cmd = cmd.trim();
if (!cmd.length) return;
this.logger().info('Got command: ' + cmd);
if (cmd === 'q' || cmd === 'wq' || cmd === 'qa') { // Vim bonus
await this.app().exit();
return;
}
let note = this.widget('noteList').currentItem;
let folder = this.widget('folderList').currentItem;
let args = splitCommandString(cmd);
for (let i = 0; i < args.length; i++) {
if (args[i] == '$n') {
args[i] = note ? note.id : '';
} else if (args[i] == '$b') {
args[i] = folder ? folder.id : '';
} else if (args[i] == '$c') {
const item = this.activeListItem();
args[i] = item ? item.id : '';
}
}
try {
await this.app().execCommand(args);
} catch (error) {
this.stdout(error.message);
}
this.widget('console').scrollBottom();
}
async updateFolderList() {
const folders = await Folder.all();
this.widget('folderList').items = folders;
}
async updateNoteList(folderId) {
const fields = Note.previewFields();
fields.splice(fields.indexOf('body'), 1);
const notes = folderId ? await Note.previews(folderId, { fields: fields }) : [];
this.widget('noteList').items = notes;
}
async updateNoteText(note) {
const text = note ? note.body : '';
this.widget('noteText').text = text;
}
// Any key after which a shortcut is not possible.
isSpecialKey(name) {
return [':', 'ENTER', 'DOWN', 'UP', 'LEFT', 'RIGHT', 'DELETE', 'BACKSPACE', 'ESCAPE', 'TAB', 'SHIFT_TAB', 'PAGE_UP', 'PAGE_DOWN'].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();
}
}
stdout(text) {
if (text === null || text === undefined) return;
let lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
const v = typeof lines[i] === 'object' ? JSON.stringify(lines[i]) : lines[i];
this.widget('console').addLine(v);
}
this.updateStatusBarMessage();
}
exit() {
this.fullScreen(false);
this.resourceServer_.stop();
}
updateStatusBarMessage() {
const consoleWidget = this.widget('console');
let msg = '';
const text = consoleWidget.lastLine;
const cmd = this.app().currentCommand();
if (cmd) {
msg += cmd.name();
if (cmd.cancellable()) msg += ' [Press Ctrl+C to cancel]';
msg += ': ';
}
if (text && text.length) {
msg += text;
}
if (msg !== '') this.widget('statusBar').setItemAt(0, msg);
}
async setupResourceServer() {
const linkStyle = chalk.blue.underline;
const noteTextWidget = this.widget('noteText');
const resourceIdRegex = /^:\/[a-f0-9]+$/i
const noteLinks = {};
const hasProtocol = function(s, protocols) {
if (!s) return false;
s = s.trim().toLowerCase();
for (let i = 0; i < protocols.length; i++) {
if (s.indexOf(protocols[i] + '://') === 0) return true;
}
return false;
}
// By default, before the server is started, only the regular
// URLs appear in blue.
noteTextWidget.markdownRendererOptions = {
linkUrlRenderer: (index, url) => {
if (!url) return url;
if (resourceIdRegex.test(url)) {
return url;
} else if (hasProtocol(url, ['http', 'https'])) {
return linkStyle(url);
} else {
return url;
}
},
};
this.resourceServer_ = new ResourceServer();
this.resourceServer_.setLogger(this.app().logger());
this.resourceServer_.setLinkHandler(async (path, response) => {
const link = noteLinks[path];
if (link.type === 'url') {
response.writeHead(302, { 'Location': link.url });
return true;
}
if (link.type === 'resource') {
const resourceId = link.id;
let resource = await Resource.load(resourceId);
if (!resource) throw new Error('No resource with ID ' + resourceId); // Should be nearly impossible
if (resource.mime) response.setHeader('Content-Type', resource.mime);
response.write(await Resource.content(resource));
return true;
}
return false;
});
await this.resourceServer_.start();
if (!this.resourceServer_.started()) return;
noteTextWidget.markdownRendererOptions = {
linkUrlRenderer: (index, url) => {
if (!url) return url;
if (resourceIdRegex.test(url)) {
noteLinks[index] = {
type: 'resource',
id: url.substr(2),
};
} else if (hasProtocol(url, ['http', 'https', 'file', 'ftp'])) {
noteLinks[index] = {
type: 'url',
url: url,
};
} else if (url.indexOf('#') === 0) {
return ''; // Anchors aren't supported for now
} else {
return url;
}
return linkStyle(this.resourceServer_.baseUrl() + '/' + index);
},
};
}
async start() {
const term = this.term();
this.fullScreen();
try {
this.setupResourceServer();
this.renderer_.start();
const statusBar = this.widget('statusBar');
term.grabInput();
term.on('key', async (name, matches, data) => {
// -------------------------------------------------------------------------
// Handle special shortcuts
// -------------------------------------------------------------------------
if (name === 'CTRL_D') {
const cmd = this.app().currentCommand();
if (cmd && cmd.cancellable() && !this.commandCancelCalled_) {
this.commandCancelCalled_ = true;
await cmd.cancel();
this.commandCancelCalled_ = false;
}
await this.app().exit();
return;
}
if (name === 'CTRL_C' ) {
const cmd = this.app().currentCommand();
if (!cmd || !cmd.cancellable() || this.commandCancelCalled_) {
this.stdout(_('Press Ctrl+D or type "exit" to exit the application'));
} else {
this.commandCancelCalled_ = true;
await cmd.cancel()
this.commandCancelCalled_ = false;
}
return;
}
// -------------------------------------------------------------------------
// Build up current shortcut
// -------------------------------------------------------------------------
const now = (new Date()).getTime();
if (now - this.lastShortcutKeyTime_ > 800 || this.isSpecialKey(name)) {
this.currentShortcutKeys_ = [name];
} else {
// If the previous key was a special key (eg. up, down arrow), this new key
// starts a new shortcut.
if (this.currentShortcutKeys_.length && this.isSpecialKey(this.currentShortcutKeys_[0])) {
this.currentShortcutKeys_ = [name];
} else {
this.currentShortcutKeys_.push(name);
}
}
this.lastShortcutKeyTime_ = now;
// -------------------------------------------------------------------------
// Process shortcut and execute associated command
// -------------------------------------------------------------------------
const shortcutKey = this.currentShortcutKeys_.join('');
const cmd = shortcutKey in this.shortcuts_ ? this.shortcuts_[shortcutKey] : null;
let processShortcutKeys = !this.app().currentCommand() && cmd;
if (cmd && cmd.canRunAlongOtherCommands) processShortcutKeys = true;
if (statusBar.promptActive) processShortcutKeys = false;
if (cmd && cmd.isDocOnly) processShortcutKeys = false;
if (processShortcutKeys) {
this.logger().info('Shortcut:', shortcutKey, cmd.description());
this.currentShortcutKeys_ = [];
if (typeof cmd.action === 'function') {
await cmd.action();
} else if (typeof cmd.action === 'object') {
if (cmd.action.type === 'prompt') {
let promptOptions = {};
if ('cursorPosition' in cmd.action) promptOptions.cursorPosition = cmd.action.cursorPosition;
const commandString = await statusBar.prompt(cmd.action.initialText ? cmd.action.initialText : '', null, promptOptions);
this.addCommandToConsole(commandString);
await this.processCommand(commandString);
} else {
throw new Error('Unknown command: ' + JSON.stringify(cmd.action));
}
} else { // String
this.stdout(cmd.action);
await this.processCommand(cmd.action);
}
}
// Optimisation: Update the status bar only
// if the user is not already typing a command:
if (!statusBar.promptActive) this.updateStatusBarMessage();
});
} catch (error) {
this.fullScreen(false);
this.logger().error(error);
console.error(error);
}
process.on('unhandledRejection', (reason, p) => {
this.fullScreen(false);
console.error('Unhandled promise rejection', p, 'reason:', reason);
process.exit(1);
});
}
}
AppGui.INPUT_MODE_NORMAL = 1;
AppGui.INPUT_MODE_META = 2;
module.exports = AppGui;

View File

@ -1,52 +1,47 @@
import { JoplinDatabase } from 'lib/joplin-database.js';
import { Database } from 'lib/database.js';
import { DatabaseDriverNode } from 'lib/database-driver-node.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js';
import { BaseItem } from 'lib/models/base-item.js';
import { Note } from 'lib/models/note.js';
import { Setting } from 'lib/models/setting.js';
import { Logger } from 'lib/logger.js';
import { sprintf } from 'sprintf-js';
import { reg } from 'lib/registry.js';
import { fileExtension } from 'lib/path-utils.js';
import { _, setLocale, defaultLocale, closestSupportedLocale } from 'lib/locale.js';
import os from 'os';
import fs from 'fs-extra';
import yargParser from 'yargs-parser';
import { handleAutocompletion, installAutocompletionFile } from './autocompletion.js';
import { cliUtils } from './cli-utils.js';
const { BaseApplication } = require('lib/BaseApplication');
const { createStore, applyMiddleware } = require('redux');
const { reducer, defaultState } = require('lib/reducer.js');
const { JoplinDatabase } = require('lib/joplin-database.js');
const { Database } = require('lib/database.js');
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { BaseItem } = require('lib/models/base-item.js');
const { Note } = require('lib/models/note.js');
const { Tag } = require('lib/models/tag.js');
const { Setting } = require('lib/models/setting.js');
const { Logger } = require('lib/logger.js');
const { sprintf } = require('sprintf-js');
const { reg } = require('lib/registry.js');
const { fileExtension } = require('lib/path-utils.js');
const { shim } = require('lib/shim.js');
const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const os = require('os');
const fs = require('fs-extra');
const { cliUtils } = require('./cli-utils.js');
const EventEmitter = require('events');
class Application {
class Application extends BaseApplication {
constructor() {
super();
this.showPromptString_ = true;
this.logger_ = new Logger();
this.dbLogger_ = new Logger();
this.autocompletion_ = { active: false };
this.commands_ = {};
this.commandMetadata_ = null;
this.activeCommand_ = null;
this.allCommandsLoaded_ = false;
this.showStackTraces_ = false;
this.gui_ = null;
}
currentFolder() {
return this.currentFolder_;
gui() {
return this.gui_;
}
async refreshCurrentFolder() {
let newFolder = null;
if (this.currentFolder_) newFolder = await Folder.load(this.currentFolder_.id);
if (!newFolder) newFolder = await Folder.defaultFolder();
this.switchCurrentFolder(newFolder);
}
switchCurrentFolder(folder) {
this.currentFolder_ = folder;
Setting.setValue('activeFolderId', folder ? folder.id : '');
commandStdoutMaxWidth() {
return this.gui().stdoutMaxWidth();
}
async guessTypeAndLoadItem(pattern, options = null) {
@ -60,10 +55,35 @@ class Application {
async loadItem(type, pattern, options = null) {
let output = await this.loadItems(type, pattern, options);
if (output.length > 1) {
// output.sort((a, b) => { return a.user_updated_time < b.user_updated_time ? +1 : -1; });
// let answers = { 0: _('[Cancel]') };
// for (let i = 0; i < output.length; i++) {
// answers[i + 1] = output[i].title;
// }
// Not really useful with new UI?
throw new Error(_('More than one item match "%s". Please narrow down your query.', pattern));
// let msg = _('More than one item match "%s". Please select one:', pattern);
// const response = await cliUtils.promptMcq(msg, answers);
// if (!response) return null;
return output[response - 1];
} else {
return output.length ? output[0] : null;
}
}
async loadItems(type, pattern, options = null) {
if (type === 'folderOrNote') {
const folders = await this.loadItems(BaseModel.TYPE_FOLDER, pattern, options);
if (folders.length) return folders;
return await this.loadItems(BaseModel.TYPE_NOTE, pattern, options);
}
pattern = pattern ? pattern.toString() : '';
if (type == BaseModel.TYPE_FOLDER && (pattern == Folder.conflictFolderTitle() || pattern == Folder.conflictFolderId())) return [Folder.conflictFolder()];
@ -89,150 +109,72 @@ class Application {
item = await ItemClass.load(pattern); // Load by id
if (item) return [item];
if (pattern.length >= 4) {
item = await ItemClass.loadByPartialId(pattern);
if (item) return [item];
if (pattern.length >= 2) {
return await ItemClass.loadByPartialId(pattern);
}
}
return [];
}
// Handles the initial flags passed to main script and
// returns the remaining args.
async handleStartFlags_(argv) {
let matched = {};
argv = argv.slice(0);
argv.splice(0, 2); // First arguments are the node executable, and the node JS file
while (argv.length) {
let arg = argv[0];
let nextArg = argv.length >= 2 ? argv[1] : null;
if (arg == '--profile') {
if (!nextArg) throw new Error(_('Usage: %s', '--profile <dir-path>'));
matched.profileDir = nextArg;
argv.splice(0, 2);
continue;
stdout(text) {
return this.gui().stdout(text);
}
if (arg == '--env') {
if (!nextArg) throw new Error(_('Usage: %s', '--env <dev|prod>'));
matched.env = nextArg;
argv.splice(0, 2);
continue;
}
setupCommand(cmd) {
cmd.setStdout((text) => {
return this.stdout(text);
});
if (arg == '--update-geolocation-disabled') {
Note.updateGeolocationEnabled_ = false;
argv.splice(0, 1);
continue;
}
if (arg == '--stack-trace-enabled') {
this.showStackTraces_ = true;
argv.splice(0, 1);
continue;
}
if (arg == '--log-level') {
if (!nextArg) throw new Error(_('Usage: %s', '--log-level <none|error|warn|info|debug>'));
matched.logLevel = Logger.levelStringToId(nextArg);
argv.splice(0, 2);
continue;
}
if (arg == '--autocompletion') {
this.autocompletion_.active = true;
argv.splice(0, 1);
continue;
}
if (arg == '--ac-install') {
this.autocompletion_.install = true;
argv.splice(0, 1);
continue;
}
if (arg == '--ac-current') {
if (!nextArg) throw new Error(_('Usage: %s', '--ac-current <num>'));
this.autocompletion_.current = nextArg;
argv.splice(0, 2);
continue;
}
if (arg == '--ac-line') {
if (!nextArg) throw new Error(_('Usage: %s', '--ac-line <line>'));
let line = nextArg.replace(/\|__QUOTE__\|/g, '"');
line = line.replace(/\|__SPACE__\|/g, ' ');
line = line.replace(/\|__OPEN_RB__\|/g, '(');
line = line.replace(/\|__OPEN_CB__\|/g, ')');
line = line.split('|__SEP__|');
this.autocompletion_.line = line;
argv.splice(0, 2);
continue;
}
if (arg.length && arg[0] == '-') {
throw new Error(_('Unknown flag: %s', arg));
cmd.setDispatcher((action) => {
if (this.store()) {
return this.store().dispatch(action);
} else {
break;
return (action) => {};
}
});
cmd.setPrompt(async (message, options) => {
if (!options) options = {};
if (!options.type) options.type = 'boolean';
if (!options.booleanAnswerDefault) options.booleanAnswerDefault = 'y';
if (!options.answers) options.answers = options.booleanAnswerDefault === 'y' ? [_('Y'), _('n')] : [_('N'), _('y')];
if (options.type == 'boolean') {
message += ' (' + options.answers.join('/') + ')';
}
if (!matched.logLevel) matched.logLevel = Logger.LEVEL_INFO;
if (!matched.env) matched.env = 'prod';
let answer = await this.gui().prompt('', message + ' ');
return {
matched: matched,
argv: argv,
if (options.type === 'boolean') {
if (answer === null) return false; // Pressed ESCAPE
if (!answer) answer = options.answers[0];
let positiveIndex = options.booleanAnswerDefault == 'y' ? 0 : 1;
return answer.toLowerCase() === options.answers[positiveIndex].toLowerCase();
}
});
return cmd;
}
async exit(code = 0) {
const doExit = async () => {
this.gui().exit();
await super.exit(code);
};
// Give it a few seconds to cancel otherwise exit anyway
setTimeout(async () => {
await doExit();
}, 5000);
if (await reg.syncTarget().syncStarted()) {
this.stdout(_('Cancelling background synchronisation... Please wait.'));
const sync = await reg.syncTarget().synchronizer();
await sync.cancel();
}
escapeShellArg(arg) {
if (arg.indexOf('"') >= 0 && arg.indexOf("'") >= 0) throw new Error(_('Command line argument "%s" contains both quotes and double-quotes - aborting.', arg)); // Hopeless case
let quote = '"';
if (arg.indexOf('"') >= 0) quote = "'";
if (arg.indexOf(' ') >= 0 || arg.indexOf("\t") >= 0) return quote + arg + quote;
return arg;
}
shellArgsToString(args) {
let output = [];
for (let i = 0; i < args.length; i++) {
output.push(this.escapeShellArg(args[i]));
}
return output.join(' ');
}
onLocaleChanged() {
return;
let currentCommands = this.vorpal().commands;
for (let i = 0; i < currentCommands.length; i++) {
let cmd = currentCommands[i];
if (cmd._name == 'help') {
cmd.description(_('Provides help for a given command.'));
} else if (cmd._name == 'exit') {
cmd.description(_('Exits the application.'));
} else if (cmd.__commandObject) {
cmd.description(cmd.__commandObject.description());
}
}
}
baseModelListener(action) {
switch (action.type) {
case 'NOTES_UPDATE_ONE':
case 'NOTES_DELETE':
case 'FOLDERS_UPDATE_ONE':
case 'FOLDER_DELETE':
//reg.scheduleSync();
break;
}
await doExit();
}
commands() {
@ -246,11 +188,7 @@ class Application {
let CommandClass = require('./' + path);
let cmd = new CommandClass();
if (!cmd.enabled()) return;
cmd.log = (...object) => {
return console.log(...object);
}
cmd = this.setupCommand(cmd);
this.commands_[cmd.name()] = cmd;
});
@ -297,6 +235,10 @@ class Application {
return Object.assign({}, this.commandMetadata_);
}
hasGui() {
return this.gui() && !this.gui().isDummy();
}
findCommandByName(name) {
if (this.commands_[name]) return this.commands_[name];
@ -308,123 +250,61 @@ class Application {
e.type = 'notFound';
throw e;
}
let cmd = new CommandClass();
cmd.log = (...object) => {
return console.log(...object);
}
cmd = this.setupCommand(cmd);
this.commands_[name] = cmd;
return this.commands_[name];
}
dummyGui() {
return {
isDummy: () => { return true; },
prompt: (initialText = '', promptString = '') => { return cliUtils.prompt(initialText, promptString); },
showConsole: () => {},
maximizeConsole: () => {},
stdout: (text) => { console.info(text); },
fullScreen: (b=true) => {},
exit: () => {},
showModalOverlay: (text) => {},
hideModalOverlay: () => {},
stdoutMaxWidth: () => { return 78; }
};
}
async execCommand(argv) {
if (!argv.length) return this.execCommand(['help']);
reg.logger().info('execCommand()', argv);
const commandName = argv[0];
this.activeCommand_ = this.findCommandByName(commandName);
let outException = null;
try {
if (this.gui().isDummy() && !this.activeCommand_.supportsUi('cli')) throw new Error(_('The command "%s" is only available in GUI mode', this.activeCommand_.name()));
const cmdArgs = cliUtils.makeCommandArgs(this.activeCommand_, argv);
await this.activeCommand_.action(cmdArgs);
} catch (error) {
outException = error;
}
this.activeCommand_ = null;
if (outException) throw outException;
}
currentCommand() {
return this.activeCommand_;
}
async start() {
let argv = process.argv;
let startFlags = await this.handleStartFlags_(argv);
argv = startFlags.argv;
let initArgs = startFlags.matched;
if (argv.length) this.showPromptString_ = false;
async start(argv) {
argv = await super.start(argv);
if (process.argv[1].indexOf('joplindev') >= 0) {
if (!initArgs.profileDir) initArgs.profileDir = '/mnt/d/Temp/TestNotes2';
initArgs.logLevel = Logger.LEVEL_DEBUG;
initArgs.env = 'dev';
}
cliUtils.setStdout((object) => {
return this.stdout(object);
});
Setting.setConstant('appName', initArgs.env == 'dev' ? 'joplindev' : 'joplin');
const profileDir = initArgs.profileDir ? initArgs.profileDir : os.homedir() + '/.config/' + Setting.value('appName');
const resourceDir = profileDir + '/resources';
const tempDir = profileDir + '/tmp';
Setting.setConstant('env', initArgs.env);
Setting.setConstant('profileDir', profileDir);
Setting.setConstant('resourceDir', resourceDir);
Setting.setConstant('tempDir', tempDir);
await fs.mkdirp(profileDir, 0o755);
await fs.mkdirp(resourceDir, 0o755);
await fs.mkdirp(tempDir, 0o755);
this.logger_.addTarget('file', { path: profileDir + '/log.txt' });
this.logger_.setLevel(initArgs.logLevel);
reg.setLogger(this.logger_);
reg.dispatch = (o) => {};
this.dbLogger_.addTarget('file', { path: profileDir + '/log-database.txt' });
this.dbLogger_.setLevel(initArgs.logLevel);
const packageJson = require('./package.json');
this.logger_.info(sprintf('Starting %s %s (%s)...', packageJson.name, packageJson.version, Setting.value('env')));
this.logger_.info('Profile directory: ' + profileDir);
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
this.database_.setLogger(this.dbLogger_);
await this.database_.open({ name: profileDir + '/database.sqlite' });
reg.setDb(this.database_);
BaseModel.db_ = this.database_;
BaseModel.dispatch = (action) => { this.baseModelListener(action) }
await Setting.load();
if (Setting.value('firstStart')) {
let locale = process.env.LANG;
if (!locale) locale = defaultLocale();
locale = locale.split('.');
locale = locale[0];
reg.logger().info('First start: detected locale as ' + locale);
Setting.setValue('locale', closestSupportedLocale(locale));
Setting.setValue('firstStart', 0)
}
setLocale(Setting.value('locale'));
let currentFolderId = Setting.value('activeFolderId');
this.currentFolder_ = null;
if (currentFolderId) this.currentFolder_ = await Folder.load(currentFolderId);
if (!this.currentFolder_) this.currentFolder_ = await Folder.defaultFolder();
Setting.setValue('activeFolderId', this.currentFolder_ ? this.currentFolder_.id : '');
if (this.autocompletion_.active) {
if (this.autocompletion_.install) {
try {
await installAutocompletionFile(Setting.value('appName'), Setting.value('profileDir'));
} catch (error) {
if (error.code == 'shellNotSupported') {
console.info(error.message);
return;
}
throw error;
}
} else {
let items = await handleAutocompletion(this.autocompletion_);
if (!items.length) return;
for (let i = 0; i < items.length; i++) {
items[i] = items[i].replace(/ /g, '\\ ');
items[i] = items[i].replace(/'/g, "\\'");
items[i] = items[i].replace(/:/g, "\\:");
items[i] = items[i].replace(/\(/g, '\\(');
items[i] = items[i].replace(/\)/g, '\\)');
}
console.info(items.join("\n"));
}
return;
}
// If we have some arguments left at this point, it's a command
// so execute it.
if (argv.length) {
this.gui_ = this.dummyGui();
try {
await this.execCommand(argv);
@ -435,6 +315,33 @@ class Application {
console.info(error.message);
}
}
} else { // Otherwise open the GUI
this.initRedux();
const AppGui = require('./app-gui.js');
this.gui_ = new AppGui(this, this.store());
this.gui_.setLogger(this.logger_);
await this.gui_.start();
// Since the settings need to be loaded before the store is created, it will never
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
// initialised. So we manually call dispatchUpdateAll() to force an update.
Setting.dispatchUpdateAll();
await FoldersScreenUtils.refreshFolders();
const tags = await Tag.allWithNotes();
this.dispatch({
type: 'TAG_UPDATE_ALL',
tags: tags,
});
this.store().dispatch({
type: 'FOLDER_SELECT',
id: Setting.value('activeFolderId'),
});
}
}
}
@ -447,4 +354,4 @@ function app() {
return application_;
}
export { app };
module.exports = { app };

View File

@ -1,175 +0,0 @@
import { app } from './app.js';
import { Note } from 'lib/models/note.js';
import { Folder } from 'lib/models/folder.js';
import { Tag } from 'lib/models/tag.js';
import { cliUtils } from './cli-utils.js';
import { _ } from 'lib/locale.js';
import fs from 'fs-extra';
import os from 'os';
import yargParser from 'yargs-parser';
function autocompletionFileContent(appName, alias) {
let content = fs.readFileSync(__dirname + '/autocompletion_template.txt', 'utf8');
content = content.replace(/\|__APPNAME__\|/g, appName);
if (!alias) alias = 'joplin_alias_support_is_disabled';
content = content.replace(/\|__APPALIAS__\|/g, alias);
return content;
}
function autocompletionScriptPath(profileDir) {
return profileDir + '/autocompletion.sh';
}
async function installAutocompletionFile(appName, profileDir) {
if (process.env.SHELL.indexOf('bash') < 0) {
let error = new Error(_('Only Bash is currently supported for autocompletion.'));
error.code = 'shellNotSupported';
throw error;
}
const alias = await cliUtils.promptInput(_('Autocompletion can be made to work with an alias too (such as a one-letter command like "j").\nIf you would like to enable this, please type the alias now (leave it empty for no alias):'));
const content = autocompletionFileContent(appName, alias);
const filePath = autocompletionScriptPath(profileDir);
fs.writeFileSync(filePath, content);
console.info(_('Created autocompletion script "%s".', filePath));
const bashProfilePath = os.homedir() + '/.bashrc';
let bashrcContent = fs.readFileSync(bashProfilePath, 'utf8');
const lineToAdd = 'source ' + filePath;
if (bashrcContent.indexOf(lineToAdd) >= 0) {
console.info(_('Autocompletion script is already present in "%s".', bashProfilePath));
} else {
bashrcContent += "\n" + lineToAdd + "\n";
fs.writeFileSync(bashProfilePath, bashrcContent);
console.info(_('Added autocompletion to "%s".', bashProfilePath));
}
if (alias) {
if (bashrcContent.indexOf('alias ' + alias + '=') >= 0) {
console.info(_('Alias is already set in "%s".', bashProfilePath));
} else {
const l = 'alias ' + alias + '=' + appName;
bashrcContent += "\n" + l + "\n";
fs.writeFileSync(bashProfilePath, bashrcContent);
console.info(_('Added alias to "%s".', bashProfilePath));
}
}
console.info(_("IMPORTANT: run the following command to initialise autocompletion in the current shell:\nsource '%s'", filePath));
}
async function handleAutocompletion(autocompletion) {
let args = autocompletion.line.slice();
args.splice(0, 1);
let current = autocompletion.current - 1;
const currentWord = args[current] ? args[current] : '';
// Auto-complete the command name
if (current == 0) {
const metadata = await app().commandMetadata();
let commandNames = [];
for (let n in metadata) {
if (!metadata.hasOwnProperty(n)) continue;
const md = metadata[n];
if (md.hidden) continue;
if (currentWord == n) return [n];
if (n.indexOf(currentWord) === 0) commandNames.push(n);
}
return commandNames;
}
const commandName = args[0];
const metadata = await app().commandMetadata();
const md = metadata[commandName];
const options = md && md.options ? md.options : [];
// Auto-complete the command options
if (currentWord) {
const includeLongs = currentWord.length == 1 ? currentWord.substr(0, 1) == '-' : currentWord.substr(0, 2) == '--';
const includeShorts = currentWord.length <= 2 && currentWord.substr(0, 1) == '-' && currentWord.substr(0, 2) != '--';
if (includeLongs || includeShorts) {
const output = [];
for (let i = 0; i < options.length; i++) {
const flags = cliUtils.parseFlags(options[i][0]);
const long = flags.long ? '--' + flags.long : null;
const short = flags.short ? '-' + flags.short : null;
if (includeLongs && long && long.indexOf(currentWord) === 0) output.push(long);
if (includeShorts && short && short.indexOf(currentWord) === 0) output.push(short);
}
return output;
}
}
// Auto-complete the command arguments
let argIndex = -1;
for (let i = 0; i < args.length; i++) {
const w = args[i];
if (i == 0 || w.indexOf('-') == 0) {
continue;
}
argIndex++;
}
if (argIndex < 0) return [];
let cmdUsage = yargParser(md.usage)['_'];
cmdUsage.splice(0, 1);
if (cmdUsage.length <= argIndex) return [];
let argName = cmdUsage[argIndex];
argName = cliUtils.parseCommandArg(argName).name;
if (argName == 'note' || argName == 'note-pattern') {
if (!app().currentFolder()) return [];
const notes = await Note.previews(app().currentFolder().id, { titlePattern: currentWord + '*' });
return notes.map((n) => n.title);
}
if (argName == 'notebook') {
const folders = await Folder.search({ titlePattern: currentWord + '*' });
return folders.map((n) => n.title);
}
if (argName == 'tag') {
let tags = await Tag.search({ titlePattern: currentWord + '*' });
return tags.map((n) => n.title);
}
if (argName == 'tag-command') {
return filterList(['add', 'remove', 'list'], currentWord);
}
if (argName == 'todo-command') {
return filterList(['toggle', 'clear'], currentWord);
}
if (argName == 'command') {
const commands = await app().commandNames();
return this.filterList(commands, currentWord);
}
return [];
}
function filterList(list, currentWord) {
let output = [];
for (let i = 0; i < list.length; i++) {
if (list[i].indexOf(currentWord) !== 0) continue;
output.push(list[i]);
}
return output;
}
export { handleAutocompletion, installAutocompletionFile, autocompletionScriptPath };

View File

@ -1,44 +0,0 @@
export IFS=$'\n'
_|__APPNAME__|_completion() {
# COMP_WORDS contains each word in the current command, the last one
# being the one that needs to be completed. Convert this array
# to an "escaped" line which can be passed to the joplin CLI
# which will provide the possible autocompletion words.
ESCAPED_LINE=""
for WORD in "${COMP_WORDS[@]}"
do
if [[ -n $ESCAPED_LINE ]]; then
ESCAPED_LINE="$ESCAPED_LINE|__SEP__|"
fi
WORD="${WORD/\"/|__QUOTE__|}"
WORD="${WORD/\\(/|__OPEN_RB__|}"
WORD="${WORD/\\)/|__CLOSE_RB__|}"
WORD="${WORD/\\ /|__SPACE__|}"
ESCAPED_LINE="$ESCAPED_LINE$WORD"
done
# Call joplin with the --autocompletion flag to retrieve the autocompletion
# candidates (each on its own line), and put these into COMREPLY.
# echo "joplindev --autocompletion --ac-current "$COMP_CWORD" --ac-line "$ESCAPED_LINE"" > ~/test.txt
COMPREPLY=()
while read -r line; do
COMPREPLY+=("$line")
done <<< "$(|__APPNAME__| --autocompletion --ac-current "$COMP_CWORD" --ac-line "$ESCAPED_LINE")"
# If there's only one element and it's empty, make COMREPLY
# completely empty so that default completion takes over
# (i.e. regular file completion)
# https://stackoverflow.com/a/19062943/561309
if [[ -z ${COMPREPLY[0]} ]]; then
COMPREPLY=()
fi
}
complete -o default -F _|__APPNAME__|_completion |__APPNAME__|
complete -o default -F _|__APPNAME__|_completion |__APPALIAS__|

View File

@ -1,5 +1,13 @@
const { _ } = require('lib/locale.js');
const { reg } = require('lib/registry.js');
class BaseCommand {
constructor() {
this.stdout_ = null;
this.prompt_ = null;
}
usage() {
throw new Error('Usage not defined');
}
@ -12,6 +20,14 @@ class BaseCommand {
throw new Error('Action not defined');
}
compatibleUis() {
return ['cli', 'gui'];
}
supportsUi(ui) {
return this.compatibleUis().indexOf(ui) >= 0;
}
aliases() {
return [];
}
@ -39,6 +55,32 @@ class BaseCommand {
return r[0];
}
setDispatcher(fn) {
this.dispatcher_ = fn;
}
dispatch(action) {
if (!this.dispatcher_) throw new Error('Dispatcher not defined');
return this.dispatcher_(action);
}
setStdout(fn) {
this.stdout_ = fn;
}
stdout(text) {
if (this.stdout_) this.stdout_(text);
}
setPrompt(fn) {
this.prompt_ = fn;
}
async prompt(message, options = null) {
if (!this.prompt_) throw new Error('Prompt is undefined');
return await this.prompt_(message, options);
}
metadata() {
return {
name: this.name(),
@ -48,6 +90,10 @@ class BaseCommand {
};
}
logger() {
return reg.logger();
}
export { BaseCommand };
}
module.exports = { BaseCommand };

View File

@ -1,10 +1,7 @@
require('source-map-support').install();
require('babel-plugin-transform-runtime');
import fs from 'fs-extra';
import { fileExtension, basename, dirname } from 'lib/path-utils.js';
import wrap_ from 'word-wrap';
import { _, setLocale, languageCode } from 'lib/locale.js';
const fs = require('fs-extra');
const { fileExtension, basename, dirname } = require('lib/path-utils.js');
const wrap_ = require('word-wrap');
const { _, setLocale, languageCode } = require('lib/locale.js');
const rootDir = dirname(dirname(__dirname));
const MAX_WIDTH = 78;
@ -80,7 +77,7 @@ function getHeader() {
output.push('NAME');
output.push('');
output.push(wrap('joplin - a note taking and todo app with synchronisation capabilities'), INDENT);
output.push(wrap('joplin - a note taking and to-do app with synchronisation capabilities'), INDENT);
output.push('');
@ -88,7 +85,7 @@ function getHeader() {
output.push('');
let description = [];
description.push('Joplin is a note taking and todo application, which can handle a large number of notes organised into notebooks.');
description.push('Joplin is a note taking and to-do application, which can handle a large number of notes organised into notebooks.');
description.push('The notes are searchable, can be copied, tagged and modified with your own text editor.');
description.push("\n\n");
description.push('The notes can be synchronised with various target including the file system (for example with a network directory) or with Microsoft OneDrive.');

View File

@ -1,20 +1,20 @@
"use strict"
require('source-map-support').install();
require('babel-plugin-transform-runtime');
require('app-module-path').addPath(__dirname);
const processArgs = process.argv.splice(2, process.argv.length);
const silentLog = processArgs.indexOf('--silent') >= 0;
import { basename, dirname } from 'lib/path-utils.js';
import fs from 'fs-extra';
import gettextParser from 'gettext-parser';
const { basename, dirname } = require('lib/path-utils.js');
const fs = require('fs-extra');
const gettextParser = require('gettext-parser');
const rootDir = dirname(dirname(__dirname));
const cliDir = rootDir + '/CliClient';
const cliLocalesDir = cliDir + '/locales';
const rnDir = rootDir + '/ReactNativeClient';
const electronDir = rootDir + '/ElectronClient/app';
function execCommand(command) {
if (!silentLog) console.info('Running: ' + command);
@ -117,6 +117,9 @@ async function main() {
await createPotFile(potFilePath, [
cliDir + '/app/*.js',
cliDir + '/app/gui/*.js',
electronDir + '/*.js',
electronDir + '/gui/*.js',
rnDir + '/lib/*.js',
rnDir + '/lib/models/*.js',
rnDir + '/lib/services/*.js',
@ -141,6 +144,9 @@ async function main() {
const rnJsonLocaleDir = rnDir + '/locales';
await execCommand('rsync -a "' + jsonLocalesDir + '/" "' + rnJsonLocaleDir + '"');
const electronJsonLocaleDir = electronDir + '/locales';
await execCommand('rsync -a "' + jsonLocalesDir + '/" "' + electronJsonLocaleDir + '"');
}
main().catch((error) => {

View File

@ -1,26 +1,54 @@
require('source-map-support').install();
require('babel-plugin-transform-runtime');
const fs = require('fs-extra');
const { fileExtension, basename, dirname } = require('lib/path-utils.js');
const { _, setLocale, languageCode } = require('lib/locale.js');
const marked = require('lib/marked.js');
const Mustache = require('mustache');
import fs from 'fs-extra';
import { fileExtension, basename, dirname } from 'lib/path-utils.js';
import { _, setLocale, languageCode } from 'lib/locale.js';
import marked from 'lib/marked.js';
const headerHtml = `
<!doctype html>
const headerHtml = `<!doctype html>
<html>
<head>
<title>Joplin - a free, open source, note taking and todo application with synchronisation capabilities</title>
<title>Joplin - an open source note taking and to-do application with synchronisation capabilities</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="https://opensource.keycdn.com/fontawesome/4.7.0/font-awesome.min.css" integrity="sha384-dNpIIXE8U05kAbPhy3G1cz+yZmTzA6CY8Vg/u2L9xRnHjJiAK76m2BIEaSEV+/aU" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha256-k2WSCIexGzOj3Euiig+TlR8gA0EmPjuc79OEeY5L45g=" crossorigin="anonymous"></script>
<style>
body {
background-color: #F1F1F1;
color: #333333;
}
table {
margin-bottom: 1em;
}
td, th {
padding: .8em;
border: 1px solid #ccc;
}
h1, h2 {
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-weight: 600;
font-size: 2em;
margin-bottom: 16px;
}
h2 {
font-size: 1.6em;
}
h3 {
font-size: 1.3em;
}
code {
color: black;
background-color: #eee;
border: 1px solid #ccc;
}
pre code {
border: none;
}
.title-icon {
height: 2em;
}
@ -63,8 +91,9 @@ const headerHtml = `
.cli-screenshot .prompt {
color: #48C2F0;
}
h1 {
font-weight: bold;
.top-screenshot {
margin-top: 2em;
text-align: center;
}
.header {
position: relative;
@ -79,12 +108,74 @@ const headerHtml = `
padding-left: 2em;
padding-right: 2em;
padding-bottom: 2em;
padding-top: 2em;
}
.forkme {
position: absolute;
right: 0;
top:0;
}
.nav-wrapper {
position: relative;
width: inherit;
}
.nav {
background-color: black;
display: table;
width: inherit;
}
.nav.sticky {
position:fixed;
top: 0;
width: inherit;
box-shadow: 0 0 10px #000000;
}
.nav a {
color: white;
display: inline-block;
padding: .6em .9em .6em .9em;
}
.nav ul {
padding-left: 2em;
margin-bottom: 0;
display: table-cell;
min-width: 165px;
}
.nav ul li {
display: inline-block;
padding: 0;
}
.nav li.selected {
background-color: #222;
font-weight: bold;
}
.nav-right {
display: table-cell;
width: 100%;
text-align: right;
vertical-align: middle;
line-height: 0;
}
.nav-right .share-btn {
display: none;
}
.share-btn-github {
display: inline-block;
}
.nav-right .small-share-btn {
display: none;
}
.nav-right .share-btn-github {
display: inline-block;
}
@media all and (min-width: 400px) {
.nav-right .share-btn {
display: inline-block;
}
.nav-right .small-share-btn {
display: none;
}
}
</style>
</head>
@ -93,68 +184,96 @@ const headerHtml = `
<div class="container">
<div class="header">
<a class="forkme" href="https://github.com/laurent22/joplin"><img src="docs/images/ForkMe.png"/></a>
<h1 id="joplin"><img class="title-icon" src="docs/images/Icon512.png">oplin</h1>
<p class="sub-title">A free, open source, note taking and todo application with synchronisation capabilities.</p>
<a class="forkme" href="https://github.com/laurent22/joplin"><img src="{{{imageBaseUrl}}}/ForkMe.png"/></a>
<h1 id="joplin"><img class="title-icon" src="{{{imageBaseUrl}}}/Icon512.png">oplin</h1>
<p class="sub-title">An open source note taking and to-do application with synchronisation capabilities.</p>
</div>
<div class="nav-wrapper">
<div class="nav">
<ul>
<li class="{{selectedHome}}"><a href="{{baseUrl}}/" title="Home"><i class="fa fa-home"></i></a></li>
<li class="{{selectedTerminal}}"><a href="{{baseUrl}}/terminal" title="Terminal"><i class="fa fa-terminal"></i></a></li>
<li class="{{selectedDesktop}}"><a href="{{baseUrl}}/desktop" title="Desktop"><i class="fa fa-desktop"></i></a></li>
</ul>
<div class="nav-right">
<iframe class="share-btn" src="https://www.facebook.com/plugins/share_button.php?href=http%3A%2F%2Fjoplin.cozic.net&layout=button&size=small&mobile_iframe=true&width=60&height=20&appId" width="60" height="20" style="border:none;overflow:hidden" scrolling="no" frameborder="0" allowTransparency="true"></iframe>
<iframe class="share-btn" src="https://platform.twitter.com/widgets/tweet_button.html?url=http%3A%2F%2Fjoplin.cozic.net" width="62" height="20" title="Tweet" style="border: 0; overflow: hidden;"></iframe>
<iframe class="share-btn share-btn-github" src="https://ghbtns.com/github-btn.html?user=laurent22&repo=joplin&type=star&count=true" frameborder="0" scrolling="0" width="80px" height="20px"></iframe>
</div>
</div>
</div>
<div class="content">
`;
const footerHtml = `
<hr/>Copyright (c) 2017 Laurent Cozic
</body>
</html>
`;
const screenshotHtml = `
<table class="screenshots">
<tr>
<th>
Mobile
</th>
<th>
Command line
</th>
</tr>
<tr>
<td>
<img class="mobile-screenshot" src="docs/images/Mobile.png"/>
</td>
<td class="cli-screenshot-wrapper">
<pre class="cli-screenshot">
<span class="prompt">joplin:/My notebook$</span> ls -n 12
[ ] 8am conference call
[ ] Make vet appointment
[ ] Go pick up parcel
[ ] Pay flat rent 💸
[X] Book ferry 🚢
[X] Deploy Joplin app
Open source stuff
Swimming pool time table 🏊
Grocery shopping list 📝
Work itinerary
Tuesday random note
Vacation plans
</pre>
</td>
</tr>
</table>
`;
// const screenshotHtml = `
// <table class="screenshots">
// <tr>
// <th>
// Mobile
// </th>
// <th>
// Command line
// </th>
// </tr>
// <tr>
// <td>
// <img class="mobile-screenshot" src="docs/images/Mobile.png"/>
// </td>
// <td class="cli-screenshot-wrapper">
// <pre class="cli-screenshot">
// <span class="prompt">joplin:/My notebook$</span> ls -n 12
// [ ] 8am conference call ☎
// [ ] Make vet appointment
// [ ] Go pick up parcel
// [ ] Pay flat rent 💸
// [X] Book ferry 🚢
// [X] Deploy Joplin app
// Open source stuff
// Swimming pool time table 🏊
// Grocery shopping list 📝
// Work itinerary
// Tuesday random note
// Vacation plans ☀
// </pre>
// </td>
// </tr>
// </table>
// `;
const gaHtml = `
const scriptHtml = `
<script>
function stickyHeader() {
if ($(window).scrollTop() > 179) {
$('.nav').addClass('sticky');
} else {
$('.nav').removeClass('sticky');
}
}
$(window).scroll(function() {
stickyHeader();
});
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-103586105-1', 'auto');
ga('send', 'pageview');
</script>
`;
// <a href="#" class="small-share-btn" style="background-color: #365899;"><img src="images/ShareFacebook.svg" style=" width: 1.2em; height: 1.2em"/></a>
// <a href="#" class="small-share-btn" style="background-color: #1b95e0;"><img src="images/ShareTwitter.svg" style=" width: 1.2em; height: 1.2em"/></a>
// <a href="#" class="small-share-btn" style="background-color: #eee;"><img src="images/ShareGithub.svg" style=" width: 1.2em; height: 1.2em"/></a>
const rootDir = dirname(dirname(__dirname));
function markdownToHtml(md) {
@ -169,16 +288,31 @@ function markdownToHtml(md) {
renderer: renderer,
});
output = output.replace(/<!-- \[SCREENSHOTS\] -->/, screenshotHtml);
//output = output.replace(/<!-- \[SCREENSHOTS\] -->/, screenshotHtml);
return headerHtml + output + gaHtml + footerHtml;
return headerHtml + output + scriptHtml + footerHtml;
}
function renderFileToHtml(sourcePath, targetPath, params) {
const md = fs.readFileSync(sourcePath, 'utf8');
params.baseUrl = 'http://joplin.cozic.net';
params.imageBaseUrl = params.baseUrl + '/images';
const html = Mustache.render(markdownToHtml(md), params);
fs.writeFileSync(targetPath, html);
}
async function main() {
const md = fs.readFileSync(rootDir + '/README.md', 'utf8');
const html = markdownToHtml(md);
renderFileToHtml(rootDir + '/README.md', rootDir + '/docs/index.html', {
selectedHome: 'selected',
});
fs.writeFileSync(rootDir + '/index.html', html);
renderFileToHtml(rootDir + '/README_terminal.md', rootDir + '/docs/terminal/index.html', {
selectedTerminal: 'selected',
});
renderFileToHtml(rootDir + '/README_desktop.md', rootDir + '/docs/desktop/index.html', {
selectedDesktop: 'selected',
});
}
main().catch((error) => {

View File

@ -1,18 +1,15 @@
"use strict"
require('source-map-support').install();
require('babel-plugin-transform-runtime');
import fs from 'fs-extra';
import { Logger } from 'lib/logger.js';
import { dirname } from 'lib/path-utils.js';
import { DatabaseDriverNode } from 'lib/database-driver-node.js';
import { JoplinDatabase } from 'lib/joplin-database.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js';
import { Setting } from 'lib/models/setting.js';
import { sprintf } from 'sprintf-js';
const fs = require('fs-extra');
const { Logger } = require('lib/logger.js');
const { dirname } = require('lib/path-utils.js');
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
const { JoplinDatabase } = require('lib/joplin-database.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { Setting } = require('lib/models/setting.js');
const { sprintf } = require('sprintf-js');
const exec = require('child_process').exec
process.on('unhandledRejection', (reason, p) => {

View File

@ -1,6 +1,6 @@
import yargParser from 'yargs-parser';
import { _ } from 'lib/locale.js';
import { time } from 'lib/time-utils.js';
const yargParser = require('yargs-parser');
const { _ } = require('lib/locale.js');
const { time } = require('lib/time-utils.js');
const stringPadding = require('string-padding');
const cliUtils = {};
@ -127,6 +127,37 @@ cliUtils.makeCommandArgs = function(cmd, argv) {
return output;
}
cliUtils.promptMcq = function(message, answers) {
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
message += "\n\n";
for (let n in answers) {
if (!answers.hasOwnProperty(n)) continue;
message += _('%s: %s', n, answers[n]) + "\n";
}
message += "\n";
message += _('Your choice: ');
return new Promise((resolve, reject) => {
rl.question(message, (answer) => {
rl.close();
if (!(answer in answers)) {
reject(new Error(_('Invalid answer: %s', answer)));
return;
}
resolve(answer);
});
});
}
cliUtils.promptConfirm = function(message, answers = null) {
if (!answers) answers = [_('Y'), _('n')];
const readline = require('readline');
@ -163,15 +194,38 @@ cliUtils.promptInput = function(message) {
});
}
// Note: initialText is there to have the same signature as statusBar.prompt() so that
// it can be a drop-in replacement, however initialText is not used (and cannot be
// with readline.question?).
cliUtils.prompt = function(initialText = '', promptString = ':') {
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve, reject) => {
rl.question(promptString, (answer) => {
rl.close();
resolve(answer);
});
});
}
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 {
@ -185,11 +239,11 @@ cliUtils.redrawDone = function() {
if (!redrawStarted_) return;
if (redrawLastLog_) {
console.info(redrawLastLog_);
this.stdout_(redrawLastLog_);
}
redrawLastLog_ = null;
redrawStarted_ = false;
}
export { cliUtils };
module.exports = { cliUtils };

View File

@ -0,0 +1,31 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { shim } = require('lib/shim.js');
const fs = require('fs-extra');
class Command extends BaseCommand {
usage() {
return 'attach <note> <file>';
}
description() {
return _('Attaches the given file to the note.');
}
async action(args) {
let title = args['note'];
let note = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
if (!note) throw new Error(_('Cannot find "%s".', title));
const localFilePath = args['file'];
await shim.attachFileToNote(note, localFilePath);
}
}
module.exports = Command;

View File

@ -1,9 +1,9 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
class Command extends BaseCommand {
@ -21,6 +21,10 @@ class Command extends BaseCommand {
];
}
enabled() {
return false;
}
async action(args) {
let title = args['note'];
@ -28,7 +32,7 @@ class Command extends BaseCommand {
if (!item) throw new Error(_('Cannot find "%s".', title));
const content = args.options.verbose ? await Note.serialize(item) : await Note.serializeForEdit(item);
this.log(content);
this.stdout(content);
}
}

View File

@ -1,7 +1,7 @@
import { BaseCommand } from './base-command.js';
import { _, setLocale } from 'lib/locale.js';
import { app } from './app.js';
import { Setting } from 'lib/models/setting.js';
const { BaseCommand } = require('./base-command.js');
const { _, setLocale } = require('lib/locale.js');
const { app } = require('./app.js');
const { Setting } = require('lib/models/setting.js');
class Command extends BaseCommand {
@ -33,16 +33,21 @@ class Command extends BaseCommand {
if (!args.name && !args.value) {
let keys = Setting.keys(!verbose, 'cli');
keys.sort();
for (let i = 0; i < keys.length; i++) {
const value = Setting.value(keys[i]);
if (!verbose && !value) continue;
this.log(renderKeyValue(keys[i]));
this.stdout(renderKeyValue(keys[i]));
}
app().gui().showConsole();
app().gui().maximizeConsole();
return;
}
if (args.name && !args.value) {
this.log(renderKeyValue(args.name));
this.stdout(renderKeyValue(args.name));
app().gui().showConsole();
app().gui().maximizeConsole();
return;
}

View File

@ -1,9 +1,9 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
class Command extends BaseCommand {

View File

@ -1,10 +1,10 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js';
import { time } from 'lib/time-utils.js';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { time } = require('lib/time-utils.js');
class Command extends BaseCommand {
@ -13,13 +13,13 @@ class Command extends BaseCommand {
}
description() {
return _('Marks a todo as done.');
return _('Marks a to-do as done.');
}
static async handleAction(args, isCompleted) {
const note = await app().loadItem(BaseModel.TYPE_NOTE, args.note);
if (!note) throw new Error(_('Cannot find "%s".', args.note));
if (!note.is_todo) throw new Error(_('Note is not a todo: "%s"', args.note));
if (!note.is_todo) throw new Error(_('Note is not a to-do: "%s"', args.note));
const todoCompleted = !!note.todo_completed;
@ -32,7 +32,7 @@ class Command extends BaseCommand {
}
async action(args) {
Command.handleAction(args, true);
await Command.handleAction(args, true);
}
}

View File

@ -1,9 +1,9 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js';
import { Tag } from 'lib/models/tag.js';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { Tag } = require('lib/models/tag.js');
class Command extends BaseCommand {
@ -36,7 +36,7 @@ class Command extends BaseCommand {
items = items.concat(tags);
this.log(JSON.stringify(items));
this.stdout(JSON.stringify(items));
}
}

View File

@ -1,12 +1,14 @@
import fs from 'fs-extra';
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { Folder } from 'lib/models/folder.js';
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';
const fs = require('fs-extra');
const { BaseCommand } = require('./base-command.js');
const { uuid } = require('lib/uuid.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { Setting } = require('lib/models/setting.js');
const { BaseModel } = require('lib/base-model.js');
const { cliUtils } = require('./cli-utils.js');
const { time } = require('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.log(_('Done editing.'));
if (tempFilePath) fs.removeSync(tempFilePath);
}
const textEditorPath = () => {
@ -36,55 +35,75 @@ 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.'));
let note = await app().loadItem(BaseModel.TYPE_NOTE, title);
if (!note) {
const ok = await cliUtils.promptConfirm(_('Note does not exist: "%s". Create it?', title));
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);
this.log(_('Starting to edit note. Close the editor to get back to the prompt.'));
// -------------------------------------------------------------------------
// Start editing the file
// -------------------------------------------------------------------------
await fs.writeFile(tempFilePath, content);
this.logger().info('Disabling fullscreen...');
let watchTimeout = null;
watcher = fs.watch(tempFilePath, (eventType, filename) => {
// We need a timeout because for each change to the file, multiple events are generated.
app().gui().showModalOverlay(_('Starting to edit note. Close the editor to get back to the prompt.'));
await app().gui().forceRender();
const termState = app().gui().term().saveState();
if (watchTimeout) return;
const spawnSync = require('child_process').spawnSync;
spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
watchTimeout = setTimeout(async () => {
let updatedNote = await fs.readFile(tempFilePath, 'utf8');
updatedNote = await Note.unserializeForEdit(updatedNote);
app().gui().term().restoreState(termState);
app().gui().hideModalOverlay();
app().gui().forceRender();
// -------------------------------------------------------------------------
// Save the note and clean up
// -------------------------------------------------------------------------
const updatedContent = await fs.readFile(tempFilePath, 'utf8');
if (updatedContent !== originalContent) {
let updatedNote = await Note.unserializeForEdit(updatedContent);
updatedNote.id = note.id;
await Note.save(updatedNote);
process.stdout.write('.');
watchTimeout = null;
}, 200);
this.stdout(_('Note has been saved.'));
}
this.dispatch({
type: 'NOTE_SELECT',
id: note.id,
});
const childProcess = spawn(editorPath, editorArgs, { stdio: 'inherit' });
childProcess.on('exit', async (error, code) => {
await onFinishedEditing();
});
} catch(error) {
await onFinishedEditing();
throw error;

View File

@ -0,0 +1,21 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
class Command extends BaseCommand {
usage() {
return 'exit';
}
description() {
return _('Exits the application.');
}
async action(args) {
await app().exit();
}
}
module.exports = Command;

View File

@ -0,0 +1,36 @@
const { BaseCommand } = require('./base-command.js');
const { Database } = require('lib/database.js');
const { app } = require('./app.js');
const { Setting } = require('lib/models/setting.js');
const { _ } = require('lib/locale.js');
const { ReportService } = require('lib/services/report.js');
const fs = require('fs-extra');
class Command extends BaseCommand {
usage() {
return 'export-sync-status';
}
description() {
return 'Export sync status';
}
hidden() {
return true;
}
async action(args) {
const service = new ReportService();
const csv = await service.basicItemList({ format: 'csv' });
const filePath = Setting.value('profileDir') + '/syncReport-' + (new Date()).getTime() + '.csv';
await fs.writeFileSync(filePath, csv);
this.stdout('Sync status exported to ' + filePath);
app().gui().showConsole();
app().gui().maximizeConsole();
}
}
module.exports = Command;

View File

@ -1,20 +1,20 @@
import { BaseCommand } from './base-command.js';
import { Exporter } from 'lib/services/exporter.js';
import { BaseModel } from 'lib/base-model.js';
import { Note } from 'lib/models/note.js';
import { reg } from 'lib/registry.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import fs from 'fs-extra';
const { BaseCommand } = require('./base-command.js');
const { Exporter } = require('lib/services/exporter.js');
const { BaseModel } = require('lib/base-model.js');
const { Note } = require('lib/models/note.js');
const { reg } = require('lib/registry.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const fs = require('fs-extra');
class Command extends BaseCommand {
usage() {
return 'export <destination>';
return 'export <directory>';
}
description() {
return _('Exports Joplin data to the given target.');
return _('Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.');
}
options() {
@ -26,7 +26,7 @@ class Command extends BaseCommand {
async action(args) {
let exportOptions = {};
exportOptions.destDir = args.destination;
exportOptions.destDir = args.directory;
exportOptions.writeFile = (filePath, data) => {
return fs.writeFile(filePath, data);
};

View File

@ -1,9 +1,9 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
class Command extends BaseCommand {
@ -21,7 +21,9 @@ class Command extends BaseCommand {
let item = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
if (!item) throw new Error(_('Cannot find "%s".', title));
const url = Note.geolocationUrl(item);
this.log(url);
this.stdout(url);
app().gui().showConsole();
}
}

View File

@ -1,10 +1,11 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { renderCommandHelp } from './help-utils.js';
import { Database } from 'lib/database.js';
import { Setting } from 'lib/models/setting.js';
import { _ } from 'lib/locale.js';
import { ReportService } from 'lib/services/report.js';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { renderCommandHelp } = require('./help-utils.js');
const { Database } = require('lib/database.js');
const { Setting } = require('lib/models/setting.js');
const { wrap } = require('lib/string-utils.js');
const { _ } = require('lib/locale.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
@ -16,19 +17,72 @@ class Command extends BaseCommand {
return _('Displays usage information.');
}
async action(args) {
const commands = args['command'] ? [app().findCommandByName(args['command'])] : app().commands();
allCommands() {
const commands = app().commands();
let output = [];
for (let n in commands) {
if (!commands.hasOwnProperty(n)) continue;
const command = commands[n];
output.push(renderCommandHelp(command));
if (command.hidden()) continue;
if (!command.enabled()) continue;
output.push(command);
}
output.sort();
output.sort((a, b) => a.name() < b.name() ? -1 : +1);
this.log(output.join("\n\n"));
return output;
}
async action(args) {
const stdoutWidth = app().commandStdoutMaxWidth();
if (args.command === 'shortcuts') {
if (app().gui().isDummy()) {
throw new Error(_('Shortcuts are not available in CLI mode.'));
}
const shortcuts = app().gui().shortcuts();
let rows = [];
for (let n in shortcuts) {
if (!shortcuts.hasOwnProperty(n)) continue;
const shortcut = shortcuts[n];
if (!shortcut.description) continue;
n = shortcut.friendlyName ? shortcut.friendlyName : n;
rows.push([n, shortcut.description()]);
}
cliUtils.printArray(this.stdout.bind(this), rows);
} else if (args.command === 'all') {
const commands = this.allCommands();
const output = commands.map((c) => renderCommandHelp(c));
this.stdout(output.join('\n\n'));
} else if (args.command) {
const command = app().findCommandByName(args['command']);
if (!command) throw new Error(_('Cannot find "%s".', args.command));
this.stdout(renderCommandHelp(command, stdoutWidth));
} else {
const commandNames = this.allCommands().map((a) => a.name());
this.stdout(_('Type `help [command]` for more information about a command.'));
this.stdout('');
this.stdout(_('The possible commands are:'));
this.stdout('');
this.stdout(commandNames.join(', '));
this.stdout('');
this.stdout(_('In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.'));
this.stdout('');
this.stdout(_('To move from one pane to another, press Tab or Shift+Tab.'));
this.stdout(_('Use the arrows and page up/down to scroll the lists and text areas (including this console).'));
this.stdout(_('To maximise/minimise the console, press "TC".'));
this.stdout(_('To enter command line mode, press ":"'));
this.stdout(_('To exit command line mode, press ESCAPE'));
this.stdout(_('For the complete list of available keyboard shortcuts, type `help shortcuts`'));
}
app().gui().showConsole();
app().gui().maximizeConsole();
}
}

View File

@ -1,10 +1,10 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
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';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Folder } = require('lib/models/folder.js');
const { importEnex } = require('import-enex');
const { filename, basename } = require('lib/path-utils.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
@ -19,7 +19,6 @@ class Command extends BaseCommand {
options() {
return [
['-f, --force', _('Do not ask for confirmation.')],
['--fuzzy-matching', 'For debugging purposes. Do not use.'],
];
}
@ -32,11 +31,12 @@ class Command extends BaseCommand {
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 cliUtils.promptConfirm(msg);
const ok = force ? true : await this.prompt(msg);
if (!ok) return;
let lastProgress = '';
let options = {
fuzzyMatching: args.options['fuzzy-matching'] === true,
onProgress: (progressState) => {
let line = [];
line.push(_('Found: %d.', progressState.loaded));
@ -45,18 +45,22 @@ 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));
cliUtils.redraw(line.join(' '));
lastProgress = line.join(' ');
cliUtils.redraw(lastProgress);
},
onError: (error) => {
let s = error.trace ? error.trace : error.toString();
this.log(s);
this.stdout(s);
},
}
folder = !folder ? await Folder.save({ title: folderTitle }) : folder;
this.log(_('Importing notes...'));
app().gui().showConsole();
this.stdout(_('Importing notes...'));
await importEnex(folder.id, filePath, options);
cliUtils.redrawDone();
this.stdout(_('The notes have been imported: %s', lastProgress));
}
}

View File

@ -1,13 +1,13 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js';
import { Setting } from 'lib/models/setting.js';
import { Note } from 'lib/models/note.js';
import { sprintf } from 'sprintf-js';
import { time } from 'lib/time-utils.js';
import { cliUtils } from './cli-utils.js';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Setting } = require('lib/models/setting.js');
const { Note } = require('lib/models/note.js');
const { sprintf } = require('sprintf-js');
const { time } = require('lib/time-utils.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
@ -19,14 +19,18 @@ class Command extends BaseCommand {
return _('Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.');
}
enabled() {
return false;
}
options() {
return [
['-n, --limit <num>', _('Displays only the first top <num> notes.')],
['-s, --sort <field>', _('Sorts the item by <field> (eg. title, updated_time, created_time).')],
['-r, --reverse', _('Reverses the sorting order.')],
['-t, --type <type>', _('Displays only the items of the specific type(s). Can be `n` for notes, `t` for todos, or `nt` for notes and todos (eg. `-tt` would display only the todos, while `-ttd` would display notes and todos.')],
['-t, --type <type>', _('Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.')],
['-f, --format <format>', _('Either "text" or "json"')],
['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for todos), TITLE')],
['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE')],
];
}
@ -63,7 +67,7 @@ class Command extends BaseCommand {
}
if (options.format && options.format == 'json') {
this.log(JSON.stringify(items));
this.stdout(JSON.stringify(items));
} else {
let hasTodos = false;
for (let i = 0; i < items.length; i++) {
@ -112,7 +116,7 @@ class Command extends BaseCommand {
rows.push(row);
}
cliUtils.printArray(this.log, rows);
cliUtils.printArray(this.stdout.bind(this), rows);
}
}

View File

@ -1,8 +1,8 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { Folder } from 'lib/models/folder.js';
import { reg } from 'lib/registry.js';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Folder } = require('lib/models/folder.js');
const { reg } = require('lib/registry.js');
class Command extends BaseCommand {

View File

@ -1,7 +1,7 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { Note } from 'lib/models/note.js';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Note } = require('lib/models/note.js');
class Command extends BaseCommand {
@ -23,6 +23,8 @@ class Command extends BaseCommand {
note = await Note.save(note);
Note.updateGeolocation(note.id);
app().switchCurrentFolder(app().currentFolder());
}
}

View File

@ -1,7 +1,7 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { Note } from 'lib/models/note.js';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Note } = require('lib/models/note.js');
class Command extends BaseCommand {
@ -10,7 +10,7 @@ class Command extends BaseCommand {
}
description() {
return _('Creates a new todo.');
return _('Creates a new to-do.');
}
async action(args) {
@ -24,6 +24,8 @@ class Command extends BaseCommand {
note = await Note.save(note);
Note.updateGeolocation(note.id);
app().switchCurrentFolder(app().currentFolder());
}
}

View File

@ -1,22 +1,22 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
class Command extends BaseCommand {
usage() {
return 'mv <note-pattern> [notebook]';
return 'mv <note> [notebook]';
}
description() {
return _('Moves the notes matching <note-pattern> to [notebook].');
return _('Moves the notes matching <note> to [notebook].');
}
async action(args) {
const pattern = args['note-pattern'];
const pattern = args['note'];
const destination = args['notebook'];
const folder = await Folder.loadByField('title', destination);

View File

@ -0,0 +1,40 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
class Command extends BaseCommand {
usage() {
return 'ren <item> <name>';
}
description() {
return _('Renames the given <item> (note or notebook) to <name>.');
}
async action(args) {
const pattern = args['item'];
const name = args['name'];
const item = await app().loadItem('folderOrNote', pattern);
if (!item) throw new Error(_('Cannot find "%s".', pattern));
const newItem = {
id: item.id,
title: name,
type_: item.type_,
};
if (item.type_ === BaseModel.TYPE_FOLDER) {
await Folder.save(newItem);
} else {
await Note.save(newItem);
}
}
}
module.exports = Command;

View File

@ -1,50 +0,0 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { BaseItem } from 'lib/models/base-item.js';
import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js';
import { BaseModel } from 'lib/base-model.js';
import { cliUtils } from './cli-utils.js';
class Command extends BaseCommand {
usage() {
return 'rm <note-pattern>';
}
description() {
return _('Deletes the notes matching <note-pattern>.');
}
options() {
return [
['-f, --force', _('Deletes the items without asking for confirmation.')],
];
}
async action(args) {
const pattern = args['note-pattern'];
const recursive = args.options && args.options.recursive === true;
const force = args.options && args.options.force === true;
// if (recursive) {
// const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
// if (!folder) throw new Error(_('Cannot find "%s".', pattern));
// //const ok = force ? true : await vorpalUtils.cmdPromptConfirm(this, _('Delete notebook "%s"?', folder.title));
// if (!ok) return;
// await Folder.delete(folder.id);
// await app().refreshCurrentFolder();
// } else {
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
if (!notes.length) throw new Error(_('Cannot find "%s".', pattern));
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);
}
}
module.exports = Command;

View File

@ -0,0 +1,40 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseItem } = require('lib/models/base-item.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { BaseModel } = require('lib/base-model.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
usage() {
return 'rmbook <notebook>';
}
description() {
return _('Deletes the given notebook.');
}
options() {
return [
['-f, --force', _('Deletes the notebook without asking for confirmation.')],
];
}
async action(args) {
const pattern = args['notebook'];
const force = args.options && args.options.force === true;
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
const ok = force ? true : await this.prompt(_('Delete notebook "%s"?', folder.title), { booleanAnswerDefault: 'n' });
if (!ok) return;
await Folder.delete(folder.id);
}
}
module.exports = Command;

View File

@ -0,0 +1,41 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseItem } = require('lib/models/base-item.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { BaseModel } = require('lib/base-model.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
usage() {
return 'rmnote <note-pattern>';
}
description() {
return _('Deletes the notes matching <note-pattern>.');
}
options() {
return [
['-f, --force', _('Deletes the notes without asking for confirmation.')],
];
}
async action(args) {
const pattern = args['note-pattern'];
const force = args.options && args.options.force === true;
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
if (!notes.length) throw new Error(_('Cannot find "%s".', pattern));
const ok = force ? true : await this.prompt(notes.length > 1 ? _('%d notes match this pattern. Delete them?', notes.length) : _('Delete note?'), { booleanAnswerDefault: 'n' });
if (!ok) return;
let ids = notes.map((n) => n.id);
await Note.batchDelete(ids);
}
}
module.exports = Command;

View File

@ -1,11 +1,12 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js';
import { sprintf } from 'sprintf-js';
import { time } from 'lib/time-utils.js';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { sprintf } = require('sprintf-js');
const { time } = require('lib/time-utils.js');
const { uuid } = require('lib/uuid.js');
class Command extends BaseCommand {
@ -17,6 +18,10 @@ class Command extends BaseCommand {
return _('Searches for the given <pattern> in all the notes.');
}
compatibleUis() {
return ['gui'];
}
async action(args) {
let pattern = args['pattern'];
let folderTitle = args['notebook'];
@ -27,34 +32,52 @@ class Command extends BaseCommand {
if (!folder) throw new Error(_('Cannot find "%s".', folderTitle));
}
let fields = Note.previewFields();
fields.push('body');
const notes = await Note.previews(folder ? folder.id : null, {
fields: fields,
anywherePattern: '*' + pattern + '*',
const searchId = uuid.create();
this.dispatch({
type: 'SEARCH_ADD',
search: {
id: searchId,
title: pattern,
query_pattern: pattern,
query_folder_id: folder ? folder.id : '',
type_: BaseModel.TYPE_SEARCH,
},
});
const fragmentLength = 50;
this.dispatch({
type: 'SEARCH_SELECT',
id: searchId,
});
let parents = {};
// let fields = Note.previewFields();
// fields.push('body');
// const notes = await Note.previews(folder ? folder.id : null, {
// fields: fields,
// anywherePattern: '*' + pattern + '*',
// });
for (let i = 0; i < notes.length; i++) {
const note = notes[i];
const parent = parents[note.parent_id] ? parents[note.parent_id] : await Folder.load(note.parent_id);
parents[note.parent_id] = parent;
// const fragmentLength = 50;
const idx = note.body.indexOf(pattern);
let line = '';
if (idx >= 0) {
let fragment = note.body.substr(Math.max(0, idx - fragmentLength / 2), fragmentLength);
fragment = fragment.replace(/\n/g, ' ');
line = sprintf('%s: %s / %s: %s', BaseModel.shortId(note.id), parent.title, note.title, fragment);
} else {
line = sprintf('%s: %s / %s', BaseModel.shortId(note.id), parent.title, note.title);
}
// let parents = {};
this.log(line);
}
// for (let i = 0; i < notes.length; i++) {
// const note = notes[i];
// const parent = parents[note.parent_id] ? parents[note.parent_id] : await Folder.load(note.parent_id);
// parents[note.parent_id] = parent;
// const idx = note.body.indexOf(pattern);
// let line = '';
// if (idx >= 0) {
// let fragment = note.body.substr(Math.max(0, idx - fragmentLength / 2), fragmentLength);
// fragment = fragment.replace(/\n/g, ' ');
// line = sprintf('%s: %s / %s: %s', BaseModel.shortId(note.id), parent.title, note.title, fragment);
// } else {
// line = sprintf('%s: %s / %s', BaseModel.shortId(note.id), parent.title, note.title);
// }
// this.stdout(line);
// }
}
}

View File

@ -1,10 +1,10 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js';
import { BaseItem } from 'lib/models/base-item.js';
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { BaseItem } = require('lib/models/base-item.js');
class Command extends BaseCommand {
@ -12,6 +12,10 @@ class Command extends BaseCommand {
return 'set <note> <name> [value]';
}
enabled() {
return false;
}
description() {
return _('Sets the property <name> of the given <note> to the given [value].');
}

Some files were not shown because too many files have changed in this diff Show More