Skip to content

Instantly share code, notes, and snippets.

@bigmistqke
Created March 29, 2025 13:14
Show Gist options
  • Save bigmistqke/563b5c6b870154de56f24443d389029c to your computer and use it in GitHub Desktop.
Save bigmistqke/563b5c6b870154de56f24443d389029c to your computer and use it in GitHub Desktop.
hex editor
import { render } from "solid-js/web";
import { createSignal, Index, For, type JSX } from "solid-js";
import { createStore, type SetStoreFunction } from "solid-js/store";
import "./index.css";
function getNodeAndOffsetAtIndex(element: Node, index: number) {
const nodes = element.childNodes;
let accumulator = 0;
// Determine which node contains the selection-(start|end)
for (const node of nodes) {
const contentLength = node.textContent?.length || 0;
accumulator += contentLength;
if (accumulator >= index) {
const offset = index - (accumulator - contentLength);
if (node instanceof Text) {
return {
node,
offset,
};
}
return getNodeAndOffsetAtIndex(node, offset);
}
}
throw `Could not find node`;
}
type RangeVector = { start: number; end: number };
function getSelection(element: HTMLElement): RangeVector {
const selection = document.getSelection();
if (!selection || selection.rangeCount === 0) {
return { start: 0, end: 0 };
}
const documentRange = selection.getRangeAt(0);
// Create a range that spans from the start of the contenteditable to the selection start
const elementRange = document.createRange();
elementRange.selectNodeContents(element);
elementRange.setEnd(documentRange.startContainer, documentRange.startOffset);
// The length of the elementRange gives the start offset relative to the whole content
const start = elementRange.toString().length;
const end = start + documentRange.toString().length;
return { start, end };
}
function select(
element: HTMLElement,
{ start, end }: { start: number; end?: number },
) {
const selection = document.getSelection()!;
const range = document.createRange();
selection.removeAllRanges();
const resultStart = getNodeAndOffsetAtIndex(element, start);
range.setStart(resultStart.node, resultStart.offset);
if (end) {
const resultEnd = getNodeAndOffsetAtIndex(element, end);
range.setEnd(resultEnd.node, resultEnd.offset);
} else {
range.setEnd(resultStart.node, resultStart.offset);
}
selection.addRange(range);
}
const hexValues = Array.from({ length: 16 }, (_, i) => i.toString(16));
const isHex = (char: string | undefined) => char && hexValues.includes(char);
const isAscii = (char: unknown): char is string => {
if (typeof char !== "string") return false;
const charCode = char.charCodeAt(0);
return charCode > 32 && charCode < 127;
};
function floor(value: number, floor: number) {
return Math.floor(value / floor) * floor;
}
function HexEditor(props: {
array: Array<number>;
setArray(index: number, value: number): void;
}) {
let container: HTMLDivElement;
function scrollToSelection() {
const selection = window.getSelection();
if (!selection?.rangeCount) return; // Exit if no selection
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
if (rect.top < containerRect.top) {
container.scrollTop +=
rect.top - containerRect.top - containerRect.height / 4;
} else if (rect.bottom > containerRect.bottom) {
container.scrollTop +=
rect.bottom - containerRect.bottom + containerRect.height / 4;
}
}
return (
<div
ref={container!}
style={{
height: "100vh",
padding: "10px",
overflow: "auto",
display: "grid",
"grid-template-columns": "auto 1fr auto",
gap: "20px",
"font-family": "monospace",
}}
>
<div
style={{
display: "grid",
"grid-template-rows": `repeat(${props.array.length / 16}, 1fr)`,
}}
>
<Index each={Array.from({ length: props.array.length / 16 })}>
{(_, index) => (
<div style={{ "text-align": "center", padding: "5px" }}>
{(index * 16).toString(16).padStart(8, "0")}
</div>
)}
</Index>
</div>
<div
style={{
display: "grid",
"grid-template-columns": "repeat(16, 1fr)",
"user-select": "none",
}}
contentEditable
onPointerUp={(e) => {
const selection = getSelection(e.currentTarget);
if (floor(selection.start, 2) !== floor(selection.end, 2)) return;
if (e.target instanceof HTMLElement) {
select(e.target, { start: 0, end: 2 });
}
}}
onPaste={(e) => {
console.log(e.clipboardData?.getData("text/plain").split("\n"));
}}
onKeyDown={(e) => {
const selection = getSelection(e.currentTarget);
switch (e.code) {
case "ArrowLeft": {
e.preventDefault();
const start = floor(selection.start, 2) - 2;
if (start > 0) {
select(e.currentTarget, {
start,
end: start + 2,
});
}
break;
}
case "ArrowRight": {
e.preventDefault();
const end = floor(selection.end, 2) + 2;
if (end <= props.array.length * 2) {
select(e.currentTarget, {
start: end - 2,
end,
});
}
break;
}
case "ArrowUp": {
e.preventDefault();
const start = floor(selection.start, 2) - 16 * 2;
if (start > 0) {
select(e.currentTarget, {
start,
end: start + 2,
});
}
break;
}
case "ArrowDown": {
e.preventDefault();
const start = floor(selection.start, 2) + 16 * 2;
if (start <= props.array.length * 2) {
select(e.currentTarget, {
start,
end: start + 2,
});
}
break;
}
}
scrollToSelection();
}}
onBeforeInput={(e) => {
e.preventDefault();
const selection = getSelection(e.currentTarget);
switch (e.inputType) {
case "insertText": {
const data = e.data?.toLowerCase();
if (isHex(data)) {
const offset = selection.start % 2;
const index = Math.floor(selection.start / 2);
const hex = props.array[index].toString(16).padStart(2, "0");
props.setArray(
index,
parseInt(
offset === 0 ? data + hex.slice(1) : hex.slice(0, 1) + data,
16,
),
);
if (floor(selection.start + 1, 2) === selection.start) {
select(e.currentTarget, {
start: selection.start + 1,
end: selection.start + 2,
});
} else {
select(e.currentTarget, {
start: selection.start + 1,
end: selection.start + 3,
});
}
}
break;
}
case "deleteContentBackward": {
const index = Math.floor(selection.start / 2);
props.setArray(index, 0);
select(e.currentTarget, {
start: selection.start,
end: selection.end,
});
}
}
}}
>
<Index each={props.array}>
{(value) => (
<span
style={{
display: "inline-block",
"text-align": "center",
padding: "5px",
}}
>
{value().toString(16).padStart(2, "0").toUpperCase()}
</span>
)}
</Index>
</div>
<div
style={{
display: "grid",
"grid-template-columns": "repeat(16, 1fr)",
}}
contentEditable
onPointerUp={(e) => {
if (e.target instanceof HTMLElement) {
select(e.target, { start: 0, end: 1 });
}
}}
onKeyDown={(e) => {
const selection = getSelection(e.currentTarget);
switch (e.code) {
case "ArrowLeft": {
e.preventDefault();
const start = selection.start - 1;
if (start >= 0) {
select(e.currentTarget, {
start,
end: selection.start,
});
}
break;
}
case "ArrowRight": {
e.preventDefault();
const end = selection.start + 2;
if (end <= props.array.length) {
select(e.currentTarget, {
start: end - 1,
end,
});
}
break;
}
case "ArrowUp": {
e.preventDefault();
const start = selection.start - 16;
if (start >= 0) {
select(e.currentTarget, {
start,
end: start + 1,
});
}
break;
}
case "ArrowDown": {
e.preventDefault();
const start = selection.start + 16;
if (start <= props.array.length) {
select(e.currentTarget, {
start,
end: start + 1,
});
}
break;
}
}
scrollToSelection();
}}
onBeforeInput={(e) => {
e.preventDefault();
const selection = getSelection(e.currentTarget);
switch (e.inputType) {
case "insertText": {
if (isAscii(e.data)) {
const index = Math.floor(selection.start);
props.setArray(index, e.data.charCodeAt(0));
select(e.currentTarget, {
start: selection.start + 1,
end: selection.start + 2,
});
}
break;
}
case "deleteContentBackward": {
const index = Math.floor(selection.start);
props.setArray(index, 0);
select(e.currentTarget, {
start: selection.start,
end: selection.start + 1,
});
}
}
}}
>
<Index each={props.array}>
{(value) => (
<span style={{ "text-align": "center", padding: "0px 1px" }}>
{value() > 32 && value() < 127 ? String.fromCharCode(value()) : "."}
</span>
)}
</Index>
</div>
</div>
);
}
const [array, setArray] = createSignal(
Array.from({ length: 256 * 4 }, (_, i) => i % 255),
{ equals: false },
);
render(
() => (
<HexEditor
array={array()}
setArray={(index, value) =>
setArray((array) => {
array[index] = value;
return array;
})
}
/>
),
document.getElementById("app")!,
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment