1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-12 08:54:00 +02:00
joplin/CliClient/app/app-gui.js

844 lines
22 KiB
JavaScript
Raw Normal View History

const { Logger } = require('lib/logger.js');
2017-12-14 20:12:14 +02:00
const Folder = require('lib/models/Folder.js');
const Tag = require('lib/models/Tag.js');
const BaseModel = require('lib/BaseModel.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');
2017-10-05 19:17:56 +02:00
2017-10-13 19:25:58 +02:00
const chalk = require('chalk');
2017-10-05 19:17:56 +02:00
const tk = require('terminal-kit');
2017-10-13 20:21:57 +02:00
const TermWrapper = require('tkwidgets/framework/TermWrapper.js');
2017-10-05 19:17:56 +02:00
const Renderer = require('tkwidgets/framework/Renderer.js');
const DecryptionWorker = require('lib/services/DecryptionWorker');
2017-10-05 19:17:56 +02:00
2017-10-07 18:30:27 +02:00
const BaseWidget = require('tkwidgets/BaseWidget.js');
2017-10-05 19:17:56 +02:00
const ListWidget = require('tkwidgets/ListWidget.js');
const TextWidget = require('tkwidgets/TextWidget.js');
const HLayoutWidget = require('tkwidgets/HLayoutWidget.js');
const VLayoutWidget = require('tkwidgets/VLayoutWidget.js');
2017-10-07 23:01:03 +02:00
const ReduxRootWidget = require('tkwidgets/ReduxRootWidget.js');
2017-10-05 19:17:56 +02:00
const RootWidget = require('tkwidgets/RootWidget.js');
const WindowWidget = require('tkwidgets/WindowWidget.js');
2017-10-07 23:01:03 +02:00
const NoteWidget = require('./gui/NoteWidget.js');
const ResourceServer = require('./ResourceServer.js');
const NoteMetadataWidget = require('./gui/NoteMetadataWidget.js');
2017-10-08 00:17:10 +02:00
const FolderListWidget = require('./gui/FolderListWidget.js');
const NoteListWidget = require('./gui/NoteListWidget.js');
2017-10-15 18:57:09 +02:00
const StatusBarWidget = require('./gui/StatusBarWidget.js');
const ConsoleWidget = require('./gui/ConsoleWidget.js');
2017-10-07 23:01:03 +02:00
2017-10-05 19:17:56 +02:00
class AppGui {
2017-10-07 23:01:03 +02:00
constructor(app, store) {
2017-10-05 19:17:56 +02:00
this.app_ = app;
2017-10-07 23:01:03 +02:00
this.store_ = store;
2017-10-07 18:30:27 +02:00
BaseWidget.setLogger(app.logger());
2017-10-13 20:21:57 +02:00
this.term_ = new TermWrapper(tk.terminal);
2017-10-05 19:17:56 +02:00
this.renderer_ = null;
this.logger_ = new Logger();
2017-10-07 23:01:03 +02:00
this.buildUi();
2017-10-06 19:38:17 +02:00
this.renderer_ = new Renderer(this.term(), this.rootWidget_);
this.app_.on('modelAction', async (event) => {
await this.handleModelAction(event.action);
});
2017-10-07 18:30:27 +02:00
this.shortcuts_ = this.setupShortcuts();
2017-10-09 00:34:01 +02:00
this.inputMode_ = AppGui.INPUT_MODE_NORMAL;
2017-10-14 20:03:23 +02:00
this.commandCancelCalled_ = false;
this.currentShortcutKeys_ = [];
2017-10-19 00:13:53 +02:00
this.lastShortcutKeyTime_ = 0;
2017-10-14 20:03:23 +02:00
2017-10-19 00:13:53 +02:00
// 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();
DecryptionWorker.instance().scheduleStart();
2017-10-14 20:03:23 +02:00
}
store() {
return this.store_;
}
2017-10-14 20:03:23 +02:00
renderer() {
return this.renderer_;
2017-10-06 19:38:17 +02:00
}
2017-10-05 19:17:56 +02:00
2017-10-14 23:44:50 +02:00
async forceRender() {
this.widget('root').invalidate();
await this.renderer_.renderRoot();
}
termSaveState() {
return this.term().saveState();
}
termRestoreState(state) {
return this.term().restoreState(state);
}
2017-12-12 20:17:30 +02:00
prompt(initialText = '', promptString = ':', options = null) {
return this.widget('statusBar').prompt(initialText, promptString, options);
}
2017-10-24 22:22:57 +02:00
stdoutMaxWidth() {
return this.widget('console').innerWidth - 1;
}
isDummy() {
return false;
}
2017-10-06 19:38:17 +02:00
buildUi() {
2017-10-07 23:01:03 +02:00
this.rootWidget_ = new ReduxRootWidget(this.store_);
2017-10-09 00:34:01 +02:00
this.rootWidget_.name = 'root';
2017-10-05 19:17:56 +02:00
2017-10-08 00:17:10 +02:00
const folderList = new FolderListWidget();
folderList.style = {
borderBottomWidth: 1,
borderRightWidth: 1,
};
2017-10-08 19:50:43 +02:00
folderList.name = 'folderList';
2017-10-09 20:05:01 +02:00
folderList.vStretch = true;
folderList.on('currentItemChange', async (event) => {
2017-10-22 19:12:16 +02:00
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
2017-11-08 23:22:24 +02:00
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()) {
2017-10-22 19:12:16 +02:00
this.store_.dispatch({
2017-11-08 23:22:24 +02:00
type: 'FOLDER_SELECT',
id: item ? item.id : null,
2017-10-22 19:12:16 +02:00
});
} else if (item.type_ === Tag.modelType()) {
this.store_.dispatch({
2017-11-08 23:22:24 +02:00
type: 'TAG_SELECT',
id: item ? item.id : null,
2017-10-22 19:12:16 +02:00
});
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
this.store_.dispatch({
type: 'SEARCH_SELECT',
id: item ? item.id : null,
});
2017-10-22 19:12:16 +02:00
}
2017-10-08 00:17:10 +02:00
});
this.rootWidget_.connect(folderList, (state) => {
return {
selectedFolderId: state.selectedFolderId,
2017-10-22 19:12:16 +02:00
selectedTagId: state.selectedTagId,
selectedSearchId: state.selectedSearchId,
2017-10-22 19:12:16 +02:00
notesParentType: state.notesParentType,
folders: state.folders,
tags: state.tags,
searches: state.searches,
2017-10-08 00:17:10 +02:00
};
2017-10-05 19:17:56 +02:00
});
const noteList = new NoteListWidget();
2017-10-08 19:50:43 +02:00
noteList.name = 'noteList';
2017-10-09 20:05:01 +02:00
noteList.vStretch = true;
noteList.style = {
2017-10-05 19:17:56 +02:00
borderBottomWidth: 1,
borderLeftWidth: 1,
borderRightWidth: 1,
2017-10-09 20:05:01 +02:00
};
2017-10-05 19:17:56 +02:00
noteList.on('currentItemChange', async () => {
2017-10-07 22:04:53 +02:00
let note = noteList.currentItem;
2017-10-07 23:01:03 +02:00
this.store_.dispatch({
2017-11-08 23:22:24 +02:00
type: 'NOTE_SELECT',
id: note ? note.id : null,
2017-10-07 23:01:03 +02:00
});
2017-10-08 00:17:10 +02:00
});
this.rootWidget_.connect(noteList, (state) => {
return {
2017-11-22 20:35:31 +02:00
selectedNoteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
2017-10-08 00:17:10 +02:00
items: state.notes,
};
2017-10-05 19:17:56 +02:00
});
2017-10-07 23:01:03 +02:00
const noteText = new NoteWidget();
noteText.hStretch = true;
2017-10-08 19:50:43 +02:00
noteText.name = 'noteText';
noteText.style = {
borderBottomWidth: 1,
borderLeftWidth: 1,
};
2017-10-07 23:01:03 +02:00
this.rootWidget_.connect(noteText, (state) => {
2017-10-28 19:44:28 +02:00
return {
2017-11-22 20:35:31 +02:00
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
2017-10-28 19:44:28 +02:00
notes: state.notes,
};
2017-10-07 23:01:03 +02:00
});
const noteMetadata = new NoteMetadataWidget();
noteMetadata.hStretch = true;
noteMetadata.name = 'noteMetadata';
noteMetadata.style = {
borderBottomWidth: 1,
borderLeftWidth: 1,
borderRightWidth: 1,
};
this.rootWidget_.connect(noteMetadata, (state) => {
2017-11-22 20:35:31 +02:00
return { noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null };
});
noteMetadata.hide();
2017-10-06 19:38:17 +02:00
const consoleWidget = new ConsoleWidget();
2017-10-09 20:05:01 +02:00
consoleWidget.hStretch = true;
consoleWidget.style = {
borderBottomWidth: 1,
};
consoleWidget.hide();
2017-10-15 18:57:09 +02:00
const statusBar = new StatusBarWidget();
statusBar.hStretch = true;
2017-10-06 19:38:17 +02:00
const noteLayout = new VLayoutWidget();
noteLayout.name = 'noteLayout';
noteLayout.addChild(noteText, { type: 'stretch', factor: 1 });
noteLayout.addChild(noteMetadata, { type: 'stretch', factor: 1 });
2017-10-06 20:01:10 +02:00
const hLayout = new HLayoutWidget();
2017-10-08 19:50:43 +02:00
hLayout.name = 'hLayout';
2017-10-06 20:01:10 +02:00
hLayout.addChild(folderList, { type: 'stretch', factor: 1 });
hLayout.addChild(noteList, { type: 'stretch', factor: 1 });
hLayout.addChild(noteLayout, { type: 'stretch', factor: 2 });
2017-10-05 19:17:56 +02:00
2017-10-06 19:38:17 +02:00
const vLayout = new VLayoutWidget();
2017-10-08 19:50:43 +02:00
vLayout.name = 'vLayout';
vLayout.addChild(hLayout, { type: 'stretch', factor: 2 });
vLayout.addChild(consoleWidget, { type: 'stretch', factor: 1 });
2017-10-15 18:57:09 +02:00
vLayout.addChild(statusBar, { type: 'fixed', factor: 1 });
2017-10-05 19:17:56 +02:00
const win1 = new WindowWidget();
2017-10-06 19:38:17 +02:00
win1.addChild(vLayout);
2017-10-08 19:50:43 +02:00
win1.name = 'mainWindow';
2017-10-05 19:17:56 +02:00
2017-10-07 23:01:03 +02:00
this.rootWidget_.addChild(win1);
2017-10-05 19:17:56 +02:00
}
2017-10-24 21:52:26 +02:00
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) {
2017-10-28 19:55:45 +02:00
if (!cmd) return;
this.stdout(chalk.cyan.bold('> ' + cmd));
}
2017-10-07 18:30:27 +02:00
setupShortcuts() {
const shortcuts = {};
2017-10-24 22:22:57 +02:00
shortcuts['TAB'] = {
friendlyName: 'Tab',
2017-10-31 00:43:11 +02:00
description: () => _('Give focus to next pane'),
2017-10-24 22:22:57 +02:00
isDocOnly: true,
}
shortcuts['SHIFT_TAB'] = {
friendlyName: 'Shift+Tab',
2017-10-31 00:43:11 +02:00
description: () => _('Give focus to previous pane'),
2017-10-24 22:22:57 +02:00
isDocOnly: true,
}
2017-10-29 17:41:30 +02:00
shortcuts[':'] = {
2017-10-31 00:43:11 +02:00
description: () => _('Enter command line mode'),
2017-10-29 17:41:30 +02:00
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
2017-10-31 00:43:11 +02:00
description: () => _('Exit command line mode'),
2017-10-29 17:41:30 +02:00
isDocOnly: true,
};
shortcuts['ENTER'] = {
2017-10-31 00:43:11 +02:00
description: () => _('Edit the selected note'),
2017-10-29 17:41:30 +02:00
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'] = {
2017-10-31 00:43:11 +02:00
description: () => _('Cancel the current command.'),
2017-10-29 17:41:30 +02:00
friendlyName: 'Ctrl+C',
isDocOnly: true,
}
shortcuts['CTRL_D'] = {
2017-10-31 00:43:11 +02:00
description: () => _('Exit the application.'),
2017-10-29 17:41:30 +02:00
friendlyName: 'Ctrl+D',
isDocOnly: true,
}
2017-10-09 00:34:01 +02:00
shortcuts['DELETE'] = {
2017-10-31 00:43:11 +02:00
description: () => _('Delete the currently selected note or notebook.'),
action: async () => {
if (this.widget('folderList').hasFocus) {
const item = this.widget('folderList').selectedJoplinItem;
if (!item) return;
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({
2017-11-17 20:57:27 +02:00
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.'));
}
}
2017-10-09 00:34:01 +02:00
};
2017-10-08 19:50:43 +02:00
shortcuts['BACKSPACE'] = {
alias: 'DELETE',
};
2017-10-09 00:34:01 +02:00
shortcuts[' '] = {
friendlyName: 'SPACE',
2017-10-31 00:43:11 +02:00
description: () => _('Set a to-do as completed / not completed'),
2017-10-09 00:34:01 +02:00
action: 'todo toggle $n',
}
2017-10-08 19:50:43 +02:00
shortcuts['tc'] = {
2017-10-31 00:43:11 +02:00
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,
}
2017-10-30 20:39:20 +02:00
shortcuts['/'] = {
2017-10-31 00:43:11 +02:00
description: () => _('Search'),
2017-10-30 20:39:20 +02:00
action: { type: 'prompt', initialText: 'search ""', cursorPosition: -2 },
};
shortcuts['tm'] = {
2017-10-31 00:43:11 +02:00
description: () => _('[t]oggle note [m]etadata.'),
action: () => {
this.toggleNoteMetadata();
},
canRunAlongOtherCommands: true,
}
2017-10-29 17:41:30 +02:00
shortcuts['mn'] = {
2017-10-31 00:43:11 +02:00
description: () => _('[M]ake a new [n]ote'),
action: { type: 'prompt', initialText: 'mknote ""', cursorPosition: -2 },
2017-10-08 19:50:43 +02:00
}
2017-10-29 17:41:30 +02:00
shortcuts['mt'] = {
2017-10-31 00:43:11 +02:00
description: () => _('[M]ake a new [t]odo'),
action: { type: 'prompt', initialText: 'mktodo ""', cursorPosition: -2 },
2017-10-08 19:50:43 +02:00
}
2017-10-29 17:41:30 +02:00
shortcuts['mb'] = {
2017-10-31 00:43:11 +02:00
description: () => _('[M]ake a new note[b]ook'),
action: { type: 'prompt', initialText: 'mkbook ""', cursorPosition: -2 },
2017-10-09 00:34:01 +02:00
}
2017-10-29 17:41:30 +02:00
shortcuts['yn'] = {
2017-10-31 00:43:11 +02:00
description: () => _('Copy ([Y]ank) the [n]ote to a notebook.'),
action: { type: 'prompt', initialText: 'cp $n ""', cursorPosition: -2 },
}
2017-10-29 17:41:30 +02:00
shortcuts['dn'] = {
2017-10-31 00:43:11 +02:00
description: () => _('Move the note to a notebook.'),
action: { type: 'prompt', initialText: 'mv $n ""', cursorPosition: -2 },
}
2017-10-07 18:30:27 +02:00
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;
2017-10-09 00:34:01 +02:00
}
maximizeConsole(doMaximize = true) {
const consoleWidget = this.widget('console');
2017-10-09 00:34:01 +02:00
if (consoleWidget.isMaximized__ === undefined) {
consoleWidget.isMaximized__ = false;
}
2017-10-09 00:34:01 +02:00
if (consoleWidget.isMaximized__ === doMaximize) return;
2017-10-09 00:34:01 +02:00
let constraints = {
type: 'stretch',
factor: !doMaximize ? 1 : 4,
};
2017-10-09 00:34:01 +02:00
consoleWidget.isMaximized__ = doMaximize;
2017-10-09 00:34:01 +02:00
this.widget('vLayout').setWidgetConstraints(consoleWidget, constraints);
2017-10-09 00:34:01 +02:00
}
minimizeConsole() {
this.maximizeConsole(false);
2017-10-09 00:34:01 +02:00
}
consoleIsMaximized() {
return this.widget('console').isMaximized__ === true;
2017-10-09 00:34:01 +02:00
}
showNoteMetadata(show = true) {
this.widget('noteMetadata').show(show);
}
hideNoteMetadata() {
this.showNoteMetadata(false);
}
toggleNoteMetadata() {
this.showNoteMetadata(!this.widget('noteMetadata').shown);
}
2017-10-05 19:17:56 +02:00
widget(name) {
2017-10-09 00:34:01 +02:00
if (name === 'root') return this.rootWidget_;
2017-10-05 19:17:56 +02:00
return this.rootWidget_.childByName(name);
}
app() {
return this.app_;
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
2017-10-09 00:34:01 +02:00
shortcuts() {
return this.shortcuts_;
}
2017-10-05 19:17:56 +02:00
term() {
return this.term_;
}
2017-10-07 18:30:27 +02:00
activeListItem() {
2017-10-09 20:05:01 +02:00
const widget = this.widget('mainWindow').focusedWidget;
2017-10-07 18:30:27 +02:00
if (!widget) return null;
2017-10-08 19:50:43 +02:00
if (widget.name == 'noteList' || widget.name == 'folderList') {
2017-10-07 22:04:53 +02:00
return widget.currentItem;
2017-10-07 18:30:27 +02:00
}
return null;
}
2017-10-06 19:38:17 +02:00
async handleModelAction(action) {
2017-10-07 20:05:35 +02:00
this.logger().info('Action:', action);
2017-10-07 18:30:27 +02:00
let state = Object.assign({}, defaultState);
2017-10-07 22:04:53 +02:00
state.notes = this.widget('noteList').items;
2017-10-06 19:38:17 +02:00
2017-10-07 18:30:27 +02:00
let newState = reducer(state, action);
2017-10-06 19:38:17 +02:00
2017-10-07 18:30:27 +02:00
if (newState !== state) {
2017-10-07 22:04:53 +02:00
this.widget('noteList').items = newState.notes;
2017-10-07 18:30:27 +02:00
}
}
2017-10-06 19:38:17 +02:00
2017-10-07 18:30:27 +02:00
async processCommand(cmd) {
if (!cmd) return;
cmd = cmd.trim();
if (!cmd.length) return;
this.logger().info('Got command: ' + cmd);
2017-10-21 18:53:43 +02:00
if (cmd === 'q' || cmd === 'wq' || cmd === 'qa') { // Vim bonus
await this.app().exit();
return;
}
try {
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 : '';
}
2017-10-07 18:30:27 +02:00
}
2017-10-06 19:38:17 +02:00
2017-10-07 18:30:27 +02:00
await this.app().execCommand(args);
} catch (error) {
2017-10-15 18:57:09 +02:00
this.stdout(error.message);
2017-10-07 18:30:27 +02:00
}
this.widget('console').scrollBottom();
// Invalidate so that the screen is redrawn in case inputting a command has moved
// the GUI up (in particular due to autocompletion), it's moved back to the right position.
this.widget('root').invalidate();
2017-10-06 19:38:17 +02:00
}
2017-10-05 19:17:56 +02:00
async updateFolderList() {
const folders = await Folder.all();
2017-10-07 22:04:53 +02:00
this.widget('folderList').items = folders;
2017-10-05 19:17:56 +02:00
}
async updateNoteList(folderId) {
2017-10-06 19:38:17 +02:00
const fields = Note.previewFields();
fields.splice(fields.indexOf('body'), 1);
const notes = folderId ? await Note.previews(folderId, { fields: fields }) : [];
2017-10-07 22:04:53 +02:00
this.widget('noteList').items = notes;
2017-10-05 19:17:56 +02:00
}
async updateNoteText(note) {
const text = note ? note.body : '';
2017-10-07 22:04:53 +02:00
this.widget('noteText').text = text;
2017-10-05 19:17:56 +02:00
}
// Any key after which a shortcut is not possible.
2017-10-09 00:34:01 +02:00
isSpecialKey(name) {
2017-10-15 18:57:09 +02:00
return [':', 'ENTER', 'DOWN', 'UP', 'LEFT', 'RIGHT', 'DELETE', 'BACKSPACE', 'ESCAPE', 'TAB', 'SHIFT_TAB', 'PAGE_UP', 'PAGE_DOWN'].indexOf(name) >= 0;
2017-10-09 00:34:01 +02:00
}
2017-10-14 20:03:23 +02:00
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);
2017-10-15 18:57:09 +02:00
}
2017-10-15 19:20:54 +02:00
this.updateStatusBarMessage();
}
exit() {
this.fullScreen(false);
this.resourceServer_.stop();
}
2017-10-15 19:20:54 +02:00
updateStatusBarMessage() {
const consoleWidget = this.widget('console');
let msg = '';
const text = consoleWidget.lastLine;
2017-10-15 19:20:54 +02:00
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);
2017-10-15 18:57:09 +02:00
}
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) => {
2017-10-26 23:57:49 +02:00
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);
},
};
}
2017-10-05 19:17:56 +02:00
async start() {
const term = this.term();
2017-10-14 20:03:23 +02:00
this.fullScreen();
2017-10-05 19:17:56 +02:00
try {
this.setupResourceServer();
2017-10-05 19:17:56 +02:00
this.renderer_.start();
2017-10-15 18:57:09 +02:00
const statusBar = this.widget('statusBar');
2017-10-07 18:30:27 +02:00
2017-10-05 19:17:56 +02:00
term.grabInput();
2017-10-07 18:30:27 +02:00
term.on('key', async (name, matches, data) => {
2017-10-14 20:03:23 +02:00
// -------------------------------------------------------------------------
// Handle special shortcuts
// -------------------------------------------------------------------------
2017-10-14 20:03:23 +02:00
if (name === 'CTRL_D') {
const cmd = this.app().currentCommand();
if (cmd && cmd.cancellable() && !this.commandCancelCalled_) {
this.commandCancelCalled_ = true;
await cmd.cancel();
this.commandCancelCalled_ = false;
}
2017-10-09 22:29:49 +02:00
await this.app().exit();
2017-10-07 18:30:27 +02:00
return;
2017-10-05 19:17:56 +02:00
}
2017-10-14 20:03:23 +02:00
if (name === 'CTRL_C' ) {
const cmd = this.app().currentCommand();
if (!cmd || !cmd.cancellable() || this.commandCancelCalled_) {
2017-10-15 18:57:09 +02:00
this.stdout(_('Press Ctrl+D or type "exit" to exit the application'));
2017-10-14 20:03:23 +02:00
} else {
this.commandCancelCalled_ = true;
await cmd.cancel()
2017-10-14 20:03:23 +02:00
this.commandCancelCalled_ = false;
}
return;
}
// -------------------------------------------------------------------------
// Build up current shortcut
// -------------------------------------------------------------------------
2017-10-09 00:34:01 +02:00
const now = (new Date()).getTime();
if (now - this.lastShortcutKeyTime_ > 800 || this.isSpecialKey(name)) {
this.currentShortcutKeys_ = [name];
2017-10-09 00:34:01 +02:00
} 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);
}
2017-10-09 00:34:01 +02:00
}
this.lastShortcutKeyTime_ = now;
// -------------------------------------------------------------------------
// Process shortcut and execute associated command
// -------------------------------------------------------------------------
const shortcutKey = this.currentShortcutKeys_.join('');
let cmd = shortcutKey in this.shortcuts_ ? this.shortcuts_[shortcutKey] : null;
// If this command is an alias to another command, resolve to the actual command
if (cmd && cmd.alias) cmd = this.shortcuts_[cmd.alias];
2017-10-17 23:56:22 +02:00
let processShortcutKeys = !this.app().currentCommand() && cmd;
if (cmd && cmd.canRunAlongOtherCommands) processShortcutKeys = true;
2017-10-17 23:56:22 +02:00
if (statusBar.promptActive) processShortcutKeys = false;
if (cmd && cmd.isDocOnly) processShortcutKeys = false;
if (processShortcutKeys) {
2017-10-31 00:43:11 +02:00
this.logger().info('Shortcut:', shortcutKey, cmd.description());
2017-10-17 23:56:22 +02:00
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));
2017-10-07 18:30:27 +02:00
}
} else { // String
this.stdout(cmd.action);
await this.processCommand(cmd.action);
2017-10-07 18:30:27 +02:00
}
2017-10-05 19:17:56 +02:00
}
2017-10-15 19:20:54 +02:00
// Optimisation: Update the status bar only
// if the user is not already typing a command:
if (!statusBar.promptActive) this.updateStatusBarMessage();
2017-10-05 19:17:56 +02:00
});
} catch (error) {
2017-10-14 20:03:23 +02:00
this.fullScreen(false);
2017-10-05 19:17:56 +02:00
this.logger().error(error);
console.error(error);
}
2017-10-08 00:17:10 +02:00
process.on('unhandledRejection', (reason, p) => {
2017-10-14 20:03:23 +02:00
this.fullScreen(false);
2017-10-08 00:17:10 +02:00
console.error('Unhandled promise rejection', p, 'reason:', reason);
process.exit(1);
});
2017-10-05 19:17:56 +02:00
}
}
2017-10-09 00:34:01 +02:00
AppGui.INPUT_MODE_NORMAL = 1;
AppGui.INPUT_MODE_META = 2;
module.exports = AppGui;