2018-03-09 22:59:12 +02:00
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' ) ;
2018-03-15 20:08:46 +02:00
const ResourceService = require ( 'lib/services/ResourceService' ) ;
2018-03-09 22:59:12 +02:00
const BaseModel = require ( 'lib/BaseModel.js' ) ;
const Folder = require ( 'lib/models/Folder.js' ) ;
const BaseItem = require ( 'lib/models/BaseItem.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 Cache = require ( 'lib/Cache' ) ;
2017-07-10 22:03:46 +02:00
2017-11-04 14:23:46 +02:00
class Application extends BaseApplication {
2018-03-09 22:59:12 +02:00
2017-07-10 22:03:46 +02:00
constructor ( ) {
2017-11-04 14:23:46 +02:00
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 ;
2018-03-09 22:59:12 +02:00
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 ) {
let output = await this . loadItems ( type , pattern , options ) ;
2017-09-10 19:32:04 +02:00
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;
2017-09-10 19:32:04 +02:00
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 ) {
2018-03-09 22:59:12 +02:00
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 ) ;
}
2018-03-09 22:59:12 +02:00
pattern = pattern ? pattern . toString ( ) : '' ;
2017-07-22 20:16:16 +02:00
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 ) ;
2018-03-09 22:59:12 +02:00
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 } ) ;
2018-03-09 22:59:12 +02:00
} else { // Single item
2017-07-11 20:17:23 +02:00
let item = null ;
if ( type == BaseModel . TYPE _NOTE ) {
2018-03-09 22:59:12 +02:00
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 ] ;
2017-09-10 19:32:04 +02:00
if ( pattern . length >= 2 ) {
return await ItemClass . loadByPartialId ( pattern ) ;
2017-07-11 20:17:23 +02:00
}
2017-07-10 22:47:01 +02:00
}
return [ ] ;
2017-07-10 22:03:46 +02:00
}
2017-10-20 00:02:13 +02:00
stdout ( text ) {
return this . gui ( ) . stdout ( text ) ;
}
2017-10-07 19:07:38 +02:00
setupCommand ( cmd ) {
2018-03-09 22:59:12 +02:00
cmd . setStdout ( ( text ) => {
2017-10-20 00:02:13 +02:00
return this . stdout ( text ) ;
2017-10-07 19:07:38 +02:00
} ) ;
2018-03-09 22:59:12 +02:00
cmd . setDispatcher ( ( action ) => {
2017-10-20 00:02:13 +02:00
if ( this . store ( ) ) {
return this . store ( ) . dispatch ( action ) ;
} else {
2018-03-09 22:59:12 +02:00
return ( action ) => { } ;
2017-10-20 00:02:13 +02:00
}
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 = { } ;
2018-03-09 22:59:12 +02:00
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
2018-03-09 22:59:12 +02:00
if ( options . type == 'boolean' ) {
message += ' (' + options . answers . join ( '/' ) + ')' ;
2017-10-07 19:07:38 +02:00
}
2018-03-09 22:59:12 +02:00
let answer = await this . gui ( ) . prompt ( '' , message + ' ' , options ) ;
2017-10-15 18:57:09 +02:00
2018-03-09 22:59:12 +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 ] ;
2018-03-09 22:59:12 +02:00
let 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 ) {
2017-10-24 21:40:15 +02:00
const doExit = async ( ) => {
this . gui ( ) . exit ( ) ;
2017-11-04 14:23:46 +02:00
await super . exit ( code ) ;
2017-10-24 21:40:15 +02:00
} ;
// Give it a few seconds to cancel otherwise exit anyway
setTimeout ( async ( ) => {
await doExit ( ) ;
} , 5000 ) ;
2017-11-24 20:06:30 +02:00
if ( await reg . syncTarget ( ) . syncStarted ( ) ) {
2018-03-09 22:59:12 +02:00
this . stdout ( _ ( 'Cancelling background synchronisation... Please wait.' ) ) ;
2017-11-24 20:06:30 +02:00
const sync = await reg . syncTarget ( ) . synchronizer ( ) ;
2017-10-24 21:40:15 +02:00
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 _ ) {
2018-03-09 22:59:12 +02:00
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
2018-03-09 22:59:12 +02:00
let 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 ) {
let temp = [ ] ;
for ( let n in this . commands _ ) {
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 ( ) ;
let output = [ ] ;
for ( let n in metadata ) {
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
2018-03-09 22:59:12 +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 = { } ;
2017-08-04 19:11:10 +02:00
for ( let n in commands ) {
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
2018-03-09 22:59:12 +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
}
2017-11-05 02:17:48 +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 {
2018-03-09 22:59:12 +02:00
CommandClass = require ( _ _dirname + '/command-' + name + '.js' ) ;
2017-08-03 19:48:14 +02:00
} catch ( error ) {
2018-03-09 22:59:12 +02:00
if ( error . message && error . message . indexOf ( 'Cannot find module' ) >= 0 ) {
let e = new Error ( _ ( 'No such command: %s' , name ) ) ;
e . type = 'notFound' ;
2017-12-06 21:29:58 +02:00
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
}
2017-10-20 00:02:13 +02:00
dummyGui ( ) {
return {
2018-03-09 22:59:12 +02:00
isDummy : ( ) => { return true ; } ,
prompt : ( initialText = '' , promptString = '' , options = null ) => { return cliUtils . prompt ( initialText , promptString , options ) ; } ,
2017-10-20 00:02:13 +02:00
showConsole : ( ) => { } ,
maximizeConsole : ( ) => { } ,
2018-03-09 22:59:12 +02:00
stdout : ( text ) => { console . info ( text ) ; } ,
fullScreen : ( b = true ) => { } ,
2017-10-24 21:40:15 +02:00
exit : ( ) => { } ,
2018-03-09 22:59:12 +02:00
showModalOverlay : ( text ) => { } ,
2017-10-24 21:52:26 +02:00
hideModalOverlay : ( ) => { } ,
2018-03-09 22:59:12 +02:00
stdoutMaxWidth : ( ) => { return 100 ; } ,
2017-12-28 21:14:03 +02:00
forceRender : ( ) => { } ,
termSaveState : ( ) => { } ,
2018-03-09 22:59:12 +02:00
termRestoreState : ( state ) => { } ,
2017-10-20 00:02:13 +02:00
} ;
}
2017-08-03 19:48:14 +02:00
async execCommand ( argv ) {
2018-03-09 22:59:12 +02:00
if ( ! argv . length ) return this . execCommand ( [ 'help' ] ) ;
2018-03-14 19:28:41 +02:00
// 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 ) ;
2017-10-24 23:37:36 +02:00
2017-10-17 19:18:31 +02:00
let outException = null ;
try {
2018-03-09 22:59:12 +02:00
if ( this . gui ( ) . isDummy ( ) && ! this . activeCommand _ . supportsUi ( 'cli' ) ) throw new Error ( _ ( 'The command "%s" is only available in GUI mode' , this . activeCommand _ . name ( ) ) ) ;
2017-10-23 23:48:29 +02:00
const cmdArgs = cliUtils . makeCommandArgs ( this . activeCommand _ , argv ) ;
2017-10-17 19:18:31 +02:00
await this . activeCommand _ . action ( cmdArgs ) ;
} catch ( error ) {
outException = error ;
}
2017-10-14 20:03:23 +02:00
this . activeCommand _ = null ;
2017-10-17 19:18:31 +02:00
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
}
2018-02-17 00:53:53 +02:00
async loadKeymaps ( ) {
const defaultKeyMap = [
2018-03-09 22:59:12 +02:00
{ "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" } ,
{ "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 }
2018-02-17 00:53:53 +02:00
] ;
// 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 ++ ) {
2018-03-09 22:59:12 +02:00
itemsByCommand [ defaultKeyMap [ i ] . command ] = defaultKeyMap [ i ]
2018-02-17 00:53:53 +02:00
}
2018-03-09 22:59:12 +02:00
const filePath = Setting . value ( 'profileDir' ) + '/keymap.json' ;
2018-02-17 00:53:53 +02:00
if ( await fs . pathExists ( filePath ) ) {
try {
2018-03-09 22:59:12 +02:00
let configString = await fs . readFile ( filePath , 'utf-8' ) ;
configString = configString . replace ( /^\s*\/\/.*/ , '' ) ; // Strip off comments
2018-02-17 00:53:53 +02:00
const keymap = JSON . parse ( configString ) ;
for ( let keymapIndex = 0 ; keymapIndex < keymap . length ; keymapIndex ++ ) {
const item = keymap [ keymapIndex ] ;
itemsByCommand [ item . command ] = item ;
}
} catch ( error ) {
2018-03-09 22:59:12 +02:00
let msg = error . message ? error . message : '' ;
msg = 'Could not load keymap ' + filePath + '\n' + msg ;
2018-02-17 00:53:53 +02:00
error . message = msg ;
throw error ;
}
}
const output = [ ] ;
for ( let n in itemsByCommand ) {
if ( ! itemsByCommand . hasOwnProperty ( n ) ) continue ;
output . push ( itemsByCommand [ n ] ) ;
}
return output ;
}
2017-11-04 14:23:46 +02:00
async start ( argv ) {
argv = await super . start ( argv ) ;
2017-07-24 20:01:40 +02:00
2018-03-09 22:59:12 +02:00
cliUtils . setStdout ( ( object ) => {
2017-10-25 19:41:36 +02:00
return this . stdout ( object ) ;
} ) ;
2017-10-20 00:02:13 +02:00
// If we have some arguments left at this point, it's a command
// so execute it.
if ( argv . length ) {
this . gui _ = this . dummyGui ( ) ;
2018-03-09 22:59:12 +02:00
this . currentFolder _ = await Folder . load ( Setting . value ( 'activeFolderId' ) ) ;
2017-12-07 20:12:46 +02:00
2017-10-20 00:02:13 +02:00
try {
await this . execCommand ( argv ) ;
} catch ( error ) {
if ( this . showStackTraces _ ) {
2018-02-25 19:01:16 +02:00
console . error ( error ) ;
2017-10-20 00:02:13 +02:00
} else {
console . info ( error . message ) ;
}
2018-02-25 19:01:16 +02:00
process . exit ( 1 ) ;
2017-10-20 00:02:13 +02:00
}
2018-03-09 22:59:12 +02:00
} else { // Otherwise open the GUI
2017-11-04 15:23:15 +02:00
this . initRedux ( ) ;
2017-10-20 00:02:13 +02:00
2018-02-17 00:53:53 +02:00
const keymap = await this . loadKeymaps ( ) ;
2018-03-09 22:59:12 +02:00
const AppGui = require ( './app-gui.js' ) ;
2018-02-17 00:53:53 +02:00
this . gui _ = new AppGui ( this , this . store ( ) , keymap ) ;
2017-10-20 00:02:13 +02:00
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 ( ) ;
2017-10-20 00:02:13 +02:00
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 ( ) ;
2018-03-15 20:08:46 +02:00
2017-10-22 19:12:16 +02:00
this . dispatch ( {
2018-03-09 22:59:12 +02:00
type : 'TAG_UPDATE_ALL' ,
2017-12-14 19:58:10 +02:00
items : tags ,
2017-10-22 19:12:16 +02:00
} ) ;
2017-10-20 00:02:13 +02:00
this . store ( ) . dispatch ( {
2018-03-09 22:59:12 +02:00
type : 'FOLDER_SELECT' ,
id : Setting . value ( 'activeFolderId' ) ,
2017-10-20 00:02:13 +02:00
} ) ;
}
2017-07-10 22:03:46 +02:00
}
2018-03-09 22:59:12 +02:00
2017-07-10 22:03:46 +02:00
}
let application _ = null ;
function app ( ) {
if ( application _ ) return application _ ;
application _ = new Application ( ) ;
return application _ ;
}
2018-03-09 22:59:12 +02:00
module . exports = { app } ;