Skip to content

Instantly share code, notes, and snippets.

@cessor
Created September 6, 2021 15:19
Show Gist options
  • Save cessor/7cb2c1f75bfe994751396ab58df0ed9f to your computer and use it in GitHub Desktop.
Save cessor/7cb2c1f75bfe994751396ab58df0ed9f to your computer and use it in GitHub Desktop.
hexview.py
import argparse
import curses
import string
import sys
from curses import wrapper
from curses.textpad import rectangle
from pathlib import Path
class HexDisplay:
def __init__(self, content: bytes, offset=True, chars=True):
self._content = content
self._offset = offset
self._chars = chars
def __iter__(self):
offset_display = ''
char_display = ''
# Split byte content in 16 byte rows
for i in range(0, len(self._content), 16):
row = self._content[i:i + 16]
# Optional: Display a row offset
if self._offset:
offset_display = f'{i:08X} '
# Render each byte in its hexadecimal representation
bytes_display = ' '.join([f'{byte:02X}' for byte in row])
# The last row might be shorter, so add padding
padding = ' '.join([' '] * (16 - len(row)))
# Optional: Append ascii represenation of each byte
if self._chars:
char_display = ' ' + ''.join([
# ASCII if byte is printable, else "."
chr(byte) if 0x20 <= byte <= 0x7f else '.' for byte in row
])
yield f'{offset_display}{bytes_display}{padding}{char_display}'
class Dialog:
HEIGHT = 5
WIDTH = 24
def __init__(self, title, dialog, textbox, y, x):
self._title = title
self._dialog = dialog
self._textbox = textbox
self._y = y
self._x = x
def _display(self, buffer_):
height, width = self.HEIGHT, self.WIDTH
title = self._title
self._dialog.clear()
# Outer Border
rectangle(self._dialog, 0, 0, height-1, width-2)
# Inner Border
rectangle(self._dialog, 1, 2, height-2, width-4)
# Title line:
center = (width // 2) - (len(title) // 2)
self._dialog.addstr(0, center, title)
self._dialog.refresh()
# Shift cursor to imitate typing
self._dialog.move(2, 4 + len(buffer_))
# Text in textbox
self._textbox.clear()
self._textbox.addstr(0, 0, ''.join(buffer_))
self._textbox.refresh(0, 0,
self._y + 2, self._x + 4,
self._y + 2 + 1, self._x + 4 + 15)
def prompt(self):
"""Promt a hexadecimal number"""
buffer_ = []
while True:
self._display(buffer_)
key = self._dialog.getch()
if chr(key) in string.hexdigits:
# Accept hex values
buffer_.append(chr(key))
continue
if key in (8, curses.KEY_BACKSPACE, ):
# Remove last item
if buffer_:
buffer_.pop()
continue
if key in (10, curses.KEY_ENTER):
# Calculate line number for byte offset
return int(''.join(buffer_), 16) // 16
@classmethod
def load(cls, screen, pad, title):
height, width = cls.HEIGHT, cls.WIDTH
# Get height from screen, because pad might be waaayy longer
max_height, _ = screen.getmaxyx()
_, max_width = pad.getmaxyx()
# Center on Screen
dialog_y = (max_height // 2) - (height // 2)
dialog_x = (max_width // 2) - (width // 2)
# height, width, x, y,
dialog = curses.newwin(height, width, dialog_y, dialog_x)
# Textbox
textbox = curses.newpad(1, 16)
return cls(title, dialog, textbox, dialog_y, dialog_x)
class Window:
def __init__(self, screen, pad, dialog):
self._screen = screen
self._pad = pad
self._dialog = dialog
self._viewport_height, self._viewport_width = self._screen.getmaxyx()
self._page_height, self._page_width = self._pad.getmaxyx()
self._row_offset = 0
def show(self):
self._screen.clear()
self._screen.refresh()
while True:
self._display()
# Read Input
key = self._screen.getch()
# Handle Exit
if key in (27, ord('q')): # ESC or Q
exit(0)
# Handle jump dialog
if key in (ord('g'), ord('G')):
line_number = self._show_jump_dialog()
if line_number > (self._page_height - self._viewport_height):
# Clamp to end
self._row_offset = self._page_height - self._viewport_height
else:
self._row_offset = line_number
continue
# Handle input to scroll display
self._row_offset = self._scroll_display(key, self._row_offset)
def _show_jump_dialog(self):
curses.curs_set(1)
try:
line_number = self._dialog.prompt()
except ValueError:
return self._row_offset
curses.curs_set(0)
return line_number
def _display(self):
# Content offset (y,x)
# Viewport ((y,x) top left, (y,x) bottom right
self._pad.refresh(
self._row_offset, 0,
0, 0,
self._viewport_height - 1, self._viewport_width - 1
)
self._screen.refresh()
def _scroll_display(self, key, row_offset):
# Home
if key == curses.KEY_HOME:
return 0
# Scroll Up, (Cursor or Keypad)
if key in (curses.KEY_UP, 259, 56):
return max(row_offset - 1, 0)
# Page Up
if key in (curses.KEY_PPAGE,):
return max(row_offset - self._viewport_height, 0)
# Page Down
if key in (curses.KEY_NPAGE,):
if self._viewport_height >= self._page_height:
return row_offset
return min(row_offset + self._viewport_height, (self._page_height - self._viewport_height))
# Scroll Down (Cursor or Keypad)
if key in (curses.KEY_DOWN, 258, 50):
if self._viewport_height >= self._page_height:
return row_offset
return min(row_offset + 1, (self._page_height - self._viewport_height))
# End
if key == curses.KEY_END:
if self._viewport_height >= self._page_height:
return row_offset
return self._page_height - self._viewport_height
return row_offset
@classmethod
def load(cls, screen, hex_display):
lines = list(hex_display)
rows = len(lines)
cols = len(lines[0])
# Setup hexcode display
pad = curses.newpad(rows, cols)
for line_number, line in enumerate(lines):
pad.addstr(line_number, 0, line)
# Setup Go To Line-Dialog
dialog = Dialog.load(screen, pad, title="Goto Byte (hex)")
return Window(screen, pad, dialog)
def main(screen):
parser = argparse.ArgumentParser(
description='Display a binary file.'
)
parser.add_argument(
'file',
type=str,
help='A binary file to display'
)
parser.add_argument(
'-no',
'--no-offset',
action='store_false',
default=True, # Default to displaying
help='Deactivate byte offset display',
)
parser.add_argument(
'-na',
'--no-ascii',
action='store_false',
default=True, # Default to displaying
help='Deactivate ascii display',
)
arguments = parser.parse_args()
content = Path(arguments.file).read_bytes()
hex_display = HexDisplay(
content=content,
offset=arguments.no_offset,
chars=arguments.no_ascii
)
window = Window.load(screen, hex_display)
window.show()
if __name__ == '__main__':
wrapper(main)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment