DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Electron Adventures: Episode 69: Opening Files

In this nice episode, we'll add a file opening dialog to your hex editor.

Architecture issues

This should be very straightforward, but we run into Electron architecture issue. Electron apps have two parts - renderer process and main process.

Conceptually we can think of them as frontend and backend, so displaying open file dialog should obviously be the responsibility of the renderer (frontend) process right?

  • renderer = frontend
  • main = backend

It doesn't quite work like this. What Electron does is really:

  • renderer = things browsers can do
  • main = things browsers cannot do

And as interacting with files is not something browsers let websites do, this actually goes to the main (backend), even though conceptually it's backwards.

Passing data to frontend

And we run into another issue. Electron lacks any simple way to pass data to the frontend, and more specifically to the preload. As our data is fairly simple we'll use query string for it, like we did all the way back in episode 3.

So let's get started!

index.js

let { app, BrowserWindow, dialog } = require("electron")

async function createWindow() {
  let {canceled, filePaths} = await dialog.showOpenDialog({
    properties: ['openFile', 'multiSelections', 'showHiddenFiles']
  })
  if (canceled) {
    app.quit()
  }
  for(let path of filePaths) {
    let qs = new URLSearchParams({ path }).toString();
    let win = new BrowserWindow({
      width: 1024,
      height: 768,
      webPreferences: {
        preload: `${__dirname}/preload.js`,
      },
    })
    win.loadURL(`http://localhost:5000/?${qs}`)
  }
}

app.on("ready", createWindow)

app.on("window-all-closed", () => {
  app.quit()
})
Enter fullscreen mode Exit fullscreen mode

Before we just opened one window. Now we first show dialog. We need to tell it to show hidden files, as we want to open a lot of weird ones (like /bin/bash for the screenshot below) and at least OSX has very aggressive hiding defaults. If dialog was cancelled, then we quit.

If not, we loop through all selected files, and open a browser window for each one, passing it as query string.

preload.js

let fs = require("fs")
let { contextBridge } = require("electron")

let q = new URLSearchParams(window.location.search)

let path = q.get("path")
let data = fs.readFileSync(path)

contextBridge.exposeInMainWorld(
  "api", { path, data }
)
Enter fullscreen mode Exit fullscreen mode

Now the preload gets the path, actually reads the data, and passes both to the frontend.

Technically frontend doesn't need the path, as it has access to the same query parameters, but I want to abstract away this messy data passing a bit.

src/App.svelte

<script>
  import {Buffer} from "buffer/"
  import MainView from "./MainView.svelte"
  import Decodings from "./Decodings.svelte"
  import StatusBar from "./StatusBar.svelte"
  import { tick } from "svelte"

  let data = Buffer.from(window.api.data)
  let offset = 0

  let t0 = performance.now()
  tick().then(() => {
    let t1 = performance.now()
    console.log(`Loaded ${Math.round(data.length / 1024)}kB in ${t1 - t0}ms`)
  })
</script>

<div class="editor">
  <MainView {data} on:changeoffset={e => offset = e.detail}/>
  <Decodings {data} {offset} />
  <StatusBar {offset} />
</div>

<svelte:head>
  <title>{window.api.path.split("/").slice(-1)[0]}</title>
</svelte:head>

<style>
:global(body) {
  background-color: #222;
  color: #fff;
  font-family: monospace;
  padding: 0;
  margin: 0;
}

.editor {
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: auto;
}
.editor > :global(*) {
  background-color: #444;
}
</style>
Enter fullscreen mode Exit fullscreen mode

All the frontend is the same as before except for one line here - setting title to <title>{window.api.path.split("/").slice(-1)[0]}</title>

Results

Here's the results:

Episode 69 Screenshot

That's enough for hex editor. In the next episode, we'll start a new project.

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

Discussion (0)