DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on • Updated on

Electron Adventures: Episode 21: File Manager

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:

let currentDirectory = () => {
  return process.cwd()
}
Enter fullscreen mode Exit fullscreen mode

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.

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",
  }))
}
Enter fullscreen mode Exit fullscreen mode

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

let { contextBridge } = require("electron")

contextBridge.exposeInMainWorld(
  "api", { directoryContents, currentDirectory }
)
Enter fullscreen mode Exit fullscreen mode

App.js

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

<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>
Enter fullscreen mode Exit fullscreen mode

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 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

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.

Discussion (0)