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()
})
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 }
)
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>
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:
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.
Top comments (0)