Merge branch 'master' into alarm-support
5
.gitignore
vendored
@ -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
@ -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
BIN
Assets/DemoDesktop.PNG
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
Assets/DemoDesktop.psd
Normal file
BIN
Assets/Icon-Android-1024.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
Assets/Icon-Android-512.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
Assets/Icon-Android.psd
Normal file
BIN
Assets/Icon-ios-512.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
Assets/Icon-ios.psd
Normal file
BIN
Assets/Joplin.ico
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
Assets/Laptop-Terminal.psd
Normal file
BIN
Assets/LinuxIcons/1024x1024.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
Assets/LinuxIcons/128x128.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
Assets/LinuxIcons/144x144.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
Assets/LinuxIcons/16x16.png
Normal file
After Width: | Height: | Size: 679 B |
BIN
Assets/LinuxIcons/24x24.png
Normal file
After Width: | Height: | Size: 968 B |
BIN
Assets/LinuxIcons/256x256.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
Assets/LinuxIcons/32x32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
Assets/LinuxIcons/48x48.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
Assets/LinuxIcons/512x512.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
Assets/LinuxIcons/72x72.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
Assets/LinuxIcons/96x96.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 170 KiB |
BIN
Assets/Screenshots/Screenshot_1511192771.png
Normal file
After Width: | Height: | Size: 205 KiB |
BIN
Assets/Screenshots/Screenshot_1511192805.png
Normal file
After Width: | Height: | Size: 300 KiB |
BIN
Assets/Screenshots/Screenshot_1511193130.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
Assets/Screenshots/Screenshot_1511193142.png
Normal file
After Width: | Height: | Size: 277 KiB |
BIN
Assets/Screenshots/Screenshot_1511193169.png
Normal file
After Width: | Height: | Size: 133 KiB |
BIN
Assets/Screenshots/Screenshot_1511193181.png
Normal file
After Width: | Height: | Size: 214 KiB |
BIN
Assets/Screenshots/Screenshot_1511193188.png
Normal file
After Width: | Height: | Size: 181 KiB |
BIN
Assets/Screenshots/Screenshot_1511193192.png
Normal file
After Width: | Height: | Size: 133 KiB |
BIN
Assets/Screenshots/iOS/Screenshot_iPad_Paysage.jpg
Normal file
After Width: | Height: | Size: 288 KiB |
BIN
Assets/Screenshots/iOS/Screenshot_iPad_Paysage.png
Normal file
After Width: | Height: | Size: 335 KiB |
BIN
Assets/Screenshots/iOS/Screenshot_iPad_Paysage.psd
Normal file
BIN
Assets/Screenshots/iOS/Screenshot_iPhone_Portrait.jpg
Normal file
After Width: | Height: | Size: 226 KiB |
BIN
Assets/Screenshots/iOS/Screenshot_iPhone_Portrait.png
Normal file
After Width: | Height: | Size: 254 KiB |
BIN
Assets/Screenshots/iOS/Screenshot_iPhone_Portrait.psd
Normal file
BIN
Assets/Tablet.psd
Normal file
2
Assets/build_mac_icons.sh
Normal file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
iconutil --convert icns macOs.iconset
|
2
Assets/check-square-o.svg
Normal 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
@ -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
After Width: | Height: | Size: 1.1 KiB |
BIN
Assets/iOSIcons/29x2.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
Assets/iOSIcons/29x3.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
Assets/iOSIcons/40.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
Assets/iOSIcons/40x2.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
Assets/iOSIcons/40x3.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
Assets/iOSIcons/57.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
Assets/iOSIcons/57x2.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
Assets/iOSIcons/60x2.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
Assets/iOSIcons/60x3.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
Assets/iOSIcons/AppStore.png
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
Assets/macOs.icns
Normal file
BIN
Assets/macOs.iconset/icon_1024x1024.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
Assets/macOs.iconset/icon_128x128.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
Assets/macOs.iconset/icon_16x16.png
Normal file
After Width: | Height: | Size: 679 B |
BIN
Assets/macOs.iconset/icon_256x256.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
Assets/macOs.iconset/icon_32x32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
Assets/macOs.iconset/icon_512x512.png
Normal file
After Width: | Height: | Size: 28 KiB |
2
Assets/square-o.svg
Normal 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 |
@ -1,4 +0,0 @@
|
||||
{
|
||||
"presets": ["env", "react"],
|
||||
"plugins": ["syntax-async-functions","transform-runtime"]
|
||||
}
|
4
CliClient/.gitignore
vendored
@ -16,4 +16,6 @@ tests/cli-integration/
|
||||
*.mo
|
||||
*.*~
|
||||
tests/sync
|
||||
out.txt
|
||||
out.txt
|
||||
linkToLocal.sh
|
||||
yarn-error.log
|
97
CliClient/app/ResourceServer.js
Normal 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
@ -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;
|
@ -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);
|
||||
return output.length ? output[0] : null;
|
||||
|
||||
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
|
||||
stdout(text) {
|
||||
return this.gui().stdout(text);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
setupCommand(cmd) {
|
||||
cmd.setStdout((text) => {
|
||||
return this.stdout(text);
|
||||
});
|
||||
|
||||
if (arg == '--env') {
|
||||
if (!nextArg) throw new Error(_('Usage: %s', '--env <dev|prod>'));
|
||||
matched.env = nextArg;
|
||||
argv.splice(0, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
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) => {};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!matched.logLevel) matched.logLevel = Logger.LEVEL_INFO;
|
||||
if (!matched.env) matched.env = 'prod';
|
||||
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')];
|
||||
|
||||
return {
|
||||
matched: matched,
|
||||
argv: argv,
|
||||
if (options.type == 'boolean') {
|
||||
message += ' (' + options.answers.join('/') + ')';
|
||||
}
|
||||
|
||||
let answer = await this.gui().prompt('', message + ' ');
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// Give it a few seconds to cancel otherwise exit anyway
|
||||
setTimeout(async () => {
|
||||
await doExit();
|
||||
}, 5000);
|
||||
|
||||
shellArgsToString(args) {
|
||||
let output = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
output.push(this.escapeShellArg(args[i]));
|
||||
if (await reg.syncTarget().syncStarted()) {
|
||||
this.stdout(_('Cancelling background synchronisation... Please wait.'));
|
||||
const sync = await reg.syncTarget().synchronizer();
|
||||
await sync.cancel();
|
||||
}
|
||||
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,132 +250,97 @@ 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);
|
||||
const cmdArgs = cliUtils.makeCommandArgs(this.activeCommand_, argv);
|
||||
await this.activeCommand_.action(cmdArgs);
|
||||
|
||||
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');
|
||||
// If we have some arguments left at this point, it's a command
|
||||
// so execute it.
|
||||
if (argv.length) {
|
||||
this.gui_ = this.dummyGui();
|
||||
|
||||
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;
|
||||
try {
|
||||
await this.execCommand(argv);
|
||||
} catch (error) {
|
||||
if (this.showStackTraces_) {
|
||||
console.info(error);
|
||||
} else {
|
||||
console.info(error.message);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
} else { // Otherwise open the GUI
|
||||
this.initRedux();
|
||||
|
||||
try {
|
||||
await this.execCommand(argv);
|
||||
} catch (error) {
|
||||
if (this.showStackTraces_) {
|
||||
console.info(error);
|
||||
} else {
|
||||
console.info(error.message);
|
||||
}
|
||||
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 };
|
@ -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 };
|
@ -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__|
|
@ -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 };
|
@ -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.');
|
||||
|
@ -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) => {
|
||||
|
@ -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,13 +108,75 @@ const headerHtml = `
|
||||
padding-left: 2em;
|
||||
padding-right: 2em;
|
||||
padding-bottom: 2em;
|
||||
padding-top: 2em;
|
||||
}
|
||||
.forkme {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top:0;
|
||||
}
|
||||
</style>
|
||||
.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>
|
||||
|
||||
<body>
|
||||
@ -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(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');
|
||||
function stickyHeader() {
|
||||
if ($(window).scrollTop() > 179) {
|
||||
$('.nav').addClass('sticky');
|
||||
} else {
|
||||
$('.nav').removeClass('sticky');
|
||||
}
|
||||
}
|
||||
|
||||
ga('create', 'UA-103586105-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
$(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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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 };
|
31
CliClient/app/command-attach.js
Normal 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;
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
updatedNote.id = note.id;
|
||||
await Note.save(updatedNote);
|
||||
process.stdout.write('.');
|
||||
watchTimeout = null;
|
||||
}, 200);
|
||||
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);
|
||||
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();
|
||||
});
|
||||
await onFinishedEditing();
|
||||
|
||||
} catch(error) {
|
||||
await onFinishedEditing();
|
||||
throw error;
|
||||
|
21
CliClient/app/command-exit.js
Normal 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;
|
36
CliClient/app/command-export-sync-status.js
Normal 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;
|
@ -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);
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
@ -18,15 +18,19 @@ class Command extends BaseCommand {
|
||||
description() {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
40
CliClient/app/command-ren.js
Normal 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;
|
@ -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;
|
40
CliClient/app/command-rmbook.js
Normal 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;
|
41
CliClient/app/command-rmnote.js
Normal 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;
|
@ -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);
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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].');
|
||||
}
|
||||
|