joplin/CliClient/app/app-gui.js

789 lines
21 KiB
JavaScript
Raw Normal View History

2018-03-09 17:49:35 +00:00
const { Logger } = require("lib/logger.js");
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");
const chalk = require("chalk");
const tk = require("terminal-kit");
const TermWrapper = require("tkwidgets/framework/TermWrapper.js");
const Renderer = require("tkwidgets/framework/Renderer.js");
const DecryptionWorker = require("lib/services/DecryptionWorker");
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");
2017-10-07 21:01:03 +00:00
2017-10-05 17:17:56 +00:00
class AppGui {
constructor(app, store, keymap) {
try {
this.app_ = app;
this.store_ = store;
BaseWidget.setLogger(app.logger());
this.term_ = new TermWrapper(tk.terminal);
// Some keys are directly handled by the tkwidget framework
// so they need to be remapped in a different way.
this.tkWidgetKeys_ = {
2018-03-09 17:49:35 +00:00
focus_next: "TAB",
focus_previous: "SHIFT_TAB",
move_up: "UP",
move_down: "DOWN",
page_down: "PAGE_DOWN",
page_up: "PAGE_UP",
};
2017-10-13 18:21:57 +00:00
this.renderer_ = null;
this.logger_ = new Logger();
this.buildUi();
2017-10-07 21:01:03 +00:00
this.renderer_ = new Renderer(this.term(), this.rootWidget_);
2017-10-06 17:38:17 +00:00
2018-03-09 17:49:35 +00:00
this.app_.on("modelAction", async event => {
await this.handleModelAction(event.action);
});
2017-10-07 16:30:27 +00:00
this.keymap_ = this.setupKeymap(keymap);
2017-10-08 22:34:01 +00:00
this.inputMode_ = AppGui.INPUT_MODE_NORMAL;
2017-10-08 22:34:01 +00:00
this.commandCancelCalled_ = false;
2017-10-14 18:03:23 +00:00
this.currentShortcutKeys_ = [];
this.lastShortcutKeyTime_ = 0;
2017-10-14 18:03:23 +00: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();
} catch (error) {
this.fullScreen(false);
console.error(error);
process.exit(1);
}
2017-10-14 18:03:23 +00:00
}
store() {
return this.store_;
}
2017-10-14 18:03:23 +00:00
renderer() {
return this.renderer_;
2017-10-06 17:38:17 +00:00
}
2017-10-05 17:17:56 +00:00
2017-10-14 21:44:50 +00:00
async forceRender() {
2018-03-09 17:49:35 +00:00
this.widget("root").invalidate();
2017-10-14 21:44:50 +00:00
await this.renderer_.renderRoot();
}
termSaveState() {
return this.term().saveState();
}
termRestoreState(state) {
return this.term().restoreState(state);
}
2018-03-09 17:49:35 +00:00
prompt(initialText = "", promptString = ":", options = null) {
return this.widget("statusBar").prompt(initialText, promptString, options);
}
2017-10-24 20:22:57 +00:00
stdoutMaxWidth() {
2018-03-09 17:49:35 +00:00
return this.widget("console").innerWidth - 1;
2017-10-24 20:22:57 +00:00
}
isDummy() {
return false;
}
2017-10-06 17:38:17 +00:00
buildUi() {
2017-10-07 21:01:03 +00:00
this.rootWidget_ = new ReduxRootWidget(this.store_);
2018-03-09 17:49:35 +00:00
this.rootWidget_.name = "root";
this.rootWidget_.autoShortcutsEnabled = false;
2017-10-05 17:17:56 +00:00
2017-10-07 22:17:10 +00:00
const folderList = new FolderListWidget();
folderList.style = {
borderBottomWidth: 1,
borderRightWidth: 1,
};
2018-03-09 17:49:35 +00:00
folderList.name = "folderList";
2017-10-09 18:05:01 +00:00
folderList.vStretch = true;
2018-03-09 17:49:35 +00:00
folderList.on("currentItemChange", async event => {
2017-10-22 17:12:16 +00:00
const item = folderList.currentItem;
2018-03-09 17:49:35 +00:00
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
2018-03-09 17:49:35 +00: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 17:12:16 +00:00
this.store_.dispatch({
2018-03-09 17:49:35 +00:00
type: "FOLDER_SELECT",
id: item ? item.id : null,
2017-10-22 17:12:16 +00:00
});
} else if (item.type_ === Tag.modelType()) {
this.store_.dispatch({
2018-03-09 17:49:35 +00:00
type: "TAG_SELECT",
id: item ? item.id : null,
2017-10-22 17:12:16 +00:00
});
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
this.store_.dispatch({
2018-03-09 17:49:35 +00:00
type: "SEARCH_SELECT",
id: item ? item.id : null,
});
2017-10-22 17:12:16 +00:00
}
2017-10-07 22:17:10 +00:00
});
2018-03-09 17:49:35 +00:00
this.rootWidget_.connect(folderList, state => {
2017-10-07 22:17:10 +00:00
return {
selectedFolderId: state.selectedFolderId,
2017-10-22 17:12:16 +00:00
selectedTagId: state.selectedTagId,
selectedSearchId: state.selectedSearchId,
2017-10-22 17:12:16 +00:00
notesParentType: state.notesParentType,
folders: state.folders,
tags: state.tags,
searches: state.searches,
2017-10-07 22:17:10 +00:00
};
2017-10-05 17:17:56 +00:00
});
const noteList = new NoteListWidget();
2018-03-09 17:49:35 +00:00
noteList.name = "noteList";
2017-10-09 18:05:01 +00:00
noteList.vStretch = true;
noteList.style = {
2017-10-05 17:17:56 +00:00
borderBottomWidth: 1,
borderLeftWidth: 1,
borderRightWidth: 1,
2017-10-09 18:05:01 +00:00
};
2018-03-09 17:49:35 +00:00
noteList.on("currentItemChange", async () => {
2017-10-07 20:04:53 +00:00
let note = noteList.currentItem;
2017-10-07 21:01:03 +00:00
this.store_.dispatch({
2018-03-09 17:49:35 +00:00
type: "NOTE_SELECT",
2017-11-08 21:22:24 +00:00
id: note ? note.id : null,
2017-10-07 21:01:03 +00:00
});
2017-10-07 22:17:10 +00:00
});
2018-03-09 17:49:35 +00:00
this.rootWidget_.connect(noteList, state => {
2017-10-07 22:17:10 +00:00
return {
2017-11-22 18:35:31 +00:00
selectedNoteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
2017-10-07 22:17:10 +00:00
items: state.notes,
};
2017-10-05 17:17:56 +00:00
});
2017-10-07 21:01:03 +00:00
const noteText = new NoteWidget();
noteText.hStretch = true;
2018-03-09 17:49:35 +00:00
noteText.name = "noteText";
noteText.style = {
borderBottomWidth: 1,
borderLeftWidth: 1,
};
2018-03-09 17:49:35 +00:00
this.rootWidget_.connect(noteText, state => {
2017-10-28 17:44:28 +00:00
return {
2017-11-22 18:35:31 +00:00
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
2017-10-28 17:44:28 +00:00
notes: state.notes,
};
2017-10-07 21:01:03 +00:00
});
const noteMetadata = new NoteMetadataWidget();
noteMetadata.hStretch = true;
2018-03-09 17:49:35 +00:00
noteMetadata.name = "noteMetadata";
noteMetadata.style = {
borderBottomWidth: 1,
borderLeftWidth: 1,
borderRightWidth: 1,
};
2018-03-09 17:49:35 +00:00
this.rootWidget_.connect(noteMetadata, state => {
2017-11-22 18:35:31 +00:00
return { noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null };
});
noteMetadata.hide();
2017-10-06 17:38:17 +00:00
const consoleWidget = new ConsoleWidget();
2017-10-09 18:05:01 +00:00
consoleWidget.hStretch = true;
consoleWidget.style = {
borderBottomWidth: 1,
};
consoleWidget.hide();
2017-10-15 16:57:09 +00:00
const statusBar = new StatusBarWidget();
statusBar.hStretch = true;
2017-10-06 17:38:17 +00:00
const noteLayout = new VLayoutWidget();
2018-03-09 17:49:35 +00:00
noteLayout.name = "noteLayout";
noteLayout.addChild(noteText, { type: "stretch", factor: 1 });
noteLayout.addChild(noteMetadata, { type: "stretch", factor: 1 });
2017-10-06 18:01:10 +00:00
const hLayout = new HLayoutWidget();
2018-03-09 17:49:35 +00:00
hLayout.name = "hLayout";
hLayout.addChild(folderList, { type: "stretch", factor: 1 });
hLayout.addChild(noteList, { type: "stretch", factor: 1 });
hLayout.addChild(noteLayout, { type: "stretch", factor: 2 });
2017-10-05 17:17:56 +00:00
2017-10-06 17:38:17 +00:00
const vLayout = new VLayoutWidget();
2018-03-09 17:49:35 +00:00
vLayout.name = "vLayout";
vLayout.addChild(hLayout, { type: "stretch", factor: 2 });
vLayout.addChild(consoleWidget, { type: "stretch", factor: 1 });
vLayout.addChild(statusBar, { type: "fixed", factor: 1 });
2017-10-05 17:17:56 +00:00
const win1 = new WindowWidget();
2017-10-06 17:38:17 +00:00
win1.addChild(vLayout);
2018-03-09 17:49:35 +00:00
win1.name = "mainWindow";
2017-10-05 17:17:56 +00:00
2017-10-07 21:01:03 +00:00
this.rootWidget_.addChild(win1);
2017-10-05 17:17:56 +00:00
}
2017-10-24 19:52:26 +00:00
showModalOverlay(text) {
2018-03-09 17:49:35 +00:00
if (!this.widget("overlayWindow")) {
2017-10-24 19:52:26 +00:00
const textWidget = new TextWidget();
textWidget.hStretch = true;
textWidget.vStretch = true;
2018-03-09 17:49:35 +00:00
textWidget.text = "testing";
textWidget.name = "overlayText";
2017-10-24 19:52:26 +00:00
const win = new WindowWidget();
2018-03-09 17:49:35 +00:00
win.name = "overlayWindow";
2017-10-24 19:52:26 +00:00
win.addChild(textWidget);
this.rootWidget_.addChild(win);
}
2018-03-09 17:49:35 +00:00
this.widget("overlayWindow").activate();
this.widget("overlayText").text = text;
2017-10-24 19:52:26 +00:00
}
hideModalOverlay() {
2018-03-09 17:49:35 +00:00
if (this.widget("overlayWindow")) this.widget("overlayWindow").hide();
this.widget("mainWindow").activate();
2017-10-24 19:52:26 +00:00
}
addCommandToConsole(cmd) {
2017-10-28 17:55:45 +00:00
if (!cmd) return;
2018-03-09 17:49:35 +00:00
const isConfigPassword = cmd.indexOf("config ") >= 0 && cmd.indexOf("password") >= 0;
if (isConfigPassword) return;
2018-03-09 17:49:35 +00:00
this.stdout(chalk.cyan.bold("> " + cmd));
}
setupKeymap(keymap) {
const output = [];
2017-10-07 16:30:27 +00:00
for (let i = 0; i < keymap.length; i++) {
const item = Object.assign({}, keymap[i]);
2017-10-29 15:41:30 +00:00
2018-03-09 17:49:35 +00:00
if (!item.command) throw new Error("Missing command for keymap item: " + JSON.stringify(item));
2017-10-29 15:41:30 +00:00
2018-03-09 17:49:35 +00:00
if (!("type" in item)) item.type = "exec";
2017-10-29 15:41:30 +00:00
if (item.command in this.tkWidgetKeys_) {
2018-03-09 17:49:35 +00:00
item.type = "tkwidgets";
}
2017-10-30 18:39:20 +00:00
2018-03-09 17:49:35 +00:00
item.canRunAlongOtherCommands = item.type === "function" && ["toggle_metadata", "toggle_console"].indexOf(item.command) >= 0;
2017-10-08 17:50:43 +00:00
output.push(item);
2017-10-08 22:34:01 +00:00
}
return output;
2017-10-07 16:30:27 +00:00
}
toggleConsole() {
this.showConsole(!this.consoleIsShown());
}
showConsole(doShow = true) {
2018-03-09 17:49:35 +00:00
this.widget("console").show(doShow);
}
hideConsole() {
this.showConsole(false);
}
consoleIsShown() {
2018-03-09 17:49:35 +00:00
return this.widget("console").shown;
2017-10-08 22:34:01 +00:00
}
maximizeConsole(doMaximize = true) {
2018-03-09 17:49:35 +00:00
const consoleWidget = this.widget("console");
2017-10-08 22:34:01 +00:00
if (consoleWidget.isMaximized__ === undefined) {
consoleWidget.isMaximized__ = false;
}
2017-10-08 22:34:01 +00:00
if (consoleWidget.isMaximized__ === doMaximize) return;
2017-10-08 22:34:01 +00:00
let constraints = {
2018-03-09 17:49:35 +00:00
type: "stretch",
factor: !doMaximize ? 1 : 4,
};
2017-10-08 22:34:01 +00:00
consoleWidget.isMaximized__ = doMaximize;
2017-10-08 22:34:01 +00:00
2018-03-09 17:49:35 +00:00
this.widget("vLayout").setWidgetConstraints(consoleWidget, constraints);
2017-10-08 22:34:01 +00:00
}
minimizeConsole() {
this.maximizeConsole(false);
2017-10-08 22:34:01 +00:00
}
consoleIsMaximized() {
2018-03-09 17:49:35 +00:00
return this.widget("console").isMaximized__ === true;
2017-10-08 22:34:01 +00:00
}
showNoteMetadata(show = true) {
2018-03-09 17:49:35 +00:00
this.widget("noteMetadata").show(show);
}
hideNoteMetadata() {
this.showNoteMetadata(false);
}
toggleNoteMetadata() {
2018-03-09 17:49:35 +00:00
this.showNoteMetadata(!this.widget("noteMetadata").shown);
}
2017-10-05 17:17:56 +00:00
widget(name) {
2018-03-09 17:49:35 +00:00
if (name === "root") return this.rootWidget_;
2017-10-05 17:17:56 +00:00
return this.rootWidget_.childByName(name);
}
app() {
return this.app_;
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
keymap() {
return this.keymap_;
}
keymapItemByKey(key) {
for (let i = 0; i < this.keymap_.length; i++) {
const item = this.keymap_[i];
if (item.keys.indexOf(key) >= 0) return item;
}
return null;
2017-10-08 22:34:01 +00:00
}
2017-10-05 17:17:56 +00:00
term() {
return this.term_;
}
2017-10-07 16:30:27 +00:00
activeListItem() {
2018-03-09 17:49:35 +00:00
const widget = this.widget("mainWindow").focusedWidget;
2017-10-07 16:30:27 +00:00
if (!widget) return null;
2018-03-09 17:49:35 +00:00
if (widget.name == "noteList" || widget.name == "folderList") {
2017-10-07 20:04:53 +00:00
return widget.currentItem;
2017-10-07 16:30:27 +00:00
}
return null;
}
2017-10-06 17:38:17 +00:00
async handleModelAction(action) {
2018-03-09 17:49:35 +00:00
this.logger().info("Action:", action);
2017-10-07 18:05:35 +00:00
2017-10-07 16:30:27 +00:00
let state = Object.assign({}, defaultState);
2018-03-09 17:49:35 +00:00
state.notes = this.widget("noteList").items;
2017-10-06 17:38:17 +00:00
2017-10-07 16:30:27 +00:00
let newState = reducer(state, action);
2017-10-06 17:38:17 +00:00
2017-10-07 16:30:27 +00:00
if (newState !== state) {
2018-03-09 17:49:35 +00:00
this.widget("noteList").items = newState.notes;
2017-10-07 16:30:27 +00:00
}
}
2017-10-06 17:38:17 +00:00
async processFunctionCommand(cmd) {
2018-03-09 17:49:35 +00:00
if (cmd === "activate") {
const w = this.widget("mainWindow").focusedWidget;
if (w.name === "folderList") {
this.widget("noteList").focus();
} else if (w.name === "noteList" || w.name === "noteText") {
this.processPromptCommand("edit $n");
}
2018-03-09 17:49:35 +00:00
} else if (cmd === "delete") {
if (this.widget("folderList").hasFocus) {
const item = this.widget("folderList").selectedJoplinItem;
if (!item) return;
if (item.type_ === BaseModel.TYPE_FOLDER) {
2018-03-09 17:49:35 +00:00
await this.processPromptCommand("rmbook " + item.id);
} else if (item.type_ === BaseModel.TYPE_TAG) {
2018-03-09 17:49:35 +00:00
this.stdout(_("To delete a tag, untag the associated notes."));
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
this.store().dispatch({
2018-03-09 17:49:35 +00:00
type: "SEARCH_DELETE",
id: item.id,
});
}
2018-03-09 17:49:35 +00:00
} else if (this.widget("noteList").hasFocus) {
await this.processPromptCommand("rmnote $n");
} else {
2018-03-09 17:49:35 +00:00
this.stdout(_("Please select the note or notebook to be deleted first."));
}
2018-03-09 17:49:35 +00:00
} else if (cmd === "toggle_console") {
if (!this.consoleIsShown()) {
this.showConsole();
this.minimizeConsole();
} else {
if (this.consoleIsMaximized()) {
this.hideConsole();
} else {
this.maximizeConsole();
}
}
2018-03-09 17:49:35 +00:00
} else if (cmd === "toggle_metadata") {
this.toggleNoteMetadata();
2018-03-09 17:49:35 +00:00
} else if (cmd === "enter_command_line_mode") {
const cmd = await this.widget("statusBar").prompt();
if (!cmd) return;
this.addCommandToConsole(cmd);
2018-03-09 17:49:35 +00:00
await this.processPromptCommand(cmd);
} else {
2018-03-09 17:49:35 +00:00
throw new Error("Unknown command: " + cmd);
}
}
async processPromptCommand(cmd) {
2017-10-07 16:30:27 +00:00
if (!cmd) return;
cmd = cmd.trim();
if (!cmd.length) return;
2018-03-09 17:49:35 +00:00
this.logger().info("Got command: " + cmd);
2018-03-09 17:49:35 +00:00
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++) {
2018-03-09 17:49:35 +00:00
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();
2018-03-09 17:49:35 +00:00
args[i] = item ? item.id : "";
}
2017-10-07 16:30:27 +00:00
}
2017-10-06 17:38:17 +00:00
2017-10-07 16:30:27 +00:00
await this.app().execCommand(args);
} catch (error) {
2017-10-15 16:57:09 +00:00
this.stdout(error.message);
2017-10-07 16:30:27 +00:00
}
2018-03-09 17:49:35 +00: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.
2018-03-09 17:49:35 +00:00
this.widget("root").invalidate();
2017-10-06 17:38:17 +00:00
}
2017-10-05 17:17:56 +00:00
async updateFolderList() {
const folders = await Folder.all();
2018-03-09 17:49:35 +00:00
this.widget("folderList").items = folders;
2017-10-05 17:17:56 +00:00
}
async updateNoteList(folderId) {
2017-10-06 17:38:17 +00:00
const fields = Note.previewFields();
2018-03-09 17:49:35 +00:00
fields.splice(fields.indexOf("body"), 1);
2017-10-06 17:38:17 +00:00
const notes = folderId ? await Note.previews(folderId, { fields: fields }) : [];
2018-03-09 17:49:35 +00:00
this.widget("noteList").items = notes;
2017-10-05 17:17:56 +00:00
}
async updateNoteText(note) {
2018-03-09 17:49:35 +00:00
const text = note ? note.body : "";
this.widget("noteText").text = text;
2017-10-05 17:17:56 +00:00
}
// Any key after which a shortcut is not possible.
2017-10-08 22:34:01 +00:00
isSpecialKey(name) {
2018-03-09 17:49:35 +00:00
return [":", "ENTER", "DOWN", "UP", "LEFT", "RIGHT", "DELETE", "BACKSPACE", "ESCAPE", "TAB", "SHIFT_TAB", "PAGE_UP", "PAGE_DOWN"].indexOf(name) >= 0;
2017-10-08 22:34:01 +00:00
}
2017-10-14 18:03:23 +00:00
fullScreen(enable = true) {
if (enable) {
this.term().fullscreen();
this.term().hideCursor();
2018-03-09 17:49:35 +00:00
this.widget("root").invalidate();
2017-10-14 18:03:23 +00:00
} else {
this.term().fullscreen(false);
this.term().showCursor();
}
}
stdout(text) {
if (text === null || text === undefined) return;
2018-03-09 17:49:35 +00:00
let lines = text.split("\n");
for (let i = 0; i < lines.length; i++) {
2018-03-09 17:49:35 +00:00
const v = typeof lines[i] === "object" ? JSON.stringify(lines[i]) : lines[i];
this.widget("console").addLine(v);
2017-10-15 16:57:09 +00:00
}
2017-10-15 17:20:54 +00:00
this.updateStatusBarMessage();
}
exit() {
this.fullScreen(false);
this.resourceServer_.stop();
}
2017-10-15 17:20:54 +00:00
updateStatusBarMessage() {
2018-03-09 17:49:35 +00:00
const consoleWidget = this.widget("console");
2017-10-15 17:20:54 +00:00
2018-03-09 17:49:35 +00:00
let msg = "";
2017-10-15 17:20:54 +00:00
const text = consoleWidget.lastLine;
2017-10-15 17:20:54 +00:00
const cmd = this.app().currentCommand();
if (cmd) {
msg += cmd.name();
2018-03-09 17:49:35 +00:00
if (cmd.cancellable()) msg += " [Press Ctrl+C to cancel]";
msg += ": ";
2017-10-15 17:20:54 +00:00
}
if (text && text.length) {
msg += text;
}
2018-03-09 17:49:35 +00:00
if (msg !== "") this.widget("statusBar").setItemAt(0, msg);
2017-10-15 16:57:09 +00:00
}
async setupResourceServer() {
const linkStyle = chalk.blue.underline;
2018-03-09 17:49:35 +00:00
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++) {
2018-03-09 17:49:35 +00:00
if (s.indexOf(protocols[i] + "://") === 0) return true;
}
return false;
2018-03-09 17:49:35 +00:00
};
// By default, before the server is started, only the regular
// URLs appear in blue.
noteTextWidget.markdownRendererOptions = {
linkUrlRenderer: (index, url) => {
2017-10-26 21:57:49 +00:00
if (!url) return url;
if (resourceIdRegex.test(url)) {
return url;
2018-03-09 17:49:35 +00:00
} 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];
2018-03-09 17:49:35 +00:00
if (link.type === "url") {
response.writeHead(302, { Location: link.url });
return true;
}
2018-03-09 17:49:35 +00:00
if (link.type === "resource") {
const resourceId = link.id;
let resource = await Resource.load(resourceId);
2018-03-09 17:49:35 +00:00
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] = {
2018-03-09 17:49:35 +00:00
type: "resource",
id: url.substr(2),
2018-03-09 17:49:35 +00:00
};
} else if (hasProtocol(url, ["http", "https", "file", "ftp"])) {
noteLinks[index] = {
2018-03-09 17:49:35 +00:00
type: "url",
url: url,
};
2018-03-09 17:49:35 +00:00
} else if (url.indexOf("#") === 0) {
return ""; // Anchors aren't supported for now
} else {
return url;
}
2018-03-09 17:49:35 +00:00
return linkStyle(this.resourceServer_.baseUrl() + "/" + index);
},
};
}
2017-10-05 17:17:56 +00:00
async start() {
const term = this.term();
2017-10-14 18:03:23 +00:00
this.fullScreen();
2017-10-05 17:17:56 +00:00
try {
this.setupResourceServer();
2017-10-05 17:17:56 +00:00
this.renderer_.start();
2018-03-09 17:49:35 +00:00
const statusBar = this.widget("statusBar");
2017-10-07 16:30:27 +00:00
2017-10-05 17:17:56 +00:00
term.grabInput();
2018-03-09 17:49:35 +00:00
term.on("key", async (name, matches, data) => {
// -------------------------------------------------------------------------
// Handle special shortcuts
// -------------------------------------------------------------------------
2018-03-09 17:49:35 +00:00
if (name === "CTRL_D") {
2017-10-14 18:03:23 +00:00
const cmd = this.app().currentCommand();
if (cmd && cmd.cancellable() && !this.commandCancelCalled_) {
this.commandCancelCalled_ = true;
await cmd.cancel();
this.commandCancelCalled_ = false;
}
2017-10-09 20:29:49 +00:00
await this.app().exit();
2017-10-07 16:30:27 +00:00
return;
2017-10-05 17:17:56 +00:00
}
2017-10-14 18:03:23 +00:00
2018-03-09 17:49:35 +00:00
if (name === "CTRL_C") {
2017-10-14 18:03:23 +00:00
const cmd = this.app().currentCommand();
if (!cmd || !cmd.cancellable() || this.commandCancelCalled_) {
2017-10-15 16:57:09 +00:00
this.stdout(_('Press Ctrl+D or type "exit" to exit the application'));
2017-10-14 18:03:23 +00:00
} else {
this.commandCancelCalled_ = true;
2018-03-09 17:49:35 +00:00
await cmd.cancel();
2017-10-14 18:03:23 +00:00
this.commandCancelCalled_ = false;
}
return;
}
// -------------------------------------------------------------------------
// Build up current shortcut
// -------------------------------------------------------------------------
2018-03-09 17:49:35 +00:00
const now = new Date().getTime();
2017-10-08 22:34:01 +00:00
if (now - this.lastShortcutKeyTime_ > 800 || this.isSpecialKey(name)) {
this.currentShortcutKeys_ = [name];
2017-10-08 22:34:01 +00: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-08 22:34:01 +00:00
}
this.lastShortcutKeyTime_ = now;
// -------------------------------------------------------------------------
// Process shortcut and execute associated command
// -------------------------------------------------------------------------
2018-03-09 17:49:35 +00:00
const shortcutKey = this.currentShortcutKeys_.join("");
let keymapItem = this.keymapItemByKey(shortcutKey);
// If this command is an alias to another command, resolve to the actual command
let processShortcutKeys = !this.app().currentCommand() && keymapItem;
if (keymapItem && keymapItem.canRunAlongOtherCommands) processShortcutKeys = true;
2017-10-17 21:56:22 +00:00
if (statusBar.promptActive) processShortcutKeys = false;
if (processShortcutKeys) {
2018-03-09 17:49:35 +00:00
this.logger().info("Shortcut:", shortcutKey, keymapItem);
2017-10-17 21:56:22 +00:00
this.currentShortcutKeys_ = [];
2018-03-09 17:49:35 +00:00
if (keymapItem.type === "function") {
this.processFunctionCommand(keymapItem.command);
2018-03-09 17:49:35 +00:00
} else if (keymapItem.type === "prompt") {
let promptOptions = {};
2018-03-09 17:49:35 +00:00
if ("cursorPosition" in keymapItem) promptOptions.cursorPosition = keymapItem.cursorPosition;
const commandString = await statusBar.prompt(keymapItem.command ? keymapItem.command : "", null, promptOptions);
this.addCommandToConsole(commandString);
await this.processPromptCommand(commandString);
2018-03-09 17:49:35 +00:00
} else if (keymapItem.type === "exec") {
this.stdout(keymapItem.command);
await this.processPromptCommand(keymapItem.command);
2018-03-09 17:49:35 +00:00
} else if (keymapItem.type === "tkwidgets") {
this.widget("root").handleKey(this.tkWidgetKeys_[keymapItem.command]);
} else {
2018-03-09 17:49:35 +00:00
throw new Error("Unknown command type: " + JSON.stringify(keymapItem));
2017-10-07 16:30:27 +00:00
}
2017-10-05 17:17:56 +00:00
}
2017-10-15 17:20:54 +00:00
// Optimisation: Update the status bar only
// if the user is not already typing a command:
if (!statusBar.promptActive) this.updateStatusBarMessage();
2017-10-05 17:17:56 +00:00
});
} catch (error) {
2017-10-14 18:03:23 +00:00
this.fullScreen(false);
2017-10-05 17:17:56 +00:00
this.logger().error(error);
console.error(error);
}
2017-10-07 22:17:10 +00:00
2018-03-09 17:49:35 +00:00
process.on("unhandledRejection", (reason, p) => {
2017-10-14 18:03:23 +00:00
this.fullScreen(false);
2018-03-09 17:49:35 +00:00
console.error("Unhandled promise rejection", p, "reason:", reason);
2017-10-07 22:17:10 +00:00
process.exit(1);
});
2017-10-05 17:17:56 +00:00
}
}
2017-10-08 22:34:01 +00:00
AppGui.INPUT_MODE_NORMAL = 1;
AppGui.INPUT_MODE_META = 2;
module.exports = AppGui;