DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Electron Adventures: Episode 62: Hex Editor Displaying Data

Hex editor is not a very complicated project, but to keep the posts manageable let's do it one step at a time.

Let's start by displaying data in the MainView.

fast-printf

One function that most languages have but browser-side JavaScript somehow lacks is anything like printf.

In pretty much any other language, to get a 2 digit hex number you can do printf("%02x", i) or something like that.

Fortunately there's a lot of npm packages for it, but many are called "printf" but do not implement even basic printf functionality.

After a few tries with other packages I found that fast-printf does everything I need.

src/App.svelte

To start displaying data, first we need to generate some. And to we can just throw some numbers into an array in a loop. We'll actually want to use Buffer or Uint8Array for this eventually, but one thing at a time.

<script>
  import MainView from "./MainView.svelte"
  import Decodings from "./Decodings.svelte"
  import StatusBar from "./StatusBar.svelte"

  let data = []
  let offset = 1234

  for (let i=0; i<10010; i++) {
    data.push(i & 0xFF)
  }
</script>

<div class="editor">
  <MainView {data} />
  <Decodings {data} {offset} />
  <StatusBar {offset} />
</div>

<svelte:head>
  <title>fancy-data.bin</title>
</svelte:head>
Enter fullscreen mode Exit fullscreen mode

src/StatusBar.svelte

For hex files there's situations where we want to dispaly offset as decimal, and situations where we want to display offset as hex. Since we have a lot of space on the status bar, we can do both.

printf from fast-printf package will handle the formatting.

<script>
  import { printf } from "fast-printf"
  export let offset

  $: hexOffset = printf("%x", offset)
</script>

<div>
  Offset: {offset} ({hexOffset})
</div>

<style>
  div {
    margin-top: 8px;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

src/MainView.svelte

Svelte doesn't have {#while} or {#for} loops, just {#each}, so we need to convert data to slices.

We can put the slicing in $: block so it happens automtically whenever data changes.

<script>
  import Slice from "./Slice.svelte"

  export let data

  let slices

  $: {
    slices = []
    for (let i=0; i<data.length; i+=16) {
      slices.push({
        offset: i,
        data: data.slice(i, i+16),
      })
    }
  }

</script>

<div class="main">
  <table>
    {#each slices as slice}
      <Slice {...slice} />
    {/each}
  </table>
</div>

<style>
  .main {
    flex: 1 1 auto;
    overflow-y: auto;
  }
  table {
    width: 100%;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

src/Slice.svelte

This component represents one row of the main view. It needs to display offset, hex data, and ascii data.

We sometimes want to display decimal and sometimes hex offset, but there's definitely no space for both. It would be nice to have some shortcut to switch between the modes.

<script>
  import { printf } from "fast-printf"
  import HexGroup from "./HexGroup.svelte"
  import AsciiSlice from "./AsciiSlice.svelte"

  export let offset
  export let data
</script>

<tr>
  <td class="offset">{printf("%06d", offset)}</td>
  <HexGroup data={data.slice(0, 4)} />
  <HexGroup data={data.slice(4, 8)} />
  <HexGroup data={data.slice(8, 12)} />
  <HexGroup data={data.slice(12, 16)} />
  <AsciiSlice {data} />
</tr>

<style>
  tr:nth-child(even) {
    background-color: #555;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

src/HexGroup.svelte

For now this component is very simple for now, thanks to printf. We'll need to modify it so it tells us which exact cell is being hovered.

<script>
  import { printf } from "fast-printf"

  export let data
</script>

<td class="hex">
  <span>
    {data[0] !== undefined ? printf("%02x", data[0]) : ""}
  </span>
  <span>
    {data[1] !== undefined ? printf("%02x", data[1]) : ""}
  </span>
  <span>
    {data[2] !== undefined ? printf("%02x", data[2]) : ""}
  </span>
  <span>
    {data[3] !== undefined ? printf("%02x", data[3]) : ""}
  </span>
</td>
Enter fullscreen mode Exit fullscreen mode

src/AsciiSlice.svelte

And finally, the ASCII preview of the data of the slice.

There are three cases here:

  • it's a printable ASCII character - then we print it
  • it's space - then we print it as &nbsp; to keep data aligned
  • it's anything else - then we put a gray dot instead, to keep other data aligned

An obvious question is why don't we print UTF8 characters. This is mainly because that complicate data alignment a lot. And what if start of a character is on one line, but the rest of it is on the next? Or when there are combining characters? Binary data rarely has enough complex UTF8 to justify this. And the table under the code should handle such cases well enough.

<script>
  export let data
</script>

<td class="ascii">
  {#each data as d}
    {#if d >= 33 && d <= 126}
      {String.fromCharCode(d)}
    {:else if d == 32}
      &nbsp;
    {:else}
      <span class="unprintable">.</span>
    {/if}
  {/each}
</td>

<style>
  .unprintable {
    color: #aaa;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

Result

Here's the results:

Episode 62 Screenshot

In the next episode, we'll make data decoding table work.

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

Discussion (0)