// Hex Editor — Display Formatting
// Renders binary data in the classic hex dump format.
///|
/// Configuration for hex dump display formatting.
struct HexViewConfig {
bytes_per_row : Int // bytes per row (default 16)
show_ascii : Bool // show ASCII column
uppercase_hex : Bool // use uppercase hex digits
show_offset : Bool // show offset column
offset_width : Int // width of offset column in hex digits
group_bytes : Int // bytes per group for spacing (0 = no grouping)
}
///|
/// Creates a default HexViewConfig.
pub fn HexViewConfig::default() -> HexViewConfig {
{
bytes_per_row: 16,
show_ascii: true,
uppercase_hex: true,
show_offset: true,
offset_width: 8,
group_bytes: 8,
}
}
///|
/// Creates a HexViewConfig with custom bytes per row.
pub fn HexViewConfig::with_bytes_per_row(
self : HexViewConfig,
bpr : Int,
) -> HexViewConfig {
{ ..self, bytes_per_row: bpr }
}
///|
/// Creates a HexViewConfig with custom show_ascii setting.
pub fn HexViewConfig::with_ascii(
self : HexViewConfig,
show : Bool,
) -> HexViewConfig {
{ ..self, show_ascii: show }
}
///|
/// Creates a HexViewConfig with custom uppercase_hex setting.
pub fn HexViewConfig::with_uppercase(
self : HexViewConfig,
upper : Bool,
) -> HexViewConfig {
{ ..self, uppercase_hex: upper }
}
///|
/// Converts a nibble (4-bit value) to a hex character.
fn nibble_to_char(n : Int, uppercase : Bool) -> Char {
match n {
0 => '0'
1 => '1'
2 => '2'
3 => '3'
4 => '4'
5 => '5'
6 => '6'
7 => '7'
8 => '8'
9 => '9'
10 => if uppercase { 'A' } else { 'a' }
11 => if uppercase { 'B' } else { 'b' }
12 => if uppercase { 'C' } else { 'c' }
13 => if uppercase { 'D' } else { 'd' }
14 => if uppercase { 'E' } else { 'e' }
15 => if uppercase { 'F' } else { 'f' }
_ => '?'
}
}
///|
/// Converts a byte to a 2-character hex string.
pub fn format_byte_hex(byte : Byte, uppercase? : Bool = true) -> String {
let i = byte.to_int()
let hi = nibble_to_char((i >> 4) & 0xF, uppercase)
let lo = nibble_to_char(i & 0xF, uppercase)
"\{hi}\{lo}"
}
///|
/// Formats an integer offset as a hex address string.
pub fn format_offset(
offset : Int,
width? : Int = 8,
uppercase? : Bool = true,
) -> String {
let w = width
// Build hex digits from right to left in an array
let chars = FixedArray::make(w, '0')
let mut n_val = offset
for i = w - 1; i >= 0; i = i - 1 {
let d = n_val & 0xF
n_val = n_val >> 4
chars[i] = nibble_to_char(d, uppercase)
}
// Build result string
let sb = StringBuilder()
for i = 0; i < w; i = i + 1 {
sb.write_char(chars[i])
}
sb.to_string()
}
///|
/// Converts a byte to its ASCII representation for the text column.
/// Printable ASCII characters (0x20-0x7E) are shown as-is.
/// Non-printable bytes are shown as '.'.
pub fn format_ascii(byte : Byte) -> Char {
let i = byte.to_int()
if i >= 0x20 && i <= 0x7E {
match i.to_char() {
Some(ch) => ch
None => '.'
}
} else {
'.'
}
}
///|
/// Formats a single row of a hex dump.
pub fn format_hex_row(
data : FixedArray[Byte],
start : Int,
length : Int,
base_offset : Int,
config : HexViewConfig,
) -> String {
let sb = StringBuilder()
// Offset column
if config.show_offset {
sb.write_string("0x")
sb.write_string(
format_offset(
base_offset,
width=config.offset_width,
uppercase=config.uppercase_hex,
),
)
sb.write_string(" ")
}
// Hex bytes
for i = 0; i < length; i = i + 1 {
if config.group_bytes > 0 && i > 0 && i % config.group_bytes == 0 {
sb.write_char(' ')
}
sb.write_string(
format_byte_hex(data[start + i], uppercase=config.uppercase_hex),
)
sb.write_char(' ')
}
// Pad missing hex positions if row is shorter than bytes_per_row
if length < config.bytes_per_row {
let missing = config.bytes_per_row - length
for j = 0; j < missing; j = j + 1 {
sb.write_string(" ")
}
// Extra space for group boundary in the padding
if config.group_bytes > 0 &&
length <= config.group_bytes &&
config.bytes_per_row > config.group_bytes {
sb.write_char(' ')
}
}
// ASCII column
if config.show_ascii {
sb.write_string(" |")
for i = 0; i < length; i = i + 1 {
sb.write_char(format_ascii(data[start + i]))
}
for j = length; j < config.bytes_per_row; j = j + 1 {
sb.write_char(' ')
}
sb.write_char('|')
}
sb.to_string()
}
///|
/// Formats a complete hex dump from a HexBuffer.
pub fn format_hex_dump(
buffer : HexBuffer,
start_offset? : Int = 0,
length? : Int,
config? : HexViewConfig,
) -> String {
let cfg = match config {
Some(c) => c
None => HexViewConfig::default()
}
let total_len = buffer.length()
let start = start_offset
let max_len = total_len - start
let display_len = match length {
None => max_len
Some(l) => if l < max_len { l } else { max_len }
}
guard display_len > 0 else { return "" }
let data = buffer.to_fixedarray()
let sb = StringBuilder()
let mut offset = 0
for _i = 0; offset < display_len; _i = _i + 1 {
let row_len = if offset + cfg.bytes_per_row <= display_len {
cfg.bytes_per_row
} else {
display_len - offset
}
let row = format_hex_row(data, start + offset, row_len, start + offset, cfg)
sb.write_string(row)
sb.write_char('\n')
offset = offset + cfg.bytes_per_row
}
sb.to_string()
}
///|
/// Returns a brief summary of a HexBuffer.
pub fn format_buffer_info(buffer : HexBuffer) -> String {
let len = buffer.length()
let size_str = format_size(len)
let file_str = match buffer.get_file_path() {
None => ""
Some(p) => p
}
let modified_str = if buffer.is_modified() { " [modified]" } else { "" }
"File: \{file_str}, Size: \{len} bytes (\{size_str})\{modified_str}"
}
///|
/// Formats a byte count in human-readable form.
pub fn format_size(len : Int) -> String {
if len < 1024 {
return "\{len} B"
}
if len < 1024 * 1024 {
let kb = len.to_double() / 1024.0
return "\{format_double(kb, 1)} KB"
}
if len < 1024 * 1024 * 1024 {
let mb = len.to_double() / (1024.0 * 1024.0)
return "\{format_double(mb, 1)} MB"
}
let gb = len.to_double() / (1024.0 * 1024.0 * 1024.0)
"\{format_double(gb, 1)} GB"
}
///|
/// Formats a Double to a fixed number of decimal places.
fn format_double(value : Double, places : Int) -> String {
let multiplier = ipow10(places).to_double()
let scaled = value * multiplier
let rounded = scaled.round().to_int()
let int_part = rounded / ipow10(places)
let frac_part = rounded % ipow10(places)
if places <= 0 {
return "\{int_part}"
}
let sb = StringBuilder()
sb.write_string("\{int_part}.")
// Format fractional part with leading zeros
let mut remaining = frac_part
let mut divisor = ipow10(places - 1)
for _i = 0; _i < places; _i = _i + 1 {
let digit = remaining / divisor
sb.write_char(nibble_to_char(digit, false))
remaining = remaining % divisor
divisor = divisor / 10
if divisor < 1 {
break
}
}
// Handle the case where divisor hits 0
sb.to_string()
}
///|
/// Computes 10^n for non-negative n.
fn ipow10(n : Int) -> Int {
let mut result = 1
for _i = 0; _i < n; _i = _i + 1 {
result = result * 10
}
result
}
///|
/// Converts a number to a fixed-width hex string (useful for UI).
pub fn to_hex_string(n : Int, width? : Int = 2) -> String {
format_offset(n, width~)
}