DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Electron Adventures: Episode 45: Viewing Files

Every file manager needs a way to view (F3) and edit (F4) files. And it's impossible to support every file type, so for some we handle them internally, and for some we launch external program.

External vs Internal

This means we have the following combinations:

  • view file externally
  • edit file externally
  • view file internally
  • edit file internally

We'll do things a bit backwards, by first implementing external viewing/editing. Then internal viewing. Internal editing is the most complex part, so we could do that either just for some very simple types (like editing where symlink goes), or by embedding some external editor.

With Electron the internal vs external distinction is a bit blurred, as we can launch Electron modal, tab, or window with essentially another app for handling some specific file type.

Editing vs Viewing

Traditional file managers made a distinction between editing and viewing. Many new systems have a single operation of "opening" a file.

There will be situations where we only have a single program for both, or when viewing program can start editing, but this is mostly bad practice. Compare for example viewing a picture in quite preview vs editing it in something like GIMP.

Routing events around

First, there's a lot of event routing. Needing to make changes in so many places suggests that maybe the architecture we picked for event routing, even after so many tries, isn't the best fit for what we're doing. I'm sure we'll revisit this issue later.

We need to add two new entries to src/commands.js:

  {
    name: "View File",
    shortcuts: [{key: "F3"}],
    action: ["activePanel", "viewFocusedFile"],
  },
  {
    name: "Edit File",
    shortcuts: [{key: "F4"}],
    action: ["activePanel", "editFocusedFile"],
  },
Enter fullscreen mode Exit fullscreen mode

src/Footer.svelte

We also need to edit the Footer to support these new commands. Maybe the footer shouldn't know about any of that, and just send F3 to Keyboard component?

Alternatively maybe the Footer should be dynamic based on context, providing what it thinks are the most relevant or most recently used commands, but we don't have enough commands to make it happen. Or maybe we should just drop it, we already have command palette which is generally a lot better.

<script>
  import { getContext } from "svelte"
  let { eventBus } = getContext("app")

  let app = eventBus.target("app")
  let activePanel = eventBus.target("activePanel")
</script>

<footer>
  <button>F1 Help</button>
  <button on:click={() => app.openPalette()}>F2 Menu</button>
  <button on:click={() => activePanel.viewFocusedFile()}>F3 View</button>
  <button on:click={() => activePanel.editFocusedFile()}>F4 Edit</button>
  <button>F5 Copy</button>
  <button>F6 Move</button>
  <button>F7 Mkdir</button>
  <button>F8 Delete</button>
  <button on:click={() => app.quit()}>F10 Quit</button>
</footer>

<svelte:window />

<style>
  footer {
    text-align: center;
    grid-area: footer;
  }

  button {
    font-family: inherit;
    font-size: inherit;
    background-color: #66b;
    color: inherit;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

src/Panel.svelte

In another bit of routing we need the event to hit the active Panel component, only to do a few checks.

We declare a reactive variable focusedPath which gives full path of focused element. It doesn't matter right now, but it's not quite right when you're on .., it will be /some/dir/current/folder/.. instead of /some/dir/current we want. We'd prefer to normalize it.

Then if F3 is pressed, and focused file is a directory (including ..), we enter it. Otherwise we tell the app to view the file, sending its full path.

If F4 is pressed, we ignore it if it's ... Otherwise we tell the app to edit the file, sending its full path.

  $: focusedPath = focused && (directory + "/" + focused.name)

  function viewFocusedFile() {
    if (focused?.type === "directory") {
      activateItem()
    } else {
      app.viewFile(focusedPath)
    }
  }
  function editFocusedFile() {
    if (focused?.name === "..") {
      return
    } else {
      app.editFile(focusedPath)
    }
  }
Enter fullscreen mode Exit fullscreen mode

There's also a small bug I fixed here. .. should not be possible to select.

  let flipSelected = (idx) => {
    if (files[idx].name === "..") {
      return
    }
    if (selected.includes(idx)) {
      selected = selected.filter(f => f !== idx)
    } else {
      selected = [...selected, idx]
    }
  }
Enter fullscreen mode Exit fullscreen mode

src/App.svelte

Now App has a change to launch its internal viewer or editor. As we don't currently have either, we fallback to external without any checks.

  function viewFile(path) {
    window.api.viewFile(path)
  }
  function editFile(path) {
    window.api.editFile(path)
  }
Enter fullscreen mode Exit fullscreen mode

src/preload.js

And finally the preload opens external editor. It should do some file type checks - or App should tell it the file type, for now I'm always using OSX open to open the file, which OSX generally routes to some sensible program, and code to edit the file or directory in VSCode.

let child_process = require("child_process")

let viewFile = (path) => {
  child_process.spawn("open", [path])
}

let editFile = (path) => {
  child_process.spawn("code", [path])
}
Enter fullscreen mode Exit fullscreen mode

Result

Here's the file manager:

Episode 45 Screenshot A

And external process it launched to F4 Edit the focused directory:

Episode 45 Screenshot B

In the next episode we'll be handling viewing some simple files internally.

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

Discussion (0)