Electron Adventures: Episode 48: path-browserify

As I was adding dialogs to the file manager, I noticed that a lot of that new functionality will require path manipulation. And it's already the most messy part of the code.

Path manipulation isn't difficult, so it's tempting to just do some regexp in various places, but it adds up to make code unclear. This is espeecially so since Javascript lacks such simple operations as "get last element of array".

So in Ruby it's possible to do:

filepath.split("/").last

JavaScript requires nasty code like:

filepath.split("/").slice(-1)[0]

Interestingly at least this one is soon coming to Javascript, and it will soon be possible to write code like this:

filepath.split("/").at(-1)

path-browserify

Backend JavaScript has path module which handles common path manipulation, but browser APIs have nothing like it.

Fortunately path is basically a bunch of regular expresions that don't depend on backend functionality in any way.

To get access to it from the browser we just need to install it:

$ npm install path-browserify

For Electron we could also expose it from the preload, but this a very poor practice. If something can be done on purely frontend side just fine, it's better to do it frontend side, as preload is security-sensitive code.

src/Panel.svelte

First we need to import path:

import path from "path-browserify"

The template used to have <header>{directory.split("/").slice(-1)[0]}</header>. We don't want code like that. Instead let's extract that to header

<div class="panel {id}" class:active={active}>
  <header>{header}</header>
  <div class="file-list" bind:this={fileListNode}>
    {#each files as file, idx}
      <File
        panelId={id}
        file={file}
        idx={idx}
        focused={idx === focusedIdx}
        selected={selected.includes(idx)}
        bind:node={fileNodes[idx]}
      />
    {/each}
  </div>
</div>

The header is now defined using path.basename - which replaces former monstrosity. It now also handles / correctly. In previous version, it would result in empty header if we got to /.

  $: header = (directory === "/") ? "/" : path.basename(directory)

We can replace path manipulation in other parts of the code:

  $: focusedPath = focused && path.join(directory, focused.name)

  function activateItem() {
    if (focused?.type === "directory") {
      if (focused.name === "..") {
        initialFocus = path.basename(directory)
      } else {
        initialFocus = null
      }
      directory = path.join(directory, focused.name)
    }
  }

That just leaves two checks we do manually, and honestly they're perfectly readable as is without any helper functions:

  • is it .. - by focused?.name === ".."
  • is it / - by directory === "/"

src/App.svelte

We start by importing path:

  import path from "path-browserify"

There are two places where we use it. First when we start we do this to set the initial directory:

  let initialDirectoryLeft = window.api.currentDirectory()
  let initialDirectoryRight = path.join(window.api.currentDirectory(), "node_modules")

To be honest we should probably save it in local storage or something, but it will do.

And next we cas use path.extname to get extension of the file:

  function viewFile(file) {
    let ext = path.extname(file).toLowerCase()
    if (ext === ".png") {
      preview = {type: "image", file, mimeType: "image/png"}
    } else if (ext === ".jpg" || ext === ".jpeg") {
      preview = {type: "image", file, mimeType: "image/jpeg"}
    } else if (ext === ".gif") {
      preview = {type: "image", file, mimeType: "image/gif"}
    } else if (/\.(css|js|json|md|txt|svelte)$/i.test(ext)) {
      preview = {type: "text", file}
    } else {
      window.api.viewFile(file)
    }
  }

This lets us replace some regexps by ===, but for longer lists, regexp is still much more concise.

And finally we need to replace various variables called path by something else like file, as import path would conflict with it.

This is a problem most other languages don't have - for Ruby uses uppercase names like Pathname or URL for modules, and lowercase names like path or url for local variables. And for that matter makes them into proper objects of appropriate types, so in Ruby version we'd be doing file.extname and directory + "node_modules" not path.extname(file) and path.join(directory, "node_modules"), and it would do the right thing.

These are small issues, but they add up to JavaScript being a poor language. Unfortunately we're pretty much stuck with it for user interfaces for the time being.

Result

Here's the results:

electron-adventures-48-screenshot.png

In the next episode, we'll take another go at adding dialogs to the app.

As usual, all the code for the episode is here.