<|start_filename|>client/scripts/search.js<|end_filename|>
var state       = require('./state');
var socket      = require('./socket');
var ui          = require('./ui');
var fs          = require('./fs');
var config      = require('./config');
var resources   = ui.resources;
var bar         = ui.bar;

socket.on('search.result', function(resource){
    fs.addResource(resource);
});

/**
 * Search object.
 *
 * @type {{executeSearch: executeSearch, toggle: toggle, getSearchRegex: getSearchRegex, reset: reset, show: show, isOpen: isOpen}}
 */
var search = {
    /**
     * Executes a search and hides elements
     * which are not matching it.
     */
    executeSearch: function(text) {
        if (text) {
            $('#fs .resource').remove();
            var mode = bar().attr('mode');
            socket.emit('search.' + mode, {text: text, root: fs.getStructure().root});
        } else {
            fs.reset();
        };
    },
    /**
     * Toggles the search box.
     */
    toggle: function(mode) {
        mode = mode || 'find';

        if (search.isOpen()) {
            search.hide();
        } else {
            search.show(mode);
        }
    },
    /**
     * Hides the search box.
     */
    hide: function() {
        bar().hide();
        bar().removeClass();
        bar().empty();
        bar().unbind();
        state.switchFocus('fs');
        fs.reset();
    },
    /**
     * Shows the search box.
     */
    show: function(mode) {
        bar().empty();
        state.switchFocus('bar');

        bar().finish();
        bar().removeClass();
        bar().addClass('message-search');
        bar().attr('contenteditable', 'true');
        bar().attr('mode', mode);

        var placeholder = 'Search files / directories';
        
        if (bar().attr('mode') === 'grep') {
          placeholder = 'Find in files';
        }

        bar().attr('data-ph', placeholder + '... (looking in ' + fs.getStructure().root.path + ')');
        bar().show();
        bar().focus();

        bar().on('keyup', function () {
            var searchValue = $(this).text();

            setTimeout(function(){
                if(searchValue == bar().text()) {
                    search.executeSearch(searchValue)
                };
            }, config.get('search.timeout'));
        });

        /**
         * Do not allow pressing 'enter'.
         */
        bar().keypress(function(e){
            return e.which != 13;
        });
    },
    /**
     * Checks whether the search box is open.
     *
     * @returns {*}
     */
    isOpen: function() {
        return bar().hasClass('message-search');
    }
};

module.exports = search;

<|start_filename|>client/scripts/keyboard.js<|end_filename|>
/**
 * A simple wrapper for KeyboardJS.
 *
 * @type {exports}
 */
module.exports = {
    on: function(combination, action) {
        KeyboardJS.on(combination, action);
    }
};

<|start_filename|>client/scripts/ui.js<|end_filename|>
var events          = require('./events');
var state           = require('./state');
var editor          = require('./editor');
var fs              = require('./fs');

/**
 * UI object.
 *
 * @type {{openFile: openFile, resources: resources, bar: bar}}
 */
var ui = {
  /**
   * Opens a file in the editor tab.
   *
   * @param resource
   */
  openFile: function(resource) {
    $('.resource.open').removeClass('open');
    $('.resource.active').addClass('open');
    $('#subject span').remove();
    fs.buildTitle(resource.parent);
    $('#subject #navigation').append('<span class="highlight">/' + resource.name + '</span>');
    $('#file').attr('filename', resource.path);
    editor.openFile(resource);
  },
  /**
   * Returns all the resources.
   *
   * @returns {*|jQuery|HTMLElement}
   */
  preview: function(){
    return $('#preview');
  },
  /**
   * Returns all the resources.
   *
   * @returns {*|jQuery|HTMLElement}
   */
  resources: function(){
    return $('.resource');
  },
  /**
   * Returns the bar.
   *
   * @returns {*|jQuery|HTMLElement}
   */
  bar: function() {
    return $('#bar');
  },
  /**
   * Returns the bar.
   *
   * @returns {*|jQuery|HTMLElement}
   */
  focus: function() {
    return $('#focus');
  }
};

var focusIcons = {
  bar: 'fa-search-plus',
  tab: 'fa-pencil-square',
  fs: 'fa-folder-open'
}

/**
 * Add an icon for the initial, default state.
 */
ui.focus().addClass(focusIcons[state.focus]);

/**
 * When the focus changes, update the focus icon
 * on the top-right of the screen.
 */
events.on('state.focus.change', function(state){
  ui.focus().removeClass(focusIcons[state.prev]);
  ui.focus().addClass(focusIcons[state.next]);
});

events.on('resource.opened', function(resource){
  ui.openFile(resource);
});

module.exports = ui;

<|start_filename|>client/scripts/tabs.js<|end_filename|>
/**
 * A wrapper for the editor tabs.
 *
 * @type {exports}
 */
var socket  = require('./socket');
var events  = require('./events');
var state   = require('./state');
var tabs    = {};

/**
 * Closes the active tab, putting the focus
 * to the one on the left or, if it doesnt
 * exist, the one on the right.
 */
tabs.closeActive = function() {
  var activeTab = $('.tab.active');
  
  if (activeTab.length) {
    !tabs.moveLeft() && !tabs.moveRight();
    activeTab.remove();
    events.dispatch('tabs.close', JSON.parse(activeTab.attr('resource')));
  }
  
  tabs.persist();
};

/**
 * Adds a new tab in the editor, setting it as active.
 *
 * @param resource
 */
tabs.add = function(resource) {
  var tab = $('<li data-toggle="tooltip" data-placement="top" title="' + resource.path + '" class="active tab">');
  tab.attr('resource', JSON.stringify(resource));
  tab.attr('resource-path', resource.path);
  tab.tooltip();

  var a = $('<a>');
  a.text(resource.name);
  tab.html(a);

  tab.click(function(){
      socket.emit('resource.open', resource);
  });

  $('.tab').removeClass('active');
  $('.tabs').append(tab);
  tabs.persist();
};

/**
 * Persists the open tabs into the localStorage.
 */
tabs.persist = function() {
    var tabs = [];
    
    $('.tab').each(function(){
      var resource = $(this).attr('resource');
      
      if (resource) {
        tabs.push(resource);
      }
    });
    
    localStorage.setItem('nikki.tabs', JSON.stringify(tabs));
}

/**
 * Selects the open tab that represents the given
 * resource.
 *
 * @param resource
 */
tabs.select = function(resource) {
    $('.tab').removeClass('active');
    $('.tab[resource-path="' + resource.path + '"]').addClass('active');
};

/**
 * Move to another tab in the specified
 * direction.
 *
 * @param direction
 */
tabs.move = function(direction) {
  var activeTab = $('.tab.active');
  var element   = activeTab[direction]('.tab[resource-path]');

  if (element.length) {
    activeTab.removeClass('active');
    element.addClass('active');
    events.dispatch('resource.opened', JSON.parse(element.attr('resource')));
    
    return true;
  }
  
  return false;
};

/**
 * Moves to the tab on the left.
 *
 * @returns {*}
 */
tabs.moveLeft = function() {
    return tabs.move('prev');
}

/**
 * Moves to the tab on the right.
 *
 * @returns {*}
 */
tabs.moveRight = function() {
    return tabs.move('next');
}

/**
 * Re-open saved tabbs
 */
socket.on('boot.done', function(){
    var savedTabs = JSON.parse(localStorage.getItem('nikki.tabs'));

    if (savedTabs) {
      savedTabs.forEach(function(resource){
          socket.emit('resource.open', JSON.parse(resource))
      });
    }
});

module.exports = tabs;

<|start_filename|>client/styles/nikki.css<|end_filename|>
#preview {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 100%;
  z-index: 10;
}

.overlayed {
  background-color: rgba(0,0,0,0.5);
}

#preview > div {
  padding-left: 2em;
  font-size: 15px;
  margin: 0 auto;
  margin-top: 2em;
  margin-bottom: 2em;
  padding-top: 2em;
  padding-bottom: 2em;
  width: 70%;
  background-color: #fff;
  -webkit-border-radius: 5px;
  -webkit-border-radius: 5px;
  -moz-border-radius: 5px;
  -moz-border-radius: 5px;
  border-radius: 5px;
  border-radius: 5px;
}

#focus {
  position: absolute;
  top: 0.2em;
  right: 0.7em;
  font-size: 5em;
}

#logo {
  position: absolute;
}

.glyphicon {
    font-size: 1.5em;
    margin: 0.5em;
    margin-left: 0;
}

#bar {
    display: none;
    position: fixed;
    width:  100%;
    bottom: 0;
    left: 0;
    padding:  25px;
    z-index: 99;
    font-size: 1.2em;
}

.message-question {
    background-color: #D1DEF0;
    border: 1px solid #232387;
    color: #131369;
}

.message-info {
    background-color: #e0fdde;
    border: 1px solid #d4e6cc;
    color: #0a7d01;
}

.message-alert {
    background-color: #f7fdd2;
    border: 1px solid #d2d8b2;
    color: #919036;
    font-size: 1.2em;
}

.message-error {
    background-color: #fde4e1;
    border: 1px solid #c7b8ad;
    color: #781c0e;
    font-size: 1.2em;
}

.message-search {
    background-color: #545454;
    border: 1px solid #211d1b;
    color: #d5d4d3;
    font-size: 1.2em;
}

#bar:empty:before{
    content:attr(data-ph)
}

#fs {
    font-size: 1.3em;
}

::-webkit-scrollbar
{
    width: 12px;  /* for vertical scrollbars */
    height: 0px; /* for horizontal scrollbars */
}

::-webkit-scrollbar-track
{
    background: rgba(0, 0, 0, 0.1);
}

::-webkit-scrollbar-thumb
{
    background: rgba(0, 0, 0, 0.5);
}

#fs ul {
    list-style-type: none;
    padding: 0;
}

.scrollable {
    overflow-y: auto;
    height:85vh;
}

#fs ul li {;
    padding-left: 2em;
}

#root {

}

#container {
    margin-top: 1em;
}

#subject {
    padding-left: 8em;
    margin-bottom: 2em;
}

#subject .highlight {
    color: #cc0033;
    margin-left: 0.3em;
}

.resource {
    padding: 10px;
}

.resource.active {
    background-color: #cccccc;
}

.resource.open {
    background-color: #eaf6e8;
}

* {
    font-family: Consolas, Monaco, 'Andale Mono', monospace;
}

#file {
    right: 0;
    width: 100%;
}

.ace_gutter, .ace_scroller {
    margin-top: 10px;
}

.tabs {
    font-size: 13px;
    background-color: #011e3a;
    -webkit-border-top-left-radius: 5px;
    -webkit-border-top-right-radius: 5px;
    -moz-border-radius-topleft: 5px;
    -moz-border-radius-topright: 5px;
    border-top-left-radius: 5px;
    border-top-right-radius: 5px;
}

.tooltip-inner {
    max-width: 100%;
}

.resource .path {
  word-wrap: break-word; 
  font-size: 0.8em;
  padding-left: 2.5em;
  color: #7A7A7A;
}

.resource .misc {
  font-family: Consolas, Monaco, 'Andale Mono', monospace;
  word-wrap: break-word; 
  font-size: 12px;
  padding-left: 2.5em;
  color: #314CC4;
}

.ace_search_field {
    color: #1b008f;
}

<|start_filename|>client/scripts/editor.js<|end_filename|>
/**
 * Representation of the editor
 */
var _           = require('lodash');
var config      = require('./config');
var events      = require('./events');
var utils       = require('./utils');
var tabs        = require('./tabs');
var state       = require('./state');
var sessions    = {};
var editor      = ace.edit("file");

/**
 * Default editor configs.
 */
editor.setTheme("ace/theme/" + config.get('editor.theme'));
editor.commands.removeCommand("replace");
editor.commands.removeCommand("expandtoline");
editor.renderer.setPadding(10)
editor.setHighlightActiveLine(false);
editor.setShowPrintMargin(false);
editor.getSession().setUseWorker(false);
editor.setFontSize(config.get('editor.font-size'));

/**
 * Get the indentation from the configuration.
 */
var getIndentation = function() {
  var indentation = config.get('editor.indentation').split(' ');

  return {
    quantity: indentation[0],
    unit: indentation[1]
  };
}

/**
 * Sets the default formatting options
 * for the current session.
 */
var setFormatting = function() {
  editor.getSession().setTabSize(getIndentation().quantity);
  editor.getSession().setUseSoftTabs(getIndentation().unit === 'spaces' || getIndentation().unit === 'space' || false);
};

/**
 * Sets the current active editor session.
 *
 * @param resource
 */
var setSession = function(resource) {
  var session;
  var sessionName = resource.path;
  saveCurrentSession(resource);

  if (_.has(sessions, sessionName)) {
    session = reopenSession(sessionName, resource);
  } else {
    session = createNewSession(sessionName, resource);
  }
  
  sessions.__current = session;
}

/**
 * Reopens an existing editor session.
 */
var reopenSession = function(name, resource) {
  tabs.select(resource);
  var session = sessions[name];

  editor.setSession(session.session);
  editor.gotoLine(session.cursor.row + 1, session.cursor.column + 1);
  
  return session;
};

/**
 * Creates a new editor session.
 */
var createNewSession = function(name, resource) {
  tabs.add(resource);
  editor.setSession(ace.createEditSession(resource.data));
  editor.getSession().setMode("ace/mode/" + utils.guessLanguage(resource.name));
  var session = {
    session: editor.getSession(),
    cursor: editor.getSelection().getCursor(),
    name: name
  };
  
  sessions[name] = session;
  
  return session;
}

/**
 * Saves the latest state of the current session.
 * 
 * Used to remember the position of the cursor in
 * this session.
 */
var saveCurrentSession = function() {
  var session = editor.getSession();
  
  if (session && sessions.__current) {
    sessions[sessions.__current.name].cursor = editor.getSelection().getCursor();
  }
};

/**
 * Whenever there is a focus change, we should
 * blur the editor if we are leaving it.
 */
events.on('state.focus.change', function(state){
  if (state.prev === 'tab') {
    editor.blur();
  }
});

/**
 * Focus on the editor whenever the global
 * focus points to it.
 */
editor.on('focus', function(){
  state.switchFocus('tab');
});

/**
 * If anyone switches the focus from outside,
 * let's set the focus on the editor as well.
 */
events.on('state.focus.tab', function(){
  editor.focus();
});

/**
 * We're gonna remove the editor's session
 * once a tab is closed.
 */
events.on('tabs.close', function(resource){
  delete sessions[resource.path];
  
  if (!$('.tab[resource]').length) {
    editor.setValue('');
    delete sessions.__current;
  }
});

/**
 * Wrapped editor.
 */
module.exports = {
  sessions: {},
  getLine: function() {
    return editor.selection.anchor.row;
  },    
  getValue: function() {
    return editor.getValue();
  },
  openFile: function(resource) {
    setSession(resource);
    setFormatting();
    state.switchFocus('tab');
  }
};

<|start_filename|>server/serve-index.js<|end_filename|>
/**
 * A simple utility to serve the index.html.
 *
 * @type {*}
 */
var fs      = require('fs');
var path    = require('path');
var debug   = require('debug')('nikki:serve-index');

module.exports = function(req, res) {
    fs.readFile(path.join(__dirname, '..', 'client/index.html'), function (err, data) {
        debug('serving index.html')
        if (err) {
            res.writeHead(500);
            return res.end('A subtle error happened. Embarassing.');
        }

        res.writeHead(200);
        res.end(data);
    });
}

<|start_filename|>client/scripts/resource.js<|end_filename|>
var events  = require('./events');
var bar     = require('./bar');
var socket  = require('./socket');

socket.on('resource.opened', function(resource){
    events.dispatch('resource.opened', resource);
});

socket.on('resource.saved', function(resource){
    bar.info('The file ' + resource.name + ' has been saved (' + resource.path + ')');
});

socket.on('resource.deleted', function(resource){
    bar.info('The ' + resource.type + ' ' + resource.name + ' has been deleted (' + resource.path + ')');
});

<|start_filename|>server/daemonize.js<|end_filename|>
var fs        = require('fs');
var proc      = require('child_process');
var blankline = console.log;

/**
 * Daemonize a nikki process.
 * 
 * Here we are gonna relaunch
 * ourselves with the --deamon
 * option.
 */
module.exports = function(port) {
  var args = process.argv;
  args.shift();
  args.splice(1, 0, '--daemon');

  out = err = fs.openSync('./nikki.log', 'a');
  
  var child = proc.spawn('node', args, {
    detached: true,
    stdio: ['ignore', out, err]
  });
  child.unref();
  
  blankline();
  console.log('Nikki running on port', port);
}

<|start_filename|>server/run.js<|end_filename|>
var argv            = require('yargs').argv;
var _               = require('lodash');
var path            = require('path');
var prettyjson      = require('prettyjson');
var config          = require('./config');
var daemonize       = require('./daemonize');
var signals         = require('./signals');
var staticproxy     = require('./static-proxy');
var socket          = require('./socket');
var serveIndex      = require('./serve-index');
var utils           = require('./utils');
var blankline       = console.log;
var defaultProject  = config.get('projects.default');
var port            = argv.port || config.get('app.port');
var host            = argv.host || config.get('app.host');

if (defaultProject === 'cwd') {
  defaultProject = process.cwd();
}
  
var url         = 'http://' + host + ':' + port + '/' + defaultProject;
var shouldOpen  = _.has(argv, 'open') ? argv.open : config.get('app.open');

var run = function() {
  var app = require('http').createServer(handler);
  
  app.listen(port);

  if (shouldOpen) {
    require("open")(url);
  }
  
  function handler (req, res) {
    if (!path.extname(req.url)) {
      serveIndex(req, res);
    }
  
    staticproxy('/bower_components', req, res);
    staticproxy('/client', req, res);
  }
  
  /**
   * HERE STARTS THE FUN!
   */
  socket.startTheFun(app);
}

module.exports = function() {
  blankline();
  console.log(prettyjson.render({'Configuration used': config.get()}, {noColor: true}));

  if (argv.daemon) {
    signals.setup();
    run();
  } else {
    daemonize(port);
    
    if (!shouldOpen) {
      blankline();
      console.log('Open your browser at ' + url + ' to access the IDE')
    }
  }
}

<|start_filename|>client/scripts/preview.js<|end_filename|>
var utils   = require('./utils');
var ui      = require('./ui');
var editor  = require('./editor');
var state   = require('./state');
var bar     = require('./bar');
var preview = {};

/**
 * Generates the preview for the current
 * open file.
 */
var generatePreview = function() {
  var handler = utils.guessLanguage(state.openFile.path);
  
  if (preview.handlers[handler]) {
    return preview.handlers[handler](editor.getValue())
  }
  
  return '<div><p>Extremely sorry but...there is no preview available for ' + handler + ' files</p><br /><img src="/client/images/unfair.gif" /></div>';
};

/**
 * All handlers registered for previewing files.
 * 
 * They expose a function that takes file contents
 * as input and returns HTML as output.
 */
preview.handlers = {
  markdown: function(content) {
    return markdown.toHTML(content);
  }
};

/**
 * Toggles the preview.
 * 
 * #GIF-FTW!
 */
preview.toggle = function(){
  if (state.openFile) {
    var overlay = ui.preview();
    
    if (!overlay.length) {
      overlay = $('<div>');
      overlay.attr('id', 'preview');
      var content = $('<div>');
      content.html(generatePreview());
      
      overlay.html(content);
      $('body').prepend(overlay);
      $('body').addClass('overlayed');
    } else {
      $('body').removeClass('overlayed');
      overlay.remove();
    }
  } else {
    bar.alert('To preview files you must focus on an open editor tab');
  }
};

module.exports = preview;

<|start_filename|>client/scripts/socket.js<|end_filename|>
/**
 * Wrapping socket.io so that
 * every other module can include
 * it as a dependency and doesnt need
 * to be injected.
 */
module.exports = io('http://localhost:' + window.location.port)

<|start_filename|>server/config.js<|end_filename|>
/**
 * Configuration module.
 *
 * We will load nikki's default configuration
 * and merge it with the user-defined configuration.
 *
 * @type {*}
 */
var _             = require('lodash');
var fs            = require('fs');
var yaml          = require('js-yaml');
var reconfig      = require('reconfig');
var p             = require('path');
var debug         = require('debug')('nikki:config');
var utils         = require('./utils');
var configuration = {
    paths:  [
        p.join(__dirname, '..'),
        p.join(utils.getUserHomeDir()),
        p.join(process.cwd())
    ]
};

var config = new reconfig(configuration);

config.reload = function() {
  _.forEach(config.get('paths'), function(path){
    try {
      var filePath  = p.join(path, '.nikki.yml');
      config.config = _.merge(config.config, yaml.safeLoad(fs.readFileSync(filePath, 'utf8')));
      debug('Loaded config in ', filePath)
    } catch (err) {
      debug('Warning while loading the config ', err)
    };
  });
};

config.reload();

module.exports = config;

<|start_filename|>gulpfile.js<|end_filename|>
'use strict';
var gulp = require('gulp'),
    plugins = require('gulp-load-plugins')(),
    runSequence = require('run-sequence'),
    browserify = require('gulp-browserify');

gulp.task('browserify', function() {
    gulp.src('client/scripts/index.js')
        .pipe(browserify({
            insertGlobals : true
        }))
        .pipe(gulp.dest('./client'));
});

gulp.task('server', function(cb) {
    plugins.nodemon({
        script: 'server/index.js',
        ext: 'js',
        env: {'NODE_ENV': 'development', 'DEBUG': 'nikki:*'},
        args: ['--port=9000', '--no-open', '--daemon', '--dport=9001']
    });
    cb();
});

gulp.task('watch', function () {
    gulp.watch('client/scripts/**/*.js', ['browserify']);
});

/**
 * Start the fun
 */
gulp.task('develop', function(cb) {
    runSequence(
        'browserify',
        'server',
        'watch',
        cb
    );
});

/**
 * Default task to run nikki in dev
 */
gulp.task('default', function(cb) {
    gulp.start('develop');
});

<|start_filename|>client/scripts/error-handler.js<|end_filename|>
/**
 * The error handler defines some behaviours
 * for when nikki decides to go bonkers.
 * 
 * Nothing weird happens here, as we are mainly
 * responsible for informing the user that
 * something weird is going on.
 */
var bar     = require('./bar');
var socket  = require('./socket');

/**
 * Generic error wrapper for when socket.io
 * wants to have some fun wiith us.
 */
socket.on('error', function(error) {
  if (error.type === 'TransportError') {
    bar.error("Aww, an awful error happened: " + error.description.message);
  } else {
    console.log(error);
  }
});

/**
 * Displaying errors.
 */
socket.on('server.error', function(error) {
  bar.error(error);
});

/**
 * I sense nastiness.
 */
socket.on('server.alert', function(error) {
  bar.alert(error);
});

<|start_filename|>client/scripts/bar.js<|end_filename|>
var state = require('./state');
var ui    = require('./ui');

/**
 * Sets a new bar up, by interrupting 
 * the previous one, adding a message and
 * a style to the new one.
 * 
 * The setup method will expect you to call
 * bar.show() to actually make it visible.
 */
var setupBar = function(message, type) {
  var bar         = ui.bar();
  
  bar.finish();
  bar.removeClass();
  bar.addClass(type)
  bar.text(message);
  
  return bar;
};

/**
 * Triggers a flash message of the given type.
 *
 * @param message
 * @param type
 */
var flash = function(message, type) {
  var bar = setupBar(message, 'message-' + type);
  bar.fadeIn(200).delay(3000).fadeOut('slow');
};

/**
 * State object.
 *
 * @type {{info: info, alert: alert, error: error}}
 */
module.exports = {
  hide: function() {
    ui.bar().removeClass().hide();
  },
  isOpen: function() {
    return ui.bar().is(':visible');
  },
  question: function() {
    var bar = ui.bar();
    
    return this.isOpen() && bar.hasClass('message-question') && bar;
  },
  ask: function(message, event, subject) {
    var bar = setupBar(message + ' (type y/n)', 'message-question');
    bar.attr('event', event);
    bar.attr('subject', JSON.stringify(subject));
    bar.show();
    bar.focus();
  },
  /**
   * Triggers an info message.
   *
   * @param message
   */
  info: function(message) {
      flash(message, 'info');
  },
  /**
   * Triggers an alert.
   *
   * @param message
   */
  alert: function(message) {
      flash(message, 'alert');
  },
  /**
   * Triggers an error.
   *
   * @param message
   */
  error: function(message) {
      flash(message, 'error');
  }
};

<|start_filename|>client/scripts/config.js<|end_filename|>
var reconfig = require('reconfig');
var socket   = require('./socket');
var events   = require('./events');

var config = new reconfig({});

socket.emit('readyOn', window.location.pathname);

socket.on('config', function(data){
  config.config = data;

  events.dispatch('config.loaded');
});

module.exports = config;

<|start_filename|>client/scripts/index.js<|end_filename|>
/**
 * Setup the sockets.
 */
var socket = require('./socket');
var events = require('./events');

/**
 * Boot the config.
 */
require('./config');

/**
 * Once the config is loaded, boot the app.
 */
events.on('config.loaded', function(){
  require('./fs');
  require('./resource');
  require('./keyboard-shortcuts');
  require('./ui');
  require('./error-handler');

  /**
   * FUN TIMES AHEAD!
   */
  socket.emit('boot', {path: window.location.pathname});
});

<|start_filename|>server/index.js<|end_filename|>
var argv            = require('yargs').argv;
var config          = require('./config');
var signals         = require('./signals');
var run             = require('./run');
var blankline       = console.log;
var port            = argv.port || config.get('app.port');

process.title = config.get('general.process-name');

/**
 * nikki --stop
 */
if (argv.stop) {
  console.log('shutting down...');
  signals.sendShutdown();
  
  return;
}

/**
 * nikki --status
 */
if (argv.status) {
  signals.status();
  
  return;
}

/**
 * If we get to this point, it means
 * we are gonna run the app.
 */
run();

<|start_filename|>client/scripts/fs.js<|end_filename|>
var _           = require('lodash');
var p           = require('path');
var keyboard    = require('./keyboard');
var socket      = require('./socket');
var state       = require('./state');

/**
 * Hovering on the filesystem will
 * automatically switch the focus
 * there.
 */
$('#fs').mouseenter(function(){
  state.switchFocus('fs');
  $(this).focus();
});

/**
 * Builds the title of the page.
 *
 * @param path
 */
var buildTitle = function(path) {
    var title       = $('<h2>')
    title.attr('id', 'navigation');

    var segments    = [];

    _.each(path.split('/').splice(1), function(directory){
        var segment = $('<a>');
        segment.addClass('segment');
        segment.text('/' + directory);
        segments.push(directory);
        segment.attr('href', '/' + segments.join('/'));

        title.append(segment);
    });

    $('#subject').html('').append(title);

    $('#navigation .segment').click(function(){
        socket.emit('resource.open', {path: $(this).attr('href')});

        return false;
    });
};

/**
 * Rules that assign some CSS classes to resources
 * based on their (guessed) type.
 */
var iconRules = {
    read: function(resource) {
        if (_.contains(['.md', '.markdown'], p.extname(resource.name))) {
            return "glyphicon fa fa-book fa-1x";
        }
    },
    lock: function(resource) {
        if (resource.type === 'file' && p.extname(resource.name) === '.lock') {
            return "glyphicon fa fa-lock fa-1x";
        }
    },
    html: function(resource) {
        if (resource.type === 'file' && p.extname(resource.name) === '.html') {
            return "glyphicon fa fa-code fa-1x";
        }
    },
    sql: function(resource) {
        if (resource.type === 'file' && p.extname(resource.name) === '.sql') {
            return "glyphicon fa fa-database fa-1x";
        }
    },
    log: function(resource) {
        if (resource.type === 'file' && p.extname(resource.name) === '.log') {
            return "glyphicon fa fa-exclamation-triangle fa-1x";
        }
    },
    coffeescript: function(resource) {
        if (resource.type === 'file' && p.extname(resource.name) === '.coffee') {
            return "glyphicon fa fa-coffee fa-1x";
        }
    },
    nikki: function(resource) {
        if (resource.name === '.nikki.yml') {
            return "glyphicon fa fa-heart fa-1x";
        }
    },
    git: function(resource) {
        if (resource.name.match(/^\.git/)) {
            return "glyphicon fa fa-git fa-1x";
        }
    },
    executable: function(resource) {
        if (resource.type === 'file' && !p.extname(resource.name)) {
            return "glyphicon fa fa-terminal fa-1x";
        }
    },
    dir: function(resource) {
        if (resource.type === 'directory') {
            return "glyphicon fa fa-folder fa-1x";
        }
    },
    file: function(resource) {
        if (resource.type === 'file') {
            return "glyphicon fa fa-file-text-o fa-1x";
        }
    }
};

/**
 * Adds an icon based on the resource.
 */
var iconize = function(icon, resource) {
    var match = false;

    _.forEach(iconRules, function(rule){
        if (!match && rule(resource)) {
            icon.addClass(rule(resource));

            match = true;
        }
    });
};

/**
 * Adds a resource to the filesystem.
 *
 * @param resource
 * @param index
 */
var addResource = function(resource) {
    var res = $('<li>')
    res.addClass(resource.type);

    var icon = $('<span>')

    iconize(icon, resource);

    res.addClass('resource');

    if (!$('.resource.active').length) {
        res.addClass('active');
    }

    res.attr('id', resource.path);
    res.text(resource.name);
    
    if (state.focus === 'bar') {
      var path = $('<p>');
      path.addClass('path')
      path.text(resource.parent);
      
      res.append(path);
      
      if (resource.misc) {
        resource.misc.forEach(function(misc){
          var m = $('<p>');
          m.addClass('misc')
          m.text("> " + misc + "...");
          
          res.append(m);
        });
      }
    }
    
    res.hover(function(){
        $('.resource.active').removeClass('active');
        res.addClass('active');
    });
    res.attr('data-resource', JSON.stringify(resource))
    res.click(function(){
        socket.emit('resource.open', resource)
    });

    res.prepend(icon);

    $('#fs ul').append(res);
}

/**
 * Opens a directory on the filesystem.
 *
 * @param fs
 */
var openDir = function(fs) {
    buildTitle(fs.root.path)
    var filesystem = $('<ul>')
    $('#fs').html('').append(filesystem);
    $('#fs').attr('filesystem', JSON.stringify(fs));

    /**
     * Super-ghetto trick explained!
     * 
     * If we are coming from a popstate we prevent
     * logging the openDir in the history,
     * else we will never be able to really go back
     * as everytime we go back we'd trigger a fs.open
     * event and adding an entry in the history...
     * 
     * Basically we'd go back in history and add a new
     * entry in it, so you really don't go back...
     */
    if (!fs.root.from || fs.root.from !== 'window.popstate') {
        history.pushState(fs, null, fs.root.path);
    }

    _.each(fs.resources, function(resource, index){
        addResource(resource)
    })

    $('[id="' + fs.root.path + '"]').addClass('active');
};

/**
 * Returns the structure of the current
 * filesystem as it was received by the
 * server.
 *
 * @returns {*}
 */
var getStructure = function() {
    return JSON.parse($('#fs').attr('filesystem'))
}

/**
 * Resets the current filesystem.
 */
var reset = function() {
    openDir(getStructure());
}

/**
 * Super-ghetto trick!
 * 
 * Adding the 'from' state to the FS
 * object.
 * 
 * Look for 'window.popstate' to check
 * why we need this.
 */
window.onpopstate = function(event) {
    if (event.state && event.state.root) {
        root        = event.state.root;
        root.from   = 'window.popstate';

        socket.emit('resource.open', root);
    }
}

var moveUp = function() {
  try {
    var path  = JSON.parse($('#fs').attr('filesystem')).root.path;
    var parts = path.split('/');
    parts.pop();
    path      = parts.join('/');

    socket.emit('resource.open', {path: path});
  } catch (err) {}
};

/**
 * Let's open a new filesystem once the
 * server sends the related event.
 */
socket.on('fs.root', function (fs) {
    openDir(fs);
});

module.exports = {
    buildTitle: buildTitle,
    addResource: addResource,
    reset: reset,
    moveUp: moveUp,
    getStructure: getStructure
}

<|start_filename|>server/static-proxy.js<|end_filename|>
/**
 * A simple proxy for serving aliased client files.
 *
 * @type {*}
 */
var fs    = require('fs');
var p     = require('path');
var url   = require('url');
var debug = require('debug')('nikki:static-proxy');

var contentTypes = {
    '.js':      'application/javascript',
    '.css':     'text/css',
    '.eot':     'application/vnd.ms-fontobject',
    '.ttf':     'font/ttf',
    '.woff':    'application/font-woff',
    '.svg':     'image/svg+xml'
};

module.exports = function(path, req, res) {
    var parts = req.url.split(path);


    if (parts[0] == '') {
        var filePath = url.parse(parts[1]).pathname;
        fs.readFile(p.join(__dirname, '..', path, filePath), function (err, data) {
            debug('Served ', filePath)
            res.writeHead(200, {'content-type': contentTypes[p.extname(filePath)]});
            res.end(data);
        });
    };
}

<|start_filename|>bin/nikki.js<|end_filename|>
#!/usr/bin/env node
'use strict';

var path    = require('path');
var fs      = require('fs');
var argv    = require('yargs').argv;
var yaml    = require('js-yaml');

var nikki  = require(path.join(__dirname, '..', 'package.json'));
var config = yaml.safeLoad(fs.readFileSync(path.join(__dirname, '..', '.nikki.yml'), 'utf8'));

console.log("Nikki v" + nikki.version);

if (argv.version) {
    return;
}

if (argv.help) {
    console.log();
    console.log('=== ' + nikki.description + ' ===');
    console.log();
    console.log(' * homepage: ' + nikki.homepage);
    console.log(' * repository: ' + nikki.repository.url);
    console.log(' * bugs: ' + nikki.bugs.url);
    console.log();
    console.log(" To start the IDE, simply run the `nikki` command.");
    console.log();
    console.log(' Options:');
    console.log();
    console.log('    --stop       stop nikki');
    console.log('    --status     checks whether nikki is running');
    console.log('    --host       defines on which host we will run (default: ' + config.app.host + ')');
    console.log('    --port       defines on which port we will run (default: ' + config.app.port + ')');
    console.log('    --dport      defines on which port the daemon will listen for signals (default: ' + config.daemon.port + ')');
    console.log('    --no-open    do not open the browser after booting');
    console.log();
    console.log(' Examples:');
    console.log();
    console.log('    $ nikki');
    console.log('    $ nikki --no-open');
    console.log('    $ nikki --port 8000');
    console.log('    $ nikki --host nikki.local --port 8000 --no-open');
    console.log();
    console.log('Proudly crafted by ' + nikki.author.name + ' and released under the ' + nikki.license + ' license');
    console.log('Feel free to reach out at ' + nikki.author.email);

    return;
}

require('./../server/index')

<|start_filename|>server/socket.js<|end_filename|>
/**
 * Handling socket events.
 *
 * @type {*}
 */
var fs          = require('fs');
var fse         = require('fs-extra');
var _           = require('lodash');
var p           = require('path');
var debug       = require('debug')('nikki:socket');
var config      = require('./config');
var utils       = require('./utils');
var search      = require('./search');

var sanitizePath = function(path) {
    return path.replace('~', utils.getUserHomeDir()).replace('//', '/');
}

module.exports = {
    watcher: null,
    startTheFun: function(app) {
        var self = this;
        var io   = require('socket.io')(app);

        io.on('connection', function (socket) {
            debug('connected');
            socket.on('readyOn', function(path){
                debug('client is ready on ', path);
                path = sanitizePath(path);

                config.config.paths.push(path);
                config.reload();

                socket.emit('config', config.get());

                socket.on('search.find', function (options) {
                    debug('client searching', options);
                    search.find(options, socket);
                });
                
                socket.on('search.grep', function (options) {
                    debug('client grepping', options);
                    search.grep(options, socket);
                });
                
                socket.on('resource.delete', function (resource) {
                    debug('deleting resource', resource);
                    fse.remove(resource.path, function(err){
                      if (err) throw err;
                    
                      socket.emit('resource.deleted', resource);
                      self.openDir({path: resource.parent}, socket);
                    });
                });

                socket.on('boot', function (resource) {
                    self.openDir(resource, socket, function(){
                      socket.emit('boot.done');
                    });
                });

                socket.on('resource.open', function(resource){
                    debug('opening resource: ', resource);
                    
                    if (resource.type === 'file') {
                        fs.readFile(resource.path, 'utf-8', function(err, data){
                            if (err && err.code === 'ENOENT') {
                              message = "File {f} does not exist, was probably deleted outside of nikki";
                              socket.emit('server.alert', message.replace('{f}', err.path));
                              return;
                            }
                            
                            resource.data = data;
                            socket.emit('resource.opened', resource);
                        });
                    } else {
                        self.openDir(resource, socket)
                    }
                });

                socket.on('resource.save', function(resource){
                    fs.writeFile(resource.path, resource.data, function(err) {
                        if (err) {
                            var message = "Unable to save file {f} -- do you have permissions to write to it? (error code: {c})".replace('{f}', resource.path).replace('{c}', err.code);

                            socket.emit('server.error', message);

                            return;
                        };
                        socket.emit('resource.saved', resource);
                    });
                    debug('saved resource ', resource);
                });
            });
        });
    },
    openDir: function(resource, socket, callback) {
        var self  = this;
        debug('opening dir ', resource);
        resource.path = sanitizePath(resource.path);

        fs.readdir(resource.path, function (err, files) {
            if (err && err.code === 'ENOENT') {
              socket.emit('server.error', "Directory {dir} does not exist".replace('{dir}', err.path));
              return;
            }
          
            resources = [];

            _.each(files, function(file){
                var path = p.join(resource.path, file);
                
                try {
                  resources.push({
                      type:   fs.lstatSync(path).isDirectory() ? 'directory' : 'file',
                      name:   file,
                      parent: resource.path,
                      path:   path
                  });
                } catch (err) {
                  socket.emit('server.error', "An error occurred doing a stat of {path}".replace('{path}', path));
                }
            });

            var resources = _.filter(_.sortBy(resources, function(resource) {
                return resource.type + resource.name;
            }), function(resource){
                return !_.contains(config.get('projects.ignore'), resource.name);
            });

            socket.emit('fs.root', {root: resource, resources: resources});
            callback && callback();

            self.watch(resource, function(){
              self.openDir(resource, socket);
            });            
        });
    },
    watch: function(resource, callback) {
      if (!config.get('general.watch')) {
        return;
      }
      
      var self = this;
      
      if (self.watcher) {
        debug('Stopping previously registered fs watcher')
        self.watcher.close();
      }
      
      debug('Watching files and directories in', resource.path)
      self.watcher = fs.watch(resource.path, {}, callback);
    }
}

<|start_filename|>client/scripts/utils.js<|end_filename|>
var path    = require('path');
var config  = require('./config');

module.exports = {
    guessLanguage: function(filename) {
        var extension = path.extname(filename).substring(1);

        return config.get('editor.extensions')[extension] || 'text';
    }
}

<|start_filename|>server/signals.js<|end_filename|>
var dnode  = require('dnode');
var config = require('./config');
var argv   = require('yargs').argv;
var port   = argv.dport || config.get('daemon.port');

/**
 * Basic signals for running
 * nikki in detached mode.
 */
module.exports = {
  /**
   * Setup the daemon.
   */
  setup: function() {
    var server = dnode({
      shutdown : function () {
          console.log('Received shutdown message...bye!')
          process.exit(0);
      }
    });
    
    server.listen(port);
  },
  /**
   * Send nikki a shutddown message, so
   * that the daemon can stop.
   */
  sendShutdown: function() {
    var d = dnode.connect(port);
        
    d.on('remote', function (remote) {
        remote.shutdown();
        console.log('Nikki has been stopped')
        process.exit(0);
    });      
    
    d.on('error', function (remote) {
        console.log('Nikki was not running');
    });
    
    return;
  },
  /**
   * Is a nikki process running?
   */
  status: function() {
    var d = dnode.connect(port);
        
    d.on('remote', function (remote) {
        console.log('Nikki is running')
        process.exit();
    });      
    
    d.on('error', function (remote) {
        console.log('Nikki is not running');
    });
    
    return;
  }
}

<|start_filename|>server/utils.js<|end_filename|>
/**
 * Utility module
 *
 * @type {{getUserHomeDir: home}}
 */
module.exports = {
    getUserHomeDir: function() {
        return process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
    }
}

<|start_filename|>server/search.js<|end_filename|>
/**
 * Utility module used to execute
 * filesystem searches.
 * 
 * @type {*}
 */
var p       = require('path');
var _       = require('lodash');
var fs      = require('fs');
var debug   = require('debug')('nikki:search');
var config  = require('./config');
var search = {};

/**
 * Returns a regex used for file search,
 * from the given text.
 *
 * We are gonna match exact words as long
 * as they're not separated by a space, which
 * means that matching the file `my_file.txt`
 * will give the following results:
 *
 * my: true
 * my fi: true
 * my dir: false
 *
 * @param text
 * @returns {*}
 */
search.getRegex = function(text) {
  var regexString = '';

  text.split(' ').forEach(function(word){
    word = word.trim();

    if (word) {
      regexString += "(.*)(" + word + ")"
    }
  });

  return RegExp(regexString, "ig");
};

/**
 * Returns an instance of the finder.
 */
search.getFinder = function(path) {
  return require('findit')(path);
};

/**
 * Notifies the client that we've found
 * something.
 */
search.notifyResult = function(resource, socket) {
  socket.emit('search.result', resource)
  debug('search result ', resource);  
};

/**
 * Executes a file search on the filesystem,
 * given the search options, and emits an
 * event to notify that a file has been
 * matched.
 *
 * @param options
 * @param socket
 */
search.find = function(options, socket) {
  var finder = search.getFinder(options.root.path);
  
  finder.on('directory', function (dir, stat, stop) {
    var base = p.basename(dir);

    if (_.contains(config.get('search.exclude'), base)) {
      stop();
    } else {
      if (dir.match(search.getRegex(options.text))) {
        search.notifyResult({
          path: dir,
          type: 'directory',
          name: base,
          parent: p.dirname(dir)
        }, socket);
      }
    }
  });
  
  finder.on('file', function (file, stat) {
    if (file.match(search.getRegex(options.text))) {
      search.notifyResult({
        path: file,
        type: 'file',
        name: p.basename(file),
        parent: p.dirname(file)
      }, socket);
    }
  });
};

/**
 * Searches for content in files.
 * 
 * We are going go tool through all files
 * in options.root.path and look for the
 * string options.text in them.
 * 
 * In other IDEs, this functionality might
 * be referred as "Find in files".
 */
search.grep = function(options, socket) {
  var finder =  search.getFinder(options.root.path);
  
  finder.on('directory', function (dir, stat, stop) {
    var base = p.basename(dir);

    if (_.contains(config.get('search.exclude'), base)) {
      stop();
    }
  });
  
  finder.on('file', function (file, stat) {
    fs.readFile(file, 'utf8', function (err,data) {
      if (err) {
        console.log(err);
        return;
      }
      
      var matches = data.match(search.getRegex(options.text));
      
      if (matches) {
        search.notifyResult({
          path: file,
          type: 'file',
          name: p.basename(file),
          parent: p.dirname(file),
          misc: matches
        }, socket);
      }
    });
  });
};

module.exports = search;

<|start_filename|>client/scripts/keyboard-shortcuts.js<|end_filename|>
/**
 * This module simply defines keyboard actions
 * to be performed on certain keyboard shortcuts.
 *
 * @type {exports}
 */
var state           = require('./state');
var config          = require('./config');
var keyboard        = require('./keyboard');
var bar             = require('./bar');
var search          = require('./search');
var editor          = require('./editor');
var socket          = require('./socket');
var tabs            = require('./tabs');
var config          = require('./config');
var ui              = require('./ui');
var fs              = require('./fs');
var preview         = require('./preview');

/**
 * Returns a keybord shortcut given its name.
 */
var shortcut = function(name) {
  var shortcut = config.get('editor.shortcuts.' + name);
  
  if (!shortcut) {
    bar.error("Configuration error: please map the '{s}' shortcut".replace('{s}', name));
  }
  
  return shortcut;
};

/**
 * Delete a file
 */
keyboard.on(shortcut('file_preview'), function() {
  preview.toggle();
});

/**
 * Delete a file
 */
keyboard.on(shortcut('directory_move_up'), function() {
  if (state.focus === 'fs') {
    fs.moveUp();
  }
});

/**
 * Delete a file
 */
keyboard.on(shortcut('question_confirm'), function() {
  var question = bar.question();
  
  if (question) {
    var event   = question.attr('event');
    var subject = JSON.parse(question.attr('subject'));
    socket.emit(event, subject);
    bar.hide();
    
    return false;
  }
});

/**
 * Delete a file
 */
keyboard.on(shortcut('question_abort'), function() {
  var question = bar.question();
  
  if (question) {
    bar.hide();
    
    return false;
  }
});

/**
 * Delete a file
 */
keyboard.on(shortcut('file_delete'), function() {
  var activeResource = $('.resource.active');
  
  if (activeResource.length) {
    var resource = JSON.parse(activeResource.attr('data-resource'));
    
    bar.ask('Are you sure you want to delete ' + resource.path + '?', 'resource.delete', resource);
  }

  return false;
});

/**
 * Open in github.
 * 
 * Please note that this feature depends on the configuration.
 */
keyboard.on(shortcut('open_in_github'), function() {
    if (state.openFile && config.get('github.enabled')) {
        var url       = 'https://github.com/';
        var username  = config.get('github.username');
        var parts     = state.openFile.path.replace(config.get('github.path'), '');
        var repo      = parts.split('/')[1];
        var path      = parts.split(repo)[1];
        var url       = url + username + '/' + repo + '/blob/master/' + path + '#L' + editor.getLine();

        window.open(url, '_tab');
    }

    return false;
});

/**
 * Closes a tab.
 */
keyboard.on(shortcut('tab_close'), function() {
    tabs.closeActive();

    return false;
});

/**
 * Moves to the tab on the left.
 */
keyboard.on(shortcut('tab_move_left'), function() {
    tabs.moveLeft();

    return false;
});

/**
 * Moves to the tab on the right.
 */
keyboard.on(shortcut('tab_move_right'), function() {
    tabs.moveRight();

    return false;
});

/**
 * Save file
 */
keyboard.on(shortcut('file_save'), function() {
    if (state.focus === 'tab') {
        bar.alert('Saving...');
        state.openFile.data = editor.getValue();

        socket.emit('resource.save', state.openFile);
    }

    return false;
});

/**
 * Switch focus
 */
keyboard.on(shortcut('focus_switch'), function() {
    var focus = 'tab';

    if (state.focus === 'tab') {
        focus = 'fs';
    }

    state.switchFocus(focus);
});

/**
 * Local search
 */
keyboard.on(shortcut('find'), function() {
    search.toggle();

    return false;
});

/**
 * Local search
 */
keyboard.on(shortcut('grep'), function() {
    search.toggle('grep');

    return false;
});

/**
 * Down arrow
 */
keyboard.on(shortcut('file_move_down'), function() {
    if (state.focus === 'fs') {
        var resource = $('.resource.active');
        var canGoDown = resource.next().hasClass('resource');

        if (canGoDown) {
            resource.removeClass('active');
            resource.next().addClass('active');
        } else {
            bar.alert("Where ya goin?")
        }

        return false;
    }
});

/**
 * Up arrow
 */
keyboard.on(shortcut('file_move_up'), function() {
    if (state.focus === 'fs') {
        var resource = $('.resource.active');
        var canGoDown = resource.prev().hasClass('resource');

        if (canGoDown) {
            resource.removeClass('active');
            resource.prev().addClass('active');
        } else {
            bar.alert("Where ya goin?")
        }

        return false;
    }
});

/**
 * Open files / directories
 */
keyboard.on(shortcut('file_open'), function() {
    if (state.focus === 'fs') {
        var resource = $('.resource.active');

        if (resource) {
            socket.emit('resource.open', JSON.parse(resource.attr('data-resource')))
        }

        return false;
    }
});

<|start_filename|>client/scripts/events.js<|end_filename|>
/**
 * A simple event dispatcher.
 *
 * @type {{on: on, dispatch: dispatch}}
 */
var _ = require('lodash');

/**
 * Here we are gonna store all the
 * listener to the events.
 */
var listeners = {};

module.exports = {
  /**
   * Used to listen to an event.
   */
  on: function(event, callback) {
    if (!listeners[event]) {
      listeners[event] = [];
    }

    listeners[event].push(callback);
  },
  /**
   * Trigger an event.
   */
  dispatch: function(event, data) {
    if (listeners[event]) {
      _.each(listeners[event], function(listener){
        listener(data);
      });
    }
  }
}

<|start_filename|>client/scripts/state.js<|end_filename|>
/**
 * This module keeps track of the state
 * of the IDE.
 *
 * @type {{openFile: null}}
 */
var events  = require('./events');

/**
 * Represents nikki's current state.
 */
var state   = {
  /**
   * Currently open file.
   */
  openFile: null,
  /**
   * Focus of the editor.
   */
  focus:    'fs',
  /**
   * Switches the focus and triggers
   * some events so that components
   * can bind focus change on their own.
   */
  switchFocus: function(focus) {
    if (this.focus === focus) {
      return;
    }
    
    events.dispatch('state.focus.change', {
      prev: this.focus,
      next: focus
    });
    events.dispatch('state.focus.' + focus, this);

    this.focus = focus;
  }
}

/**
 * Whenever a file is opened, we'll track it
 * here as the "open file".
 */
events.on('resource.opened', function(resource){
  state.openFile = resource;
});

/**
 * Exports (You don't say?!).
 */
module.exports = state;