ミラー

This commit is contained in:
2025-12-28 05:21:34 +09:00
commit 4442ef77d3
10 changed files with 1132 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.svn
hexagon

11
CHANGELOG.md Normal file
View File

@@ -0,0 +1,11 @@
## 1.1.0 (2025年12月28日)
* PgUp・PgDownで使ってページを動ける様に
* ^、0、又はHOME、及び$又はENDキーで使って同じ行列で最初や最後のHEXや文字に移動ける様に
* CTRL + Cを無効に
* OpenBSDでのコンパイラ報告の修正
* 逆検索でNとnキーの修正
* トップバーの追加
* ファイルヘッダーを青色で表示する
## 1.0.0 (2025年04月20日)
* 最初リリース

32
LICENSE.md Normal file
View File

@@ -0,0 +1,32 @@
Copyright (c) 2025 テクニカル諏訪子
Permission is hereby granted to any person obtaining a copy of the software
Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
sublicense, and/or sell copies of the Software, subject to the following conditions:
1. **Origin Attribution**:
- You must not misrepresent the origin of the Software; you must not claim
you created the original Software.
- If the Software is used in a product, you must either:
a. Provide clear attribution in the product's documentation, user interface,
or other visible areas, **OR**
b. Pay the original developers a fee they specify in writing.
2. **Usage Restriction**:
- The Software, or any derivative works, dependencies, or libraries
incorporating it, must not be used for censorship or to suppress freedom of
speech, expression, or creativity. Prohibited uses include, but are not
limited to:
- Censorship of so-called "hate speech", visuals, non-mainstream opinions,
ideas, or objective reality.
- Tools or systems designed to restrict access to information or
artistic works.
3. **Notice Preservation**:
- This license and the above copyright notice must remain intact in all copies
of the source code.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

82
Makefile Normal file
View File

@@ -0,0 +1,82 @@
UNAME_M != uname -m
UNAME_S != uname -s
OS = $(UNAME_S)
ARCH = ${UNAME_M}
.if ${UNAME_S} == "OpenBSD"
OS = openbsd
.elif ${UNAME_S} == "NetBSD"
OS = netbsd
.elif ${UNAME_S} == "FreeBSD"
OS = freebsd
.elif ${UNAME_S} == "Dragonfly"
OS = dragonfly
.elif ${UNAME_S} == "Linux"
OS = linux
.elif ${UNAME_S} == "Haiku"
OS = haiku
.elif ${UNAME_S} == "Darwin"
OS = macos
.endif
.if ${UNAME_M} == "x86_64"
ARCH = amd64
.elif ${UNAME_M} == "macppc"
ARCH = powerpc
.endif
NAME = hexagon
VERSION = 1.0.0
PREFIX = /usr/local
.if ${OS} == "linux"
PREFIX = /usr
.elif ${OS} == "haiku"
PREFIX = /boot/home/config/non-packaged
.endif
CFLAGS = -I/usr/include -I/usr/local/include -I/usr/pkg/include\
-I/usr/pkg/include/ncurses -I/boot/system/develop/headers\
-L/usr/lib -L/usr/local/lib -L/usr/pkg/lib -L/boot/system/develop/lib
.if ${UNAME_S} == "NetBSD"
LDFLAGS = -lncurses
.else
LDFLAGS = -lncursesw
.endif
CC = c++
FILES = main.cc src/*.cc
all:
${CC} -O3 ${CFLAGS} -o ${NAME} ${FILES} -std=c++17 -static ${LDFLAGS}
strip ${NAME}
debug:
${CC} -g ${CFLAGS} -o ${NAME} ${FILES} -std=c++17 ${LDFLAGS}
install:
cp -rf ${NAME} ${PREFIX}/bin
uninstall:
rm -rf ${PREFIX}/bin/${NAME}
clean:
rm -rf ${NAME} *.core
dist:
mkdir -p ${NAME}-${VERSION} release/src
cp -R LICENSE.md Makefile CHANGELOG.md README.md main.cc src make-mingw.sh\
${NAME}-${VERSION}
tar zcfv release/src/${NAME}-${VERSION}.tar.gz ${NAME}-${VERSION}
rm -rf ${NAME}-${VERSION}
release:
mkdir -p release/bin/${VERSION}/${OS}/${ARCH}
cp -rf ${NAME} release/bin/${VERSION}/${OS}/${ARCH}
publish:
rsync -rtvzP release/bin/${VERSION} 192.168.0.143:/zroot/repo/bin/${NAME}
rsync -rtvzP release/src/* 192.168.0.143:/zroot/repo/src/${NAME}
.PHONY: all debug install uninstall clean dist release publish

29
README.md Normal file
View File

@@ -0,0 +1,29 @@
# Hexagon
VIMキーバインドでのHEXエディター
## インストールする方法 | Installation
### BSD
```sh
make
doas make install
```
### Linux/macOS
```sh
bmake
sudo bmake install
```
### Haiku
```sh
bmake
bmake install
```
### Windows
```sh
chmod +x make-mingw.sh
./make-mingw.sh
```
![](スクショ.png)

20
main.cc Normal file
View File

@@ -0,0 +1,20 @@
#include <iostream>
#include "src/hexeditor.hh"
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cerr << "usage: hexagon <filename>\n";
return 1;
}
try {
HexEditor editor(argv[1]);
editor.run();
} catch (const std::exception &e) {
std::cerr << "エラー: " << e.what() << "\n";
return 1;
}
return 0;
}

5
make-mingw.sh Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
g++ -O3 -o hexagon.exe -I/mingw64/include -L/mingw64/lib main.cc src/*.cc\
-std=c++17 -static -lpdcurses -lgdi32 -lcomdlg32 -lwinmm -luser32 -lwinpthread\
-lstdc++ -mwindows

868
src/hexeditor.cc Normal file
View File

@@ -0,0 +1,868 @@
#include <algorithm>
#include <csignal>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <set>
#include <sstream>
#include <locale.h>
#include <wchar.h>
#ifdef _WIN32
#include <windows.h>
#endif
#include "hexeditor.hh"
#ifdef _WIN32
static BOOL WINAPI ConsoleCtrlHandler(DWORD dwCtrlType) {
if (dwCtrlType == CTRL_C_EVENT) return TRUE;
return FALSE;
}
#endif
const std::vector<HexEditor::FileSignature> HexEditor::signatures = {
{{ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, "PNG"},
{{ 0xFF, 0xD8 }, "JPEG"},
{{ 0x7F, 0x45, 0x4C, 0x46 }, "ELF"},
{{ 0x4D, 0x5A }, "PE"},
{{ 0x25, 0x50, 0x44, 0x46 }, "PDF"},
{{ 0x67, 0x69, 0x6D, 0x70, 0x20, 0x78, 0x63, 0x66 }, "GIMP XCF"},
{{ 0x1A, 0x45, 0xDF, 0xA3, 0x9F, 0x42, 0x86, 0x81, 0x01, 0x42,
0xF7, 0x81, 0x01, 0x42, 0xF2, 0x81, 0x04, 0x42, 0xF3, 0x81,
0x08, 0x42, 0x82, 0x84, 0x77, 0x65, 0x62, 0x6D, 0x42, 0x87,
0x81, 0x04, 0x42, 0x85, 0x81, 0x02, 0x18, 0x53, 0x80, 0x67,
0x01, 0x00, 0x00, 0x00, 0x01 }, "WEBM"},
{{ 0x1A, 0x45, 0xDF, 0xA3, 0xA3, 0x42, 0x86, 0x81, 0x01, 0x42,
0xF7, 0x81, 0x01, 0x42, 0xF2, 0x81, 0x04, 0x42, 0xF3, 0x81,
0x08, 0x42, 0x82, 0x88, 0x6D, 0x61, 0x74, 0x72, 0x6F, 0x73,
0x6B, 0x61, 0x42, 0x87, 0x81, 0x04, 0x42, 0x85, 0x81, 0x02,
0x18, 0x53, 0x80, 0x67, 0x01, 0x00, 0x00, 0x00}, "Matroska"},
{{ 0x52, 0x49, 0x46, 0x46 }, "Waveform"},
};
HexEditor::HexEditor(const std::string &filename)
: curPos(0), dpOffset(0), bpr(16),
statusMode(Status_Normal), modified(false),
running(true),
lastSearchDir(Direction_Forward),
headerLen(0), headerType("") {
// SIGINT (CTRL + C)を無効に
#ifdef _WIN32
SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE);
#else
std::signal(SIGINT, SIG_IGN);
#endif
setlocale(LC_ALL, "");
// ncurses
initscr();
clearok(stdscr, TRUE);
cbreak();
noecho();
keypad(stdscr, TRUE);
raw();
start_color();
init_pair(1, COLOR_BLACK, COLOR_WHITE); // カーソル
init_pair(2, COLOR_BLACK, COLOR_GREEN); // 変更
init_pair(3, COLOR_BLACK, COLOR_MAGENTA); // 普通
init_pair(4, COLOR_BLACK, COLOR_CYAN); // コマンド
init_pair(5, COLOR_BLACK, COLOR_YELLOW); // 検索
init_pair(6, COLOR_BLACK, COLOR_YELLOW); // 検索ハイライト
init_pair(7, COLOR_BLACK, COLOR_RED); // エラー
init_pair(8, COLOR_WHITE, COLOR_BLACK); // デフォルト
init_pair(9, COLOR_CYAN, COLOR_BLACK); // ファイルヘッダー
refresh();
// ファイルをバッファーに読み込む
std::ifstream file(filename, std::ios::binary);
if (!file) {
endwin();
throw std::runtime_error("ファイルを開くに失敗");
}
buf.assign((std::istreambuf_iterator<char>(file)), {});
file.close();
if (buf.empty()) {
endwin();
throw std::runtime_error("ファイルが空です");
}
fname = filename;
// ファイルヘッダー
for (const auto &sig : signatures) {
if (buf.size() >= sig.signature.size()) {
bool match = true;
for (size_t i = 0; i < sig.signature.size(); ++i) {
if (buf[i] != sig.signature[i]) {
match = false;
break;
}
}
if (match) {
headerLen = sig.signature.size();
headerType = sig.type;
break;
}
}
}
// ウィンドウを作成する
getmaxyx(stdscr, rows, cols);
if (rows < 4 || cols < 20) {
endwin();
throw std::runtime_error("ターミナルが小さ過ぎます");
}
bpr = std::min<size_t>(16, (cols / 2 - 10) / 4);
hexPanel = newwin(rows - 3, cols / 2, 1, 0);
asciiPanel = newwin(rows - 3, cols / 2, 1, cols / 2);
scrollok(hexPanel, TRUE);
scrollok(asciiPanel, TRUE);
wattrset(hexPanel, COLOR_PAIR(8));
wattrset(asciiPanel, COLOR_PAIR(8));
}
HexEditor::~HexEditor() {
delwin(hexPanel);
delwin(asciiPanel);
endwin();
}
void HexEditor::highlightcol(size_t i, size_t row, uint8_t byte) {
wattron(hexPanel, COLOR_PAIR(1));
mvwprintw(hexPanel, row, 10 + i * 3, "%02x", byte);
wattroff(hexPanel, COLOR_PAIR(1));
wattron(asciiPanel, COLOR_PAIR(1));
mvwaddch(asciiPanel, row, i, std::isprint(byte) ? byte : '.');
wattroff(asciiPanel, COLOR_PAIR(1));
}
void HexEditor::topbar() {
int colorPair = 3;
std::string status = "Hexagon 1.1.0";
if (!headerType.empty()) status += " | FILETYPE: " + headerType;
std::wstring wstatus(status.begin(), status.end());
wattron(stdscr, COLOR_PAIR(colorPair));
mvprintw(0, 0, "%s", status.c_str());
for (int i = status.length(); i < cols; ++i) {
mvaddch(0, i, ' ');
}
wattroff(stdscr, COLOR_PAIR(colorPair));
}
void HexEditor::statusbar() {
int colorPair;
switch (statusMode) {
case Status_Replace: colorPair = 2; break;
case Status_Normal: colorPair = 3; break;
case Status_Command: colorPair = 4; break;
case Status_Search: colorPair = 5; break;
case Status_Error: colorPair = 7; break;
}
std::string status;
if (statusMode == Status_Normal || statusMode == Status_Error) {
status = "FILE: " + fname;
status += " | OFFSET: 0x" + std::to_string(curPos);
status += " | FILE SIZE: " + std::to_string(buf.size()) + " B";
if (!lastSearch.empty()) {
status += " | SEARCH: " + lastSearch;
} else if (!lastHexSearch.empty()) {
std::ostringstream hexSearch;
for (size_t i = 0; i < lastHexSearch.size(); ++i) {
if (i > 0) hexSearch << " ";
hexSearch << std::hex << std::setfill('0') << std::setw(2)
<< (int)lastHexSearch[i];
}
status += " | SEARCH: " + hexSearch.str();
}
status += (modified ? " [+]" : "");
if (statusMode == Status_Error) status += " | ERROR: " + statusText;
else status += !statusText.empty() ? " | " + statusText : "";
} else if (statusMode == Status_Command) {
status = ":" + statusText;
} else if (statusMode == Status_Search) {
status = (lastSearchDir == Direction_Forward ? "/" : "?") + statusText;
} else if (statusMode == Status_Replace) {
status = "-- REPLACE --";
}
std::wstring wstatus(status.begin(), status.end());
wattron(stdscr, COLOR_PAIR(colorPair));
mvprintw(rows - 1, 0, "%s", status.c_str());
for (int i = status.length(); i < cols; ++i) {
mvaddch(rows - 1, i, ' ');
}
wattroff(stdscr, COLOR_PAIR(colorPair));
}
void HexEditor::render() {
werase(hexPanel);
werase(asciiPanel);
getmaxyx(stdscr, rows, cols);
size_t maxRows = rows - 3;
topbar();
std::set<size_t> matchBytes;
if (!lastSearch.empty()) {
for (size_t i = 0; i <= buf.size() - lastSearch.size(); ++i) {
bool match = true;
for (size_t j = 0; j < lastSearch.size(); ++j) {
if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
match = false;
break;
}
}
if (match) {
for (size_t j = 0; j < lastSearch.size(); ++j) {
matchBytes.insert(i + j);
}
}
}
} else if (!lastHexSearch.empty()) {
for (size_t i = 0; i <= buf.size() - lastHexSearch.size(); ++i) {
bool match = true;
for (size_t j = 0; j < lastHexSearch.size(); ++j) {
if (buf[i + j] != lastHexSearch[j]) {
match = false;
break;
}
}
if (match) {
for (size_t j = 0; j < lastHexSearch.size(); ++j) {
matchBytes.insert(i + j);
}
}
}
}
// HEXとASCIIの表示
for (size_t row = 0; row < maxRows; ++row) {
size_t offset = dpOffset + row * bpr;
if (offset >= buf.size()) break;
std::ostringstream hexLine, asciiLine;
hexLine << std::hex << std::setfill('0') << std::setw(8) << offset << ": ";
mvwprintw(hexPanel, row, 0, "%s", hexLine.str().c_str());
// HEXとASCIIのデータ
for (size_t i = 0; i < bpr && (offset + i) < buf.size(); ++i) {
uint8_t byte = buf[offset + i];
bool isMatch = matchBytes.count(offset + i) > 0;
bool isHeader = (offset + i) < headerLen;
if (offset + i == curPos) {
highlightcol(i, row, byte);
} else if (isHeader) {
wattron(hexPanel, COLOR_PAIR(9));
mvwprintw(hexPanel, row, 10 + i * 3, "%02x", byte);
wattroff(hexPanel, COLOR_PAIR(9));
wattron(asciiPanel, COLOR_PAIR(9));
mvwaddch(asciiPanel, row, i, std::isprint(byte) ? byte : '.');
wattroff(asciiPanel, COLOR_PAIR(9));
} else if (isMatch) {
wattron(hexPanel, COLOR_PAIR(6));
mvwprintw(hexPanel, row, 10 + i * 3, "%02x", byte);
wattroff(hexPanel, COLOR_PAIR(6));
wattron(asciiPanel, COLOR_PAIR(6));
mvwaddch(asciiPanel, row, i, std::isprint(byte) ? byte : '.');
wattroff(asciiPanel, COLOR_PAIR(6));
} else {
wattron(hexPanel, COLOR_PAIR(8));
mvwprintw(hexPanel, row, 10 + i * 3, "%02x", byte);
wattroff(hexPanel, COLOR_PAIR(8));
wattron(asciiPanel, COLOR_PAIR(8));
mvwaddch(asciiPanel, row, i, std::isprint(byte) ? byte : '.');
wattroff(asciiPanel, COLOR_PAIR(8));
}
}
}
statusbar();
redrawwin(hexPanel);
redrawwin(asciiPanel);
wrefresh(hexPanel);
wrefresh(asciiPanel);
refresh();
}
void HexEditor::findNextMatch() {
if (lastSearch.empty() && lastHexSearch.empty()) return;
size_t searchSize = lastSearch.empty() ? lastHexSearch.size() : lastSearch.size();
size_t startPos = curPos + 1;
for (size_t i = startPos; i <= buf.size() - searchSize; ++i) {
bool match = true;
if (!lastSearch.empty()) {
for (size_t j = 0; j < lastSearch.size(); ++j) {
if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
match = false;
break;
}
}
} else {
for (size_t j = 0; j < lastHexSearch.size(); ++j) {
if (buf[i + j] != lastHexSearch[j]) {
match = false;
break;
}
}
}
if (match) {
curPos = i;
dpOffset = curPos - (curPos % bpr);
render();
return;
}
}
for (size_t i = 0; i < startPos && i <= buf.size() - searchSize; ++i) {
bool match = true;
if (!lastSearch.empty()) {
for (size_t j = 0; j < lastSearch.size(); ++j) {
if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
match = false;
break;
}
}
} else {
for (size_t j = 0; j < lastHexSearch.size(); ++j) {
if (buf[i + j] != lastHexSearch[j]) {
match = false;
break;
}
}
}
if (match) {
curPos = i;
dpOffset = curPos - (curPos % bpr);
render();
return;
}
}
}
void HexEditor::findPrevMatch() {
if (lastSearch.empty() && lastHexSearch.empty()) return;
size_t searchSize = lastSearch.empty() ? lastHexSearch.size() : lastSearch.size();
size_t startPos = curPos > 0 ? curPos - 1 : buf.size() - searchSize;
for (size_t i = startPos + 1; i > 0; --i) {
size_t pos = i - 1;
if (pos > buf.size() - searchSize) continue;
bool match = true;
if (!lastSearch.empty()) {
for (size_t j = 0; j < lastSearch.size(); ++j) {
if (buf[pos + j] != static_cast<uint8_t>(lastSearch[j])) {
match = false;
break;
}
}
} else {
for (size_t j = 0; j < lastHexSearch.size(); ++j) {
if (buf[pos + j] != lastHexSearch[j]) {
match = false;
break;
}
}
}
if (match) {
curPos = pos;
dpOffset = curPos - (curPos % bpr);
render();
return;
}
}
for (size_t i = buf.size() - searchSize; i > startPos; --i) {
bool match = true;
if (!lastSearch.empty()) {
for (size_t j = 0; j < lastSearch.size(); ++j) {
if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
match = false;
break;
}
}
} else {
for (size_t j = 0; j < lastHexSearch.size(); ++j) {
if (buf[i + j] != lastHexSearch[j]) {
match = false;
break;
}
}
}
if (match) {
curPos = i;
dpOffset = curPos - (curPos % bpr);
render();
return;
}
}
}
void HexEditor::handleCommand() {
statusMode = Status_Command;
statusText.clear();
render();
int ch;
while ((ch = getch()) != '\n' && ch != 27) {
#ifdef _WIN32
if (ch == KEY_BACKSPACE || ch == 8) {
#else
if (ch == KEY_BACKSPACE || ch == 127) {
#endif
if (!statusText.empty()) {
statusText.pop_back();
}
} else if (ch >= 32 && ch <= 126) { // 書き込める文字
statusText += ch;
}
render();
}
if (ch == 27) { // Esc
statusMode = Status_Normal;
statusText.clear();
render();
return;
}
bool isCommand = false;
if (statusText == "w") {
handleSave();
isCommand = true;
} else if (statusText == "q") {
handleQuit(false);
isCommand = true;
} else if (statusText == "wq") {
handleSave();
handleQuit(false);
isCommand = true;
} else if (statusText == "q!") {
handleQuit(true);
isCommand = true;
} else if (statusText == "wq!") {
handleSave();
handleQuit(true);
isCommand = true;
} else if (statusText == "noh") {
lastSearch = "";
isCommand = true;
} else {
statusMode = Status_Error;
statusText.clear();
}
if (isCommand) {
statusMode = Status_Normal;
statusText.clear();
}
render();
}
void HexEditor::handleQuit(bool force) {
if (force || (!force && !modified)) {
running = false;
} else {
statusMode = Status_Error;
statusText.clear();
render();
}
}
void HexEditor::handleSave() {
std::ofstream file(fname, std::ios::binary);
if (file) {
file.write(reinterpret_cast<const char *>(buf.data()), buf.size());
file.close();
modified = false;
statusText = "保存済み";
}
}
void HexEditor::handleSearch() {
statusMode = Status_Search;
lastSearchDir = Direction_Forward;
statusText.clear();
render();
int ch;
while ((ch = getch()) != '\n' && ch != 27) {
#ifdef _WIN32
if (ch == KEY_BACKSPACE || ch == 8) {
#else
if (ch == KEY_BACKSPACE || ch == 127) {
#endif
if (!statusText.empty()) statusText.pop_back();
} else if (ch >= 32 && ch <= 126) { // 書き込める文字
statusText += ch;
}
render();
}
if (ch == 27) { // Esc
statusMode = Status_Normal;
statusText.clear();
render();
return;
}
lastSearch.clear();
lastHexSearch.clear();
matchOff.clear();
std::istringstream iss(statusText);
std::string hexByte;
bool isHexSearch = true;
std::vector<uint8_t> hexSearch;
while (iss >> hexByte) {
if (hexByte.size() != 2 || !std::all_of(hexByte.begin(), hexByte.end(), [](char c) { return std::isxdigit(c); })) {
isHexSearch = false;
break;
}
try {
hexSearch.push_back(static_cast<uint8_t>(std::stoul(hexByte, nullptr, 16)));
} catch (...) {
isHexSearch = false;
break;
}
}
if (isHexSearch && !hexSearch.empty()) { // HEX検索
lastHexSearch = hexSearch;
for (size_t i = curPos + 1; i <= buf.size() - lastHexSearch.size(); ++i) {
bool match = true;
for (size_t j = 0; j < lastHexSearch.size(); ++j) {
if (buf[i + j] != lastHexSearch[j]) {
match = false;
break;
}
}
if (match) {
matchOff.push_back(i);
curPos = i;
dpOffset = curPos - (curPos % bpr);
break;
}
}
} else { // ASCII検索
lastSearch = statusText;
for (size_t i = curPos + 1; i <= buf.size() - lastSearch.size(); ++i) {
bool match = true;
for (size_t j = 0; j < lastSearch.size(); ++j) {
if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
match = false;
break;
}
}
if (match) {
matchOff.push_back(i);
curPos = i;
dpOffset = curPos - (curPos % bpr);
break;
}
}
}
statusMode = Status_Normal;
statusText.clear();
render();
}
void HexEditor::handleReverseSearch() {
statusMode = Status_Search;
lastSearchDir = Direction_Reverse;
statusText.clear();
render();
int ch;
while ((ch = getch()) != '\n' && ch != 27) {
#ifdef _WIN32
if (ch == KEY_BACKSPACE || ch == 8) {
#else
if (ch == KEY_BACKSPACE || ch == 127) {
#endif
if (!statusText.empty()) statusText.pop_back();
} else if (ch >= 32 && ch <= 126) {
statusText += ch;
}
render();
}
if (ch == 27) { // Esc
statusMode = Status_Normal;
statusText.clear();
render();
return;
}
lastSearch.clear();
lastHexSearch.clear();
matchOff.clear();
std::istringstream iss(statusText);
std::string hexByte;
bool isHexSearch = true;
std::vector<uint8_t> hexSearch;
while (iss >> hexByte) {
if (hexByte.size() != 2 || !std::all_of(hexByte.begin(), hexByte.end(), [](char c) { return std::isxdigit(c); })) {
isHexSearch = false;
break;
}
try {
hexSearch.push_back(static_cast<uint8_t>(std::stoul(hexByte, nullptr, 16)));
} catch (...) {
isHexSearch = false;
break;
}
}
if (isHexSearch && !hexSearch.empty()) {
lastHexSearch = hexSearch;
size_t startPos = curPos > 0 ? curPos - 1 : buf.size() - lastHexSearch.size();
for (size_t i = startPos + 1; i > 0; --i) {
size_t pos = i - 1;
if (pos > buf.size() - lastHexSearch.size()) continue;
bool match = true;
for (size_t j = 0; j < lastHexSearch.size(); ++j) {
if (buf[pos + j] != lastHexSearch[j]) {
match = false;
break;
}
}
if (match) {
matchOff.push_back(pos);
curPos = pos;
dpOffset = curPos - (curPos % bpr);
break;
}
}
} else {
lastSearch = statusText;
size_t startPos = curPos > 0 ? curPos - 1 : buf.size() - lastSearch.size();
for (size_t i = startPos + 1; i > 0; --i) {
size_t pos = i - 1;
if (pos > buf.size() - lastSearch.size()) continue;
bool match = true;
for (size_t j = 0; j < lastSearch.size(); ++j) {
if (buf[pos + j] != static_cast<uint8_t>(lastSearch[j])) {
match = false;
break;
}
}
if (match) {
matchOff.push_back(pos);
curPos = pos;
dpOffset = curPos - (curPos % bpr);
break;
}
}
}
statusMode = Status_Normal;
statusText.clear();
render();
}
void HexEditor::handleReplace() {
statusMode = Status_Replace;
statusText.clear();
render();
std::string hexInput;
int ch;
while ((ch = getch()) != 27) {
if (std::isxdigit(ch) && hexInput.size() < 2) {
hexInput += ch;
statusText = "REPLACE: " + hexInput;
render();
}
if (hexInput.size() == 2) {
uint8_t byte = std::stoul(hexInput, nullptr, 16);
if (curPos < buf.size()) {
undoStack.push_back({curPos, buf[curPos], byte});
redoStack.clear();
buf[curPos] = byte;
modified = true;
curPos = std::min(curPos + 1, buf.size() - 1);
}
hexInput.clear();
statusText.clear();
render();
}
}
statusMode = Status_Normal;
statusText.clear();
render();
}
void HexEditor::undo() {
if (undoStack.empty()) {
statusMode = Status_Error;
statusText = "既に一番古い変更です";
render();
return;
}
Edit edit = undoStack.back();
undoStack.pop_back();
redoStack.push_back({edit.offset, edit.newByte, edit.oldByte});
buf[edit.offset] = edit.oldByte;
if (undoStack.empty()) modified = false;
else modified = true;
curPos = edit.offset;
dpOffset = curPos - (curPos % bpr);
render();
}
void HexEditor::redo() {
if (redoStack.empty()) {
statusMode = Status_Error;
statusText = "既に一番新しい変更です";
render();
return;
}
Edit edit = redoStack.back();
redoStack.pop_back();
undoStack.push_back({edit.offset, edit.newByte, edit.oldByte});
buf[edit.offset] = edit.newByte;
modified = true;
curPos = edit.offset;
dpOffset = curPos - (curPos % bpr);
render();
}
void HexEditor::input() {
int ch;
while (running) {
if (statusMode != Status_Normal && statusMode != Status_Error) continue;
ch = getch();
if ((ch == 'j' || ch == KEY_DOWN) && curPos + bpr < buf.size()) {
curPos += bpr; // 下
if (statusMode == Status_Error) statusMode = Status_Normal;
} else if ((ch == 'k' || ch == KEY_UP) && curPos >= bpr) {
curPos -= bpr; // 上
if (statusMode == Status_Error) statusMode = Status_Normal;
} else if ((ch == 'l' || ch == KEY_RIGHT) && curPos + 1 < buf.size()) {
curPos += 1; // 右
if (statusMode == Status_Error) statusMode = Status_Normal;
} else if ((ch == 'h' || ch == KEY_LEFT) && curPos > 0) {
curPos -= 1; // 左
if (statusMode == Status_Error) statusMode = Status_Normal;
} else if ((ch == 'J' || ch == KEY_PPAGE) && curPos >= bpr) {
size_t pageSize = (rows - 3) * bpr;
if (curPos >= pageSize) curPos -= pageSize;
else curPos = 0;
if (statusMode == Status_Error) statusMode = Status_Normal;
} else if ((ch == 'K' || ch == KEY_NPAGE) && curPos + bpr < buf.size()) {
size_t pageSize = (rows - 3) * bpr;
if (curPos + pageSize < buf.size()) {
curPos += pageSize;
dpOffset = curPos - ((rows - 4) * bpr);
}
else {
curPos = buf.size() - 1;
dpOffset = (buf.size() - 1) - ((rows - 4) * bpr);
if (dpOffset > buf.size()) dpOffset = 0;
}
if (statusMode == Status_Error) statusMode = Status_Normal;
} else if (ch == '^' || ch == '0' || ch == KEY_HOME) {
curPos = (curPos / bpr) * bpr;
if (statusMode == Status_Error) statusMode = Status_Normal;
} else if (ch == '$' || ch == KEY_END) {
size_t rowStart = (curPos / bpr) * bpr;
curPos = std::min(rowStart + bpr - 1, buf.size() - 1);
if (statusMode == Status_Error) statusMode = Status_Normal;
} else if (ch == 'i') {
if (curPos - 1 > 0) curPos--;
handleReplace();
} else if (ch == 'r') {
handleReplace();
} else if (ch == 'a') {
if (curPos + 1 < buf.size()) curPos++;
handleReplace();
} else if (ch == ':') {
handleCommand();
} else if (ch == '/') {
handleSearch();
} else if (ch == '?') {
handleReverseSearch();
} else if (ch == 'n') {
if (lastSearchDir == Direction_Forward) findNextMatch();
else findPrevMatch();
} else if (ch == 'N') {
if (lastSearchDir == Direction_Forward) findPrevMatch();
else findNextMatch();
} else if (ch == 'u') {
undo();
} else if (ch == 'R') {
redo();
} else if (ch == 'g') {
if (getch() == 'g') {
curPos = 0; // ファイルの一番上
if (statusMode == Status_Error) statusMode = Status_Normal;
}
} else if (ch == 'G') {
curPos = buf.size() - 1; // ファイルの一番下
if (statusMode == Status_Error) statusMode = Status_Normal;
} else if (ch == 'Z') {
int next = getch();
if (next == 'Q') {
handleQuit(true);
break;
} else if (next == 'Z') {
handleSave();
handleQuit(true);
break;
} else if (next == 'S') {
handleSave();
if (statusMode == Status_Error) statusMode = Status_Normal;
}
}
// 画面の動き
int rows, cols;
getmaxyx(stdscr, rows, cols);
if (curPos < dpOffset) {
dpOffset = curPos - (curPos % bpr);
} else if (curPos >= dpOffset + (rows - 3) * bpr) {
dpOffset = curPos - ((rows - 4) * bpr);
}
render();
}
}
void HexEditor::run() {
render();
input();
}

83
src/hexeditor.hh Normal file
View File

@@ -0,0 +1,83 @@
#pragma once
#ifdef _WIN32
#include <pdcurses.h>
#else
#include <ncurses.h>
#endif
#include <string>
#include <vector>
class HexEditor {
private:
enum StatusMode : uint8_t {
Status_Normal,
Status_Command,
Status_Search,
Status_Replace,
Status_Error,
};
enum SearchDirection : uint8_t {
Direction_Forward,
Direction_Reverse,
};
struct Edit {
size_t offset;
uint8_t oldByte;
uint8_t newByte;
};
struct FileSignature {
std::vector<uint8_t> signature;
std::string type;
};
size_t curPos;
size_t dpOffset;
size_t bpr;
StatusMode statusMode;
bool modified;
bool running;
std::string lastSearch;
WINDOW *hexPanel;
WINDOW *asciiPanel;
std::vector<uint8_t> buf;
std::string fname;
int rows, cols;
std::string statusText;
std::vector<uint8_t> lastHexSearch;
SearchDirection lastSearchDir;
std::vector<size_t> matchOff;
std::vector<Edit> undoStack;
std::vector<Edit> redoStack;
size_t headerLen;
std::string headerType;
static const std::vector<FileSignature> signatures;
void render();
void input();
void highlightcol(size_t i, size_t row, uint8_t byte);
void topbar();
void statusbar();
void handleCommand();
void handleSave();
void handleQuit(bool force);
void handleSearch();
void handleReverseSearch();
void findNextMatch();
void findPrevMatch();
void handleReplace();
void undo();
void redo();
public:
HexEditor(const std::string &filename);
~HexEditor();
void run();
};

BIN
スクショ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB