Electron Adventures: Episode 45: Viewing Files
Tags: javascript, electron, svelte
Published: 2021-09-08 21:38:21

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

```javascript
  {
    name: "View File",
    shortcuts: [{key: "F3"}],
    action: ["activePanel", "viewFocusedFile"],
  },
  {
    name: "Edit File",
    shortcuts: [{key: "F4"}],
    action: ["activePanel", "editFocusedFile"],
  },
```

### `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.

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

### `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.

```javascript
  $: 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)
    }
  }
```

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

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

### `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.

```javascript
  function viewFile(path) {
    window.api.viewFile(path)
  }
  function editFile(path) {
    window.api.editFile(path)
  }
```

### `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.

```javascript
let child_process = require("child_process")

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

let editFile = (path) => {
  child_process.spawn("code", [path])
}
```

### Result

Here's the file manager:

![Episode 45 Screenshot A](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/verzsxl9q4fye4sk0dhb.png)

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

![Episode 45 Screenshot B](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0nwjh325dqpm5yoeh29q.png)

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

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