Tags: Node.JS »»»» Electron »»»» Vue.js
Many kinds of applications need to browse files in the file system. For example most programmers text editors (I'm writing this in Microsoft Visual Studio Code) include a sidebar showing the file-system. Therefore it's important to have such a component to use in Electron/Vue.js applications. In this article we'll look at one way to implement such a component.
To follow along with this article, clone the following repositories:
- https://github.com/robogeek/vue-file-tree-demo -- Demonstration application
- https://github.com/robogeek/vue-file-tree -- Component to show file trees
Let's initialize the application:
$ vue init simulatedgreg/electron-vue electron-vue-file-browse
? Application Name electron-vue-file-browse
? Project description Electron/Vue.js file browser
? Select which Vue plugins to install vue-electron, vue-router
? Use linting with ESLint? No
? Set up unit testing with Karma + Mocha? No
? Set up end-to-end testing with Spectron + Mocha? No
? What build tool would you like to use? builder
? author David Herron <david@davidherron.com>
vue-cli · Generated "electron-vue-file-browse".
---
All set. Welcome to your new electron-vue project!
Make sure to check out the documentation for this boilerplate at
https://simulatedgreg.gitbooks.io/electron-vue/content/.
Next Steps:
$ cd electron-vue-file-browse
$ yarn (or `npm install`)
$ yarn run dev (or `npm run dev`)
The workspace at simulatedgreg/electron-vue
includes a very useful framework for developing Electron applications using Vue.js. This downloads the skeleton anc creates the named directory. I've configured it to use vue-electron
and vue-router
but not the other components, and to not include any ESLint or unit testing features. You may find it useful to answer these questions differently. The framework includes support for both electron-builder
and electron-packager
and I've chosen the former.
Next we set up dependencies:
$ cd electron-vue-file-browse
$ npm install buefy walkdir path-directories vue-awesome sl-vue-tree --save
$ npm install
$ npm run dev
The last step is somewhat not-useful at the moment because we've done no configuration.
The chosen dependencies include:
- Buefy is the Vue.js package that uses Bulma for components
- Walkdir supports tracking changes in the files under a given directory
- path-directories helps with splitting directory names that will be useful in building the file browser object tree
- vue-awesome encapsulates the Font Awesome icons as Vue.js components
- sl-vue-tree is a nice tree view component. We'll use this to store the file system contents
Let's start the surgery on the skeleton application.
src/main/main.js
- the Main modeule in the Main process
We'll make a few select changes in main.js
to tweak how the application launches.
mport {
app, BrowserWindow, ipcMain, dialog
} from 'electron';
import path from 'path';
import walkdir from 'walkdir';
Change the imports as shown.
In the end of the supplied createWindow
function add this line of code:
scanDirectory();
Then add the implementation of this function:
const rootdir = '/Volumes/Extra/akasha-tools/electron-vue-file-browse';
function scanDirectory() {
walkdir('src', {})
.on('file', (fn, stat) => {
mainWindow.webContents.send('file', fn.slice(rootdir.length + 1), stat);
})
.on('directory', (fn, stat) => {
mainWindow.webContents.send('directory', fn.slice(rootdir.length + 1), stat);
})
.on('error', (fn, err) => {
console.error(`!!!! ${fn} ${err}`);
});
}
The idea is to "walk" a selected directory tree, sending out information about the files in that tree. Since this is a test application there are some hard-coded values. For a real application we'd have a file browser dialog to select a directory.
The walkdir module ( https://www.npmjs.com/package/walkdir) "Walks a directory tree emitting events based on what it finds." As it traverses the directory tree it emits events concerning what it finds. We simply forward those events to the Renderer process.
The fn
argument is the complete pathname that is found. On studying this application it seems more useful to send not the complete pathname, but the segment following the root directory being scanned. Therefore we use the slice
function to trim the string appropriately.
ipcMain.on('rescan-directory', () => {
scanDirectory();
});
This message can be sent from the Renderer process to cause the Main process to rescan the directory structure.
process.on('unhandledRejection', (reason, p) => {
console.error(`Unhandled Rejection at: ${util.inspect(p)} reason: ${reason}`);
});
This catches any unhandle promise rejection errors. See: Handling unhandled Promise rejections, avoiding application crash
src/renderer/main.js
- the Main module in the Renderer process
Change main.js
to the following
import Vue from 'vue'
import { ipcRenderer } from 'electron';
import App from './App';
import router from './router';
import Buefy from 'buefy';
import 'buefy/lib/buefy.css';
Vue.use(Buefy);
if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
Vue.config.productionTip = false
export const messageBus = new Vue({
});
/* eslint-disable no-new */
new Vue({
components: { App },
router,
template: '<App/>'
}).$mount('#app')
ipcRenderer.on('file', (event, fn, stat) => {
messageBus.$emit('file', fn, stat);
});
ipcRenderer.on('directory', (event, fn, stat) => {
messageBus.$emit('directory', fn, stat);
});
Some of this carries over from the skeleton, and the rest initializes support for some of the dependencies we loaded, as well as useful objects.
We initialize Buefy (the UI toolkit), sl-tree-vue
(the tree viewer), and vue-awesome
(Font Awesome).
The messageBus
object is an extra Vue instance we'll use for sending messages around the application. Vue.js has great support for a child component to send messages to its parent component. But out of the box it does not directly support sending messages between any two components. We will use messageBus
to do so - since all Vue instances contain capabilities to emit and listen to messages.
We see two such messages - the file
and directory
events are received from the Main process, and then are emitted on the messageBus
.
src/renderer/router/index.js
-- The Router
For this application the Router is not really required, but I said "yes" to it above so the code is here. This module is where you would handle multiple application pages. As it stands there is a single route for the /
URL, and it refers to a LandingPage
component. Change it to this:
export default new Router({
routes: [
{
path: '/',
name: 'file-browser-page',
component: require('@/components/FileBrowserMain').default
},
{
path: '*',
redirect: '/'
}
]
});
src/renderer/components/FileBrowserMain.vue
What we've done so far is position this component as the main window of the application. We'll implement it as a single-file-template that will in turn demonstrate using a file-system oriented tree browser.
In src/renderer/components
delete all the supplied files. Those files comprise the demo application, and we need none of that code.
Create FileBrowserTree.vue
to contain this:
<template>
<div id="wrapper" class="columns is-gapless is-mobile">
<file-browser-tree
id="file-tree"
ref="filetree"
class="column"
@nodeClick="nodeClick"
@nodeDoubleClick="nodeDoubleClick"
@nodeDrop="nodeDrop">
<template slot="context-menu">
<div @click="doDashboard">Dashboard</div>
<div @click="doCustomers">Customers</div>
</template>
</file-browser-tree>
<div id="file-info-view" class="column">
<span v-html="fileInfo"></span>
<button class="button is-primary" @click="rescan">Rescan</button>
</div>
</div>
</template>
<script></script>
<style>
@import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro');
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body { font-family: 'Source Sans Pro', sans-serif; }
#wrapper {
height: 100vh;
width: 100vw;
}
#file-tree, #file-info-view {
height: 100%;
overflow: scroll;
}
</style>
In the <template>
we have a simple Buefy columns
component. In the left-side column is the file browser, and the right side is an area to display information. The right side also includes a button to cause rescanning of the directory content.
The Rescan button causes a message to be sent to the Main process. If you refer back to the Main process you see this message causes the chosen directory to be scanned.
When the chosen directory is scanned a series of messages are sent here from the Main process. Those messages are either file
or directory
events, and contain both a pathname and the stat
object corresponding to the file. Those messages are handled in event handlers, and turn into addPathToTree
calls which add the pathname to the tree object. We'll get to that a little later.
Note: <file-browser-tree ... ref="filetree" ...></file-browser-tree>
The ref=
attribute allows us to reference the component's object as so: this.$refs.filetree.addPathToTree(fn, stat, false);
Vue.js makes sure the $refs
object has fields corresponding to every component with a ref=
attribute. This component is documented a little further into the article.
We have three handlers for events emitted by that component:
nodeClick
Handles clicks on itemsnodeDoubleClick
Handles double-clicks on itemsnodeDrop
Handles drag-and-drop operations on items
In this demo application each prints information in the information pane.
The file tree component also supports a context menu, activated by right-click. We use <template slot-name="context-menu">
to inject menu choices into the menu. It automatically pops up at the location of the mouse pointer. The context menu items in the demo application are simply examples, each of which invokes a handler method. An important step of these handler methods is this:
this.$refs.filetree.contextMenuIsVisible = false;
This line of code instructs the file tree component to dismiss the menu.
vue-file-tree
component
We've mentioned this component several times, and now it is time to investigate it.
The repository for the component has a whole build configuration and other scripts. See
https://github.com/robogeek/vue-file-tree for details. In this article we'll only investigate the .vue
file.
To use this component in your application add the following to the dependencies
section of the package.json
in your application: "vue-file-tree": "github:robogeek/vue-file-tree",
. Then follow the further directions in the README, or work it out from the code presented earlier.
The template for this component:
<template>
<span>
<sl-vue-tree
id="vue-file-tree"
ref="slvuetree"
:value="nodes"
:allowMultiselect="false"
@nodeclick="nodeClick"
@nodedblclick="nodeDoubleClick"
@select="nodeSelect"
@toggle="nodeToggle"
@drop="nodeDrop"
@nodecontextmenu="nodeContextMenu"
@externaldrop.prevent="onExternalDropHandler">
<template slot="toggle" slot-scope="{ node }">
<span v-if="!node.isLeaf">
<font-awesome-icon
icon="caret-right"
v-if="!node.isExpanded"></font-awesome-icon>
<font-awesome-icon
icon="caret-down"
v-else-if="node.isExpanded"></font-awesome-icon>
</span>
</template>
<template slot="title" slot-scope="{ node }">
<font-awesome-icon
:icon="[ 'fab', 'js' ]"
v-if='node.data.type === "application/javascript"'></font-awesome-icon>
<font-awesome-icon
icon="table"
v-else-if='node.data.type === "application/json"'></font-awesome-icon>
<font-awesome-icon
icon="image"
v-else-if='node.data.type === "IMAGE"'></font-awesome-icon>
<font-awesome-icon
icon="code"
v-else-if='node.data.type === "EJS"'></font-awesome-icon>
<font-awesome-icon
:icon="[ 'fab', 'vuejs' ]"
v-else-if='node.data.type === "VUEJS"'></font-awesome-icon>
<font-awesome-icon
icon="file"
v-else-if="node.isLeaf"></font-awesome-icon>
{{ node.title }} </template>
<template slot="sidebar" slot-scope="{ node }">
<font-awesome-icon
icon="circle"
v-if="node.data.isModified"></font-awesome-icon>
</template>
</sl-vue-tree>
<aside class="menu vue-file-tree-contextmenu"
ref="contextmenu"
v-show="contextMenuIsVisible">
<slot name="context-menu"></slot>
</aside>
</span>
</template>
What we're doing is to implement a wrapper around yet another component, whose repository is: https://github.com/holiber/sl-vue-tree
There are a number of event handlers and other configuration we've implemented to configure default behavior that makes sense as a tree component displaying files in the file system.
The sl-vue-tree component supports a number of slot's into which we can inject HTML. We've added these:
toggle
This determines which arrow button to show for directory nodes which are expanded, versus ones which are closed.title
This describes one row of the tree component. We want to show an icon based on the file type. We have a means to select these codes to select the correct icon.sidebar
This describes an icon that appears at the far-right of the tree display.
We also have a slot of our own, context-menu
, which is where the context menu HTML arrives. In this case we are using a Buefy menu component. The display of this component is controlled by the contextMenuIsVisible
data item.
Obviously we are using the Font Awesome icon set.
Next, we add the <script>
tag:
<script>
import path from 'path';
import util from 'util';
import splitter from './path-splitdirs';
import mime from 'mime';
import slVueTree from 'sl-vue-tree';
// import 'sl-vue-tree/dist/sl-vue-tree-dark.css';
import { library } from '@fortawesome/fontawesome-svg-core';
import {
faCaretRight, faCaretDown, faTable, faImage, faFile, faCircle, faCode
} from '@fortawesome/free-solid-svg-icons';
import { faJs, faVuejs } from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
library.add(faJs, faVuejs, faCaretRight, faCaretDown, faTable, faImage, faFile, faCircle, faCode);
/* var nodes = [
{title: 'Item1', isLeaf: true},
{title: 'Item2', isLeaf: true, data: { visible: false }},
{title: 'Folder1'},
{
title: 'Folder2', isExpanded: true, children: [
{title: 'Item3', isLeaf: true},
{title: 'Item4', isLeaf: true, data: { isModified: true }}
]
}
]; */
export default {
data() {
return {
nodes: [],
contextMenuIsVisible: false
}
},
components: {
'sl-vue-tree': slVueTree,
'font-awesome-icon': FontAwesomeIcon
},
created() {
/*
* Derived from Buefy's b-dropdown
* https://github.com/buefy/buefy/blob/dev/src/components/dropdown/Dropdown.vue
*/
if (typeof window !== 'undefined') {
document.addEventListener('click', this.clickedOutside)
}
},
methods: {
nodeClick(node, event) {
this.$emit('nodeClick', event, node);
},
nodeDoubleClick(node, event) {
console.log(`nodeDoubleClick ${node.title} ${node.data.type} isLeaf ${node.isLeaf} ${util.inspect(node)}`);
if (!node.isLeaf) {
this.$refs.slvuetree.onToggleHandler(event, node);
return;
}
this.$emit('nodeDoubleClick', node);
},
nodeSelect(node) {
console.log(`nodeSelect ${util.inspect(node)}`);
},
nodeToggle(node) {
console.log(`nodeToggle ${util.inspect(node)}`);
},
nodeDrop(node) {
console.log(`nodeDrop ${util.inspect(node)}`);
this.$emit('nodeDrop', node);
},
nodeContextMenu(node, event) {
console.log(`nodeContextMenu ${util.inspect(node)}`);
this.contextMenuIsVisible = true;
const $contextMenu = this.$refs.contextmenu;
$contextMenu.style.left = event.clientX + 'px';
$contextMenu.style.top = event.clientY + 'px';
},
/**
* Close dropdown if clicked outside.
* Derived from Buefy's b-dropdown
* https://github.com/buefy/buefy/blob/dev/src/components/dropdown/Dropdown.vue
*/
clickedOutside(event) {
if (!this.isInWhiteList(event.target)) this.contextMenuIsVisible = false;
},
// If the "clickOutside" is on a target where we should ignore the click
// then we should ignore this.
// See: https://github.com/buefy/buefy/blob/dev/src/components/dropdown/Dropdown.vue
isInWhiteList(el) { return false; },
onExternalDropHandler(cursorPosition, event) {
console.log('external drop', cursorPosition, util.inspect(event));
},
addPathToTree(fn, stat, isDir) {
console.log(`addPathToTree ${fn} ${util.inspect(stat)} ${isDir}`);
console.log(`addPathToTree ${util.inspect(process)}`);
console.log(util.inspect(path));
fn = path.normalize(fn);
console.log(`addPathToTree NORMALIZED ${fn}`);
const basenm = path.basename(fn);
console.log(`addPathToTree BASENAME ${basenm}`);
const split = splitter(fn);
console.log(`addPathToTree dirs ${util.inspect(split)}`);
let curnodes = this.nodes;
for (let dir of split.dirs) {
if (dir === '.') continue;
let found = undefined;
for (let cur of curnodes) {
if (cur.isLeaf === false && cur.title === dir) {
found = cur;
break;
}
}
if (!found) {
let newnode = {
title: dir,
isLeaf: false,
children: [],
data: {
type: "DIRECTORY",
pathname: fn,
stat
}
};
console.log(`addPathToTree !found push newnode ${util.inspect(newnode)}`);
curnodes.push(newnode);
curnodes = newnode.children;
} else {
curnodes = found.children;
}
}
let newnode = {
title: basenm,
isLeaf: !isDir,
data: {
type: mime.getType(fn),
pathname: fn,
stat
}
};
if (!newnode.data.type) newnode.data.type = "text/plain";
if (newnode.data.type.startsWith('image/')) newnode.data.type = "IMAGE";
if (fn.endsWith('.ejs')) newnode.data.type = "EJS";
if (fn.endsWith('.vue')) newnode.data.type = "VUEJS";
if (!newnode.isLeaf) newnode.children = [];
console.log(`addPathToTree FINAL push newnode ${util.inspect(newnode)}`);
curnodes.push(newnode);
}
}
}
</script>
At the top of this we import various required modules. This includes the sl-vue-tree
component, and some of the Font Awesome icons. Both of those become Vue.js components available to this component.
The sl-vue-tree
component expects data items in a particular data structure. When in Bucharest do as the Romanians, so therefore we must give a correct data structure to that component. An example of the data structure is in a comment shown here.
The addPathToTree
function receives the file
and directory
messages described earlier. It receives a simple pathname string plus a Stat object. The task here is to convert that information into the structure required by sl-vue-tree
.
I ran into a problem developing this component. The requirement to create that directory structure is to split the pathname string into segments. To do this in a operating system independent manner requires careful considerations.
At first the path-directories module (https://github.com/jy95/path-directories
) was used. It takes a pathname and reliably splits it into segments accounting for Windows or POSIX file path conventions. Unfortunately as written it runs on Node.js just fine, but this vue-file-tree
component is packaged assuming it is running in a web browser. Where path-directories assumes a path
module compatible with Node.js, the packaged Vue.js component receives a path
module which presents a subset of Node.js functionality.
Therefore a replacement was coded:
const _path = require('path');
const normalize = require('normalize-path');
const parsePath = require('parse-filepath');
let isWin32 = false;
let isPosix = true;
module.exports = function(path2split) {
path2split = normalize(path2split);
if (path2split.match(/^[a-zA-Z]\:/)) { isWin32 = true; isPosix = false; }
if (path2split.indexOf('\\') >= 0) { isWin32 = true; isPosix = false; }
let path = isWin32 ? _path.win32 : _path.posix;
if (!path) path = _path;
let parsed = path.parse ? path.parse(path2split) : parsePath(path2split);
if (parsed.root === '') parsed.root = '.';
let dir = parsed.dir;
let dirz = [];
do {
dirz.unshift(path.basename(dir));
dir = path.dirname(dir);
} while (dir !== parsed.root);
parsed.dirs = dirz;
return parsed;
}
This module handles pathname normalization and robustly splitting the path irregardless of file system conventions. It returns the object supplied by the path.parse
function, augmented with a dirs
attribute containing the directory portion of the pathname but split into an array.
What addPathToTree
does with this is to add each segment of the file pathname into the data structure required by sl-vue-tree
. Then Vue.js arranges for this structure to be handed to sl-vue-tree
which causes the file system tree to diaplay.
The other methods handle various events going back and forth.
Finally we get to the <style>
section:
<style>
.vue-file-tree-contextmenu {
position: absolute;
background-color: white;
color: black;
border-radius: 2px;
cursor: pointer;
}
.vue-file-tree-contextmenu > div {
padding: 10px;
}
.vue-file-tree-contextmenu > div:hover {
background-color: rgba(100, 100, 255, 0.5);
}
#vue-file-tree {
height: 100%;
}
.sl-vue-tree {
position: relative;
cursor: default;
user-select: none;
}
.sl-vue-tree.sl-vue-tree-root {
border: 1px solid rgb(9, 22, 29);
background-color: rgb(9, 22, 29);
color: rgba(255, 255, 255, 0.5);
border-radius: 3px;
}
.sl-vue-tree-root > .sl-vue-tree-nodes-list {
overflow: hidden;
position: relative;
padding-bottom: 4px;
}
.sl-vue-tree-selected > .sl-vue-tree-node-item {
background-color: #13242d;
color: white;
}
.sl-vue-tree-node-item:hover,
.sl-vue-tree-node-item.sl-vue-tree-cursor-hover {
color: white;
}
.sl-vue-tree-node-item {
position: relative;
display: flex;
flex-direction: row;
padding-left: 10px;
padding-right: 10px;
line-height: 28px;
border: 1px solid transparent;
}
.sl-vue-tree-node-item.sl-vue-tree-cursor-inside {
border: 1px solid rgba(255, 255, 255, 0.5);
}
.sl-vue-tree-gap {
width: 25px;
min-height: 1px;
}
.sl-vue-tree-toggle {
display: inline-block;
text-align: left;
width: 20px;
}
.sl-vue-tree-sidebar {
margin-left: auto;
}
.sl-vue-tree-cursor {
position: absolute;
border: 1px solid rgba(255, 255, 255, 0.5);
height: 1px;
width: 100%;
}
.sl-vue-tree-drag-info {
position: absolute;
background-color: rgba(0,0,0,0.5);
opacity: 0.5;
margin-left: 20px;
padding: 5px 10px;
}
</style>
These CSS declarations handle two tasks: a) the context menu, b) styling of the file tree component
Running the application
We now have everything lined up and ready to go. The electron-vue
framework makes this easy:
$ npm run dev
> electron-vue-file-browse@0.0.0 dev /Volumes/Extra/akasha-tools/electron-vue-file-browse
> node .electron-vue/dev-runner.js
___ __
__ /\_ \ __ ___ /\ \__ _ __ ___ ___ __ __ __ __ __
/ ,.`\ \//\ \ / ,.`\ /'___\ \ \ ,_\ /\` __\ / __`\ /' _ `\ _______ /\ \/\ \/\ \/\ \ / ,.`\
/\ __/ \_\ \_ /\ __/ /\ \__/ \ \ \/ \ \ \/ /\ \_\ \/\ \/\ \ /\______\\ \ \/ |\ \ \_\ \/\ __/
\ \____\ /\____\\ \____\\ \____\ \ \ \_ \ \_\ \ \____/\ \_\ \_\\/______/ \ \___/ \ \____/\ \____\
\/____/ \/____/ \/____/ \/____/ \ \__\ \/_/ \/___/ \/_/\/_/ \/__/ \/___/ \/____/
\/__/
getting ready...
┏ Main Process ---------------
compiling...
┗ ----------------------------
┏ Renderer Process -----------
Hash: b470657ee609c4cee744
Version: webpack 3.12.0
Time: 8084ms
Asset Size Chunks Chunk Names
renderer.js 1.13 MB 0 [emitted] [big] renderer
styles.css 304 kB 0 [emitted] [big] renderer
index.html 508 bytes [emitted]
[0] ./node_modules/vue/dist/vue.esm.js 292 kB {0} [built]
[1] ./node_modules/vue-hot-reload-api/dist/index.js 6.24 kB {0} [built]
[5] ./src/renderer/main.js 623 bytes {0} [built]
[6] external "electron" 42 bytes {0} [not cacheable]
[12] multi ./.electron-vue/dev-client ./src/renderer/main.js 40 bytes {0} [built]
[13] ./.electron-vue/dev-client.js 731 bytes {0} [built]
[14] (webpack)-hot-middleware/client.js?noInfo=true&reload=true 7.77 kB {0} [built]
[16] external "querystring" 42 bytes {0} [not cacheable]
[19] (webpack)-hot-middleware/client-overlay.js 2.21 kB {0} [built]
[25] ./src/renderer/App.vue 1.84 kB {0} [built]
[29] ./src/renderer/router/index.js 273 bytes {0} [built]
[37] external "buefy" 42 bytes {0} [not cacheable]
[38] ./node_modules/buefy/lib/buefy.css 41 bytes {0} [built]
[39] external "vue-electron" 42 bytes {0} [not cacheable]
[41] ./node_modules/style-loader/lib/addStyles.js 8.7 kB [built]
+ 28 hidden modules
Child html-webpack-plugin for "index.html":
Asset Size Chunks Chunk Names
index.html 1.45 MB 0
[0] ./node_modules/html-webpack-plugin/lib/loader.js!./src/index.ejs 1.3 kB {0} [built]
[1] ./node_modules/lodash/lodash.js 540 kB {0} [built]
[2] (webpack)/buildin/module.js 517 bytes {0} [built]
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js!node_modules/buefy/lib/buefy.css:
[0] ./node_modules/css-loader!./node_modules/buefy/lib/buefy.css 318 kB {0} [built]
[1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
┗ ----------------------------
The framework supports hot reload while the app is running in dev mode.
While scanning the directory tree these messages are printed:
┏ Electron -------------------
walkdir sending FILE /Volumes/Extra/akasha-tools/electron-vue-file-browse/src/index.ejs
walkdir sending DIR /Volumes/Extra/akasha-tools/electron-vue-file-browse/src/main
walkdir sending DIR /Volumes/Extra/akasha-tools/electron-vue-file-browse/src/renderer
┗ ----------------------------
┏ Electron -------------------
walkdir sending FILE /Volumes/Extra/akasha-tools/electron-vue-file-browse/src/main/index.dev.js
┗ ----------------------------