DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

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

JavaScript requires nasty code like:

filepath.split("/").slice(-1)[0]
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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

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

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

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:

Episode 48 Screenshot

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.

Discussion (0)