Files
Hexagon/src/hexeditor.cc

968 lines
26 KiB
C++

/**************************************************************************************
# 076 License
Copyright (c) 2026 テクニカル諏訪子
Permission is hereby granted to any person obtaining a copy of the software
Hexagon (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.
**************************************************************************************/
#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();
// マウス対応
#ifdef _WIN32
mousemask(ALL_MOUSE_EVENTS, nullptr);
mouse_set(ALL_MOUSE_EVENTS);
request_mouse_pos();
#else
mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, nullptr);
#endif
isMouse = true;
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.2.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::handleMouse() {
#ifdef _WIN32
request_mouse_pos();
int mx = Mouse_status.x;
int my = Mouse_status.y;
bool click = false;
for (int i = 0; i < 3; ++i) {
if ((Mouse_status.button[i] & BUTTON1_PRESSED)
|| (Mouse_status.button[i] & BUTTON1_CLICKED)) {
click = true;
break;
}
}
if (!click) return;
#else
MEVENT event;
if (getmouse(&event) != OK) return;
if (!(event.bstate & (BUTTON1_PRESSED | BUTTON1_CLICKED))) return;
int mx = event.x;
int my = event.y;
#endif
if (my == 0 || my == rows - 1) return;
if (my >= 1 && my < rows - 2 && mx < cols / 2) {
// HEXパネル
int panelRow = my - 1;
int panelCol = mx;
if (panelCol < 10) return;
int i = (panelCol - 10) / 3;
size_t offset = dpOffset + panelRow * bpr + i;
if (offset < buf.size()) {
curPos = offset;
render();
}
} else if (my >= 1 && my < rows - 2 && mx >= cols / 2) {
// ASCIIパネル
int panelRow = my - 1;
int i = mx - cols / 2;
size_t offset = dpOffset + panelRow * bpr + i;
if (offset < buf.size()) {
curPos = offset;
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;
}
} else if (ch == KEY_MOUSE) {
handleMouse();
continue;
}
// 画面の動き
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();
}