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"],
},
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>
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)
}
}
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]
}
}
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)
}
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])
}
Result
Here's the file manager:
And external process it launched to F4 Edit the focused directory:
In the next episode we'll be handling viewing some simple files internally.
As usual, all the code for the episode is here.
Top comments (0)