Electron Adventures: Episode 21: File Manager
Tags: electron, javascript, svelte
Published: 2021-08-14 00:30:44

After a fun terminal app, let's build a file manager with Electron and Svelte.

We'll start small, by just displaying list of files in the current directory, and letting user navigate to other directories by clicking.

We'll start with Svelte setup from previous episodes. I think I'll have a whole episode about various boilerplate generators, for now you can just copy the files over from episode 13 and clean up anything speific to that one.

### `preload.js`

We need to export two function. First, we need to let the app know where we started, and fortunately that's very easy:

```javascript
let currentDirectory = () => {
  return process.cwd()
}
```

Second we need to return list of files in directory app wants to show. Node has multiple awkward APIs for that, and the least awkward one is one from `fs/promises`.

Unfortunately `Dirent` objects it returns do not survive travel from preload code to the frontend, and we need to turn what it returns into a plain Javascript object. I'm not completely sure why it doesn't work, I'm guessing something to do with how context isolation works.

```javascript
let { readdir } = require("fs/promises")

let directoryContents = async (path) => {
  let results = await readdir(path, {withFileTypes: true})
  return results.map(entry => ({
    name: entry.name,
    type: entry.isDirectory() ? "directory" : "file",
  }))
}
```

Now we just need to tell `preload.js` to expose both:

```javascript
let { contextBridge } = require("electron")

contextBridge.exposeInMainWorld(
  "api", { directoryContents, currentDirectory }
)
```

### `App.js`

And here's the app itself. We're using Svelte's features aggressively here.

```html
<script>
  let directory = window.api.currentDirectory()
  $: filesPromise = window.api.directoryContents(directory)
  $: isRoot = (directory === "/")

  function navigate(path) {
    if (directory === "/") {
      directory = "/" + path
    } else {
      directory += "/" + path
    }
  }
  function navigateUp() {
    directory = directory.split("/").slice(0, -1).join("/") || "/"
  }
</script>

<h1>{directory}</h1>

{#await filesPromise}
{:then files}
  {#if !isRoot}
    <div><button on:click={() => navigateUp()}>..</button></div>
  {/if}
  {#each files as entry}
    {#if entry.type === "directory"}
      <div>
        <button on:click={() => navigate(entry.name)}>{entry.name}</button>
      </div>
    {:else}
      <div>{entry.name}</div>
    {/if}
  {/each}
{/await}

<style>
  :global(body) {
    background-color: #444;
    color: #ccc;
  }
</style>
```

Let's explain some things. There's a bit of path manipulation - there are libraries for it, but I didn't want to do anything complicated, so I'm just assuming we're on a Mac, or a Linux, or such system with `/` separators.
Simply adding `/path` or removing last `/path` changes directory up or down - except we need to treat root directory specially as it's `/` not an empty string.

After that everything else like creating new promise `filesPromise`, resolving it to `files`, and setting `isRoot` is handled by Svelte's reactivity.

If you're coming from React background, it would take a few `useEffect` and [`usePromise`](https://github.com/bsonntag/react-use-promise) calls that we don't need to do, as Svelte figures it all out. Or we could move a lot of that logic to a store or custom hook, or such, but Svelte is expressive enough that a regular component will do just fine, at least for now.

### Results

Here's the result:

![Episode 21 screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5lkkum7fez5ynu22v5x9.png)

I plan to add a lot of feature to this app, but just for fun, for the next episode let's do the same thing in React and see how it compares.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-21-file-manager).