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

494 lines
14 KiB
JavaScript
Raw Normal View History

const BaseApplication = require('@joplin/lib/BaseApplication').default;
const { FoldersScreenUtils } = require('@joplin/lib/folders-screen-utils.js');
const ResourceService = require('@joplin/lib/services/ResourceService').default;
const BaseModel = require('@joplin/lib/BaseModel').default;
const Folder = require('@joplin/lib/models/Folder').default;
const BaseItem = require('@joplin/lib/models/BaseItem').default;
const Note = require('@joplin/lib/models/Note').default;
const Tag = require('@joplin/lib/models/Tag').default;
const Setting = require('@joplin/lib/models/Setting').default;
const { reg } = require('@joplin/lib/registry.js');
const { fileExtension } = require('@joplin/lib/path-utils');
2021-05-01 16:09:59 +02:00
const { splitCommandString } = require('@joplin/lib/string-utils');
const { _ } = require('@joplin/lib/locale');
const fs = require('fs-extra');
const { cliUtils } = require('./cli-utils.js');
const Cache = require('@joplin/lib/Cache');
const RevisionService = require('@joplin/lib/services/RevisionService').default;
const shim = require('@joplin/lib/shim').default;
2017-07-10 22:03:46 +02:00
class Application extends BaseApplication {
2017-07-10 22:03:46 +02:00
constructor() {
super();
2017-07-10 22:03:46 +02:00
this.showPromptString_ = true;
2017-08-04 19:51:01 +02:00
this.commands_ = {};
this.commandMetadata_ = null;
2017-08-04 18:50:12 +02:00
this.activeCommand_ = null;
2017-08-04 19:11:10 +02:00
this.allCommandsLoaded_ = false;
2017-08-21 20:32:43 +02:00
this.showStackTraces_ = false;
2017-10-05 19:17:56 +02:00
this.gui_ = null;
2018-01-17 19:59:33 +02:00
this.cache_ = new Cache();
2017-07-10 22:03:46 +02:00
}
2017-10-09 00:34:01 +02:00
gui() {
return this.gui_;
}
commandStdoutMaxWidth() {
2017-10-24 22:22:57 +02:00
return this.gui().stdoutMaxWidth();
2017-10-09 00:34:01 +02:00
}
2017-07-17 21:19:01 +02:00
async guessTypeAndLoadItem(pattern, options = null) {
let type = BaseModel.TYPE_NOTE;
if (pattern.indexOf('/') === 0) {
2017-07-17 21:19:01 +02:00
type = BaseModel.TYPE_FOLDER;
pattern = pattern.substr(1);
}
return this.loadItem(type, pattern, options);
}
2017-07-15 17:35:40 +02:00
async loadItem(type, pattern, options = null) {
const output = await this.loadItems(type, pattern, options);
if (output.length > 1) {
2017-10-27 00:22:36 +02:00
// output.sort((a, b) => { return a.user_updated_time < b.user_updated_time ? +1 : -1; });
2017-09-24 16:48:23 +02:00
2017-10-27 00:22:36 +02:00
// let answers = { 0: _('[Cancel]') };
// for (let i = 0; i < output.length; i++) {
// answers[i + 1] = output[i].title;
// }
2017-09-24 16:48:23 +02:00
2017-10-08 19:50:43 +02:00
// 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;
}
2017-07-10 22:03:46 +02:00
}
2017-07-11 20:17:23 +02:00
async loadItems(type, pattern, options = null) {
if (type === 'folderOrNote') {
2017-10-27 00:22:36 +02:00
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() : '';
2017-07-15 17:35:40 +02:00
if (type == BaseModel.TYPE_FOLDER && (pattern == Folder.conflictFolderTitle() || pattern == Folder.conflictFolderId())) return [Folder.conflictFolder()];
2017-07-11 20:17:23 +02:00
if (!options) options = {};
2017-07-13 23:26:45 +02:00
2017-07-11 20:17:23 +02:00
const parent = options.parent ? options.parent : app().currentFolder();
const ItemClass = BaseItem.itemClass(type);
if (type == BaseModel.TYPE_NOTE && pattern.indexOf('*') >= 0) {
// Handle it as pattern
if (!parent) throw new Error(_('No notebook selected.'));
2017-07-11 20:17:23 +02:00
return await Note.previews(parent.id, { titlePattern: pattern });
} else {
// Single item
2017-07-11 20:17:23 +02:00
let item = null;
if (type == BaseModel.TYPE_NOTE) {
if (!parent) throw new Error(_('No notebook has been specified.'));
item = await ItemClass.loadFolderNoteByField(parent.id, 'title', pattern);
2017-07-11 20:17:23 +02:00
} else {
item = await ItemClass.loadByTitle(pattern);
}
if (item) return [item];
item = await ItemClass.load(pattern); // Load by id
if (item) return [item];
if (pattern.length >= 2) {
return await ItemClass.loadByPartialId(pattern);
2017-07-11 20:17:23 +02:00
}
}
return [];
2017-07-10 22:03:46 +02:00
}
stdout(text) {
return this.gui().stdout(text);
}
2017-10-07 19:07:38 +02:00
setupCommand(cmd) {
cmd.setStdout(text => {
return this.stdout(text);
2017-10-07 19:07:38 +02:00
});
cmd.setDispatcher(action => {
if (this.store()) {
return this.store().dispatch(action);
} else {
return () => {};
}
2017-10-14 20:03:23 +02:00
});
2017-10-07 19:07:38 +02:00
cmd.setPrompt(async (message, options) => {
2017-10-28 19:55:45 +02:00
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')];
2017-10-28 19:55:45 +02:00
if (options.type == 'boolean') {
2019-09-19 23:51:18 +02:00
message += ` (${options.answers.join('/')})`;
2017-10-07 19:07:38 +02:00
}
2019-09-19 23:51:18 +02:00
let answer = await this.gui().prompt('', `${message} `, options);
2017-10-15 18:57:09 +02:00
if (options.type === 'boolean') {
2017-10-28 19:55:45 +02:00
if (answer === null) return false; // Pressed ESCAPE
if (!answer) answer = options.answers[0];
const positiveIndex = options.booleanAnswerDefault == 'y' ? 0 : 1;
2017-10-28 19:55:45 +02:00
return answer.toLowerCase() === options.answers[positiveIndex].toLowerCase();
2017-12-12 20:17:30 +02:00
} else {
return answer;
2017-10-07 19:07:38 +02:00
}
});
return cmd;
}
2017-10-09 22:29:49 +02:00
async exit(code = 0) {
const doExit = async () => {
this.gui().exit();
await super.exit(code);
};
// Give it a few seconds to cancel otherwise exit anyway
shim.setTimeout(async () => {
await doExit();
}, 5000);
if (await reg.syncTarget().syncStarted()) {
this.stdout(_('Cancelling background synchronisation... Please wait.'));
const sync = await reg.syncTarget().synchronizer();
await sync.cancel();
}
await doExit();
2017-10-09 22:29:49 +02:00
}
2017-12-07 20:12:46 +02:00
commands(uiType = null) {
if (!this.allCommandsLoaded_) {
fs.readdirSync(__dirname).forEach(path => {
if (path.indexOf('command-') !== 0) return;
const ext = fileExtension(path);
if (ext != 'js') return;
2017-12-07 20:12:46 +02:00
const CommandClass = require(`./${path}`);
2017-12-07 20:12:46 +02:00
let cmd = new CommandClass();
if (!cmd.enabled()) return;
cmd = this.setupCommand(cmd);
this.commands_[cmd.name()] = cmd;
});
this.allCommandsLoaded_ = true;
}
2017-08-04 19:11:10 +02:00
2017-12-07 20:12:46 +02:00
if (uiType !== null) {
const temp = [];
for (const n in this.commands_) {
2017-12-07 20:12:46 +02:00
if (!this.commands_.hasOwnProperty(n)) continue;
const c = this.commands_[n];
if (!c.supportsUi(uiType)) continue;
temp[n] = c;
}
return temp;
}
2017-08-04 19:11:10 +02:00
return this.commands_;
}
async commandNames() {
const metadata = await this.commandMetadata();
const output = [];
for (const n in metadata) {
2017-08-04 19:11:10 +02:00
if (!metadata.hasOwnProperty(n)) continue;
output.push(n);
}
return output;
2017-07-10 22:03:46 +02:00
}
2017-08-04 19:51:01 +02:00
async commandMetadata() {
if (this.commandMetadata_) return this.commandMetadata_;
2017-07-24 22:36:49 +02:00
let output = await this.cache_.getItem('metadata');
2018-01-17 19:59:33 +02:00
if (output) {
2017-08-04 19:51:01 +02:00
this.commandMetadata_ = output;
return Object.assign({}, this.commandMetadata_);
}
2017-08-04 19:11:10 +02:00
const commands = this.commands();
2017-07-24 22:36:49 +02:00
2017-08-04 19:51:01 +02:00
output = {};
for (const n in commands) {
2017-08-04 19:11:10 +02:00
if (!commands.hasOwnProperty(n)) continue;
const cmd = commands[n];
2017-08-04 19:51:01 +02:00
output[n] = cmd.metadata();
2017-07-24 22:36:49 +02:00
}
2017-08-04 19:51:01 +02:00
await this.cache_.setItem('metadata', output, 1000 * 60 * 60 * 24);
2017-08-04 19:51:01 +02:00
this.commandMetadata_ = output;
return Object.assign({}, this.commandMetadata_);
2017-07-24 22:36:49 +02:00
}
hasGui() {
return this.gui() && !this.gui().isDummy();
}
2017-08-03 19:48:14 +02:00
findCommandByName(name) {
2017-08-04 19:11:10 +02:00
if (this.commands_[name]) return this.commands_[name];
2017-08-03 19:48:14 +02:00
let CommandClass = null;
try {
2019-09-19 23:51:18 +02:00
CommandClass = require(`${__dirname}/command-${name}.js`);
2017-08-03 19:48:14 +02:00
} catch (error) {
if (error.message && error.message.indexOf('Cannot find module') >= 0) {
const e = new Error(_('No such command: %s', name));
e.type = 'notFound';
throw e;
} else {
throw error;
}
2017-08-03 19:48:14 +02:00
}
2017-10-06 19:38:17 +02:00
2017-08-03 19:48:14 +02:00
let cmd = new CommandClass();
2017-10-07 19:07:38 +02:00
cmd = this.setupCommand(cmd);
2017-08-04 19:11:10 +02:00
this.commands_[name] = cmd;
return this.commands_[name];
2017-08-03 19:48:14 +02:00
}
dummyGui() {
return {
isDummy: () => {
return true;
},
prompt: (initialText = '', promptString = '', options = null) => {
return cliUtils.prompt(initialText, promptString, options);
},
showConsole: () => {},
maximizeConsole: () => {},
stdout: text => {
console.info(text);
},
fullScreen: () => {},
exit: () => {},
showModalOverlay: () => {},
2017-10-24 21:52:26 +02:00
hideModalOverlay: () => {},
stdoutMaxWidth: () => {
return 100;
},
forceRender: () => {},
termSaveState: () => {},
termRestoreState: () => {},
};
}
2017-08-03 19:48:14 +02:00
async execCommand(argv) {
if (!argv.length) return this.execCommand(['help']);
// reg.logger().debug('execCommand()', argv);
2017-08-03 19:48:14 +02:00
const commandName = argv[0];
2017-08-04 18:50:12 +02:00
this.activeCommand_ = this.findCommandByName(commandName);
let outException = null;
try {
if (this.gui().isDummy() && !this.activeCommand_.supportsUi('cli')) throw new Error(_('The command "%s" is only available in GUI mode', this.activeCommand_.name()));
const cmdArgs = cliUtils.makeCommandArgs(this.activeCommand_, argv);
await this.activeCommand_.action(cmdArgs);
} catch (error) {
outException = error;
}
2017-10-14 20:03:23 +02:00
this.activeCommand_ = null;
if (outException) throw outException;
2017-08-04 18:50:12 +02:00
}
2017-08-20 16:29:18 +02:00
currentCommand() {
return this.activeCommand_;
2017-08-03 19:48:14 +02:00
}
async loadKeymaps() {
const defaultKeyMap = [
{ keys: [':'], type: 'function', command: 'enter_command_line_mode' },
{ keys: ['TAB'], type: 'function', command: 'focus_next' },
{ keys: ['SHIFT_TAB'], type: 'function', command: 'focus_previous' },
{ keys: ['UP'], type: 'function', command: 'move_up' },
{ keys: ['DOWN'], type: 'function', command: 'move_down' },
{ keys: ['PAGE_UP'], type: 'function', command: 'page_up' },
{ keys: ['PAGE_DOWN'], type: 'function', command: 'page_down' },
{ keys: ['ENTER'], type: 'function', command: 'activate' },
{ keys: ['DELETE', 'BACKSPACE'], type: 'function', command: 'delete' },
2020-05-22 13:50:56 +02:00
{ keys: ['n'], type: 'function', command: 'next_link' },
{ keys: ['b'], type: 'function', command: 'previous_link' },
{ keys: ['o'], type: 'function', command: 'open_link' },
{ keys: [' '], command: 'todo toggle $n' },
{ keys: ['tc'], type: 'function', command: 'toggle_console' },
{ keys: ['tm'], type: 'function', command: 'toggle_metadata' },
{ keys: ['/'], type: 'prompt', command: 'search ""', cursorPosition: -2 },
{ keys: ['mn'], type: 'prompt', command: 'mknote ""', cursorPosition: -2 },
{ keys: ['mt'], type: 'prompt', command: 'mktodo ""', cursorPosition: -2 },
{ keys: ['mb'], type: 'prompt', command: 'mkbook ""', cursorPosition: -2 },
{ keys: ['yn'], type: 'prompt', command: 'cp $n ""', cursorPosition: -2 },
{ keys: ['dn'], type: 'prompt', command: 'mv $n ""', cursorPosition: -2 },
];
// Filter the keymap item by command so that items in keymap.json can override
// the default ones.
const itemsByCommand = {};
for (let i = 0; i < defaultKeyMap.length; i++) {
itemsByCommand[defaultKeyMap[i].command] = defaultKeyMap[i];
}
2019-09-19 23:51:18 +02:00
const filePath = `${Setting.value('profileDir')}/keymap.json`;
if (await fs.pathExists(filePath)) {
try {
let configString = await fs.readFile(filePath, 'utf-8');
configString = configString.replace(/^\s*\/\/.*/, ''); // Strip off comments
const keymap = JSON.parse(configString);
for (let keymapIndex = 0; keymapIndex < keymap.length; keymapIndex++) {
const item = keymap[keymapIndex];
itemsByCommand[item.command] = item;
}
} catch (error) {
let msg = error.message ? error.message : '';
2019-09-19 23:51:18 +02:00
msg = `Could not load keymap ${filePath}\n${msg}`;
error.message = msg;
throw error;
}
}
const output = [];
for (const n in itemsByCommand) {
if (!itemsByCommand.hasOwnProperty(n)) continue;
output.push(itemsByCommand[n]);
}
// Map reserved shortcuts to their equivalent key
// https://github.com/cronvel/terminal-kit/issues/101
for (let i = 0; i < output.length; i++) {
const newKeys = output[i].keys.map(k => {
k = k.replace(/CTRL_H/g, 'BACKSPACE');
k = k.replace(/CTRL_I/g, 'TAB');
k = k.replace(/CTRL_M/g, 'ENTER');
return k;
});
output[i].keys = newKeys;
}
return output;
}
2021-05-01 16:09:59 +02:00
async commandList(argv) {
if (argv.length && argv[0] === 'batch') {
const commands = [];
const commandLines = (await fs.readFile(argv[1], 'utf-8')).split('\n');
for (const commandLine of commandLines) {
if (!commandLine.trim()) continue;
const splitted = splitCommandString(commandLine.trim());
commands.push(splitted);
}
return commands;
} else {
return [argv];
}
}
// We need this special case here because by the time the `version` command
// runs, the keychain has already been setup.
checkIfKeychainEnabled(argv) {
return argv.indexOf('version') < 0;
}
async start(argv) {
const keychainEnabled = this.checkIfKeychainEnabled(argv);
argv = await super.start(argv, { keychainEnabled });
cliUtils.setStdout(object => {
2017-10-25 19:41:36 +02:00
return this.stdout(object);
});
// If we have some arguments left at this point, it's a command
// so execute it.
if (argv.length) {
this.gui_ = this.dummyGui();
this.currentFolder_ = await Folder.load(Setting.value('activeFolderId'));
2017-12-07 20:12:46 +02:00
await this.applySettingsSideEffects();
try {
2021-05-01 16:09:59 +02:00
const commands = await this.commandList(argv);
for (const command of commands) {
await this.execCommand(command);
}
} catch (error) {
if (this.showStackTraces_) {
2018-02-25 19:01:16 +02:00
console.error(error);
} else {
console.info(error.message);
}
2018-02-25 19:01:16 +02:00
process.exit(1);
}
await Setting.saveAll();
// Need to call exit() explicitly, otherwise Node wait for any timeout to complete
// https://stackoverflow.com/questions/18050095
process.exit(0);
} else {
// Otherwise open the GUI
this.initRedux();
const keymap = await this.loadKeymaps();
const AppGui = require('./app-gui.js');
this.gui_ = new AppGui(this, this.store(), keymap);
this.gui_.setLogger(this.logger());
await this.gui_.start();
2017-10-21 18:53:43 +02:00
// Since the settings need to be loaded before the store is created, it will never
2017-11-08 23:22:24 +02:00
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
2017-10-21 18:53:43 +02:00
// initialised. So we manually call dispatchUpdateAll() to force an update.
Setting.dispatchUpdateAll();
await FoldersScreenUtils.refreshFolders();
2017-10-22 19:12:16 +02:00
const tags = await Tag.allWithNotes();
2018-03-16 16:32:47 +02:00
ResourceService.runInBackground();
RevisionService.instance().runInBackground();
2017-10-22 19:12:16 +02:00
this.dispatch({
type: 'TAG_UPDATE_ALL',
items: tags,
2017-10-22 19:12:16 +02:00
});
this.store().dispatch({
type: 'FOLDER_SELECT',
id: Setting.value('activeFolderId'),
});
}
2017-07-10 22:03:46 +02:00
}
}
let application_ = null;
function app() {
if (application_) return application_;
application_ = new Application();
return application_;
}
module.exports = { app };