master
ブランチ master
Hexagon / src / hexeditor.cc
29.8 KB、1101行 生 Blame 履歴
1/**************************************************************************************
2# 076 License
3
4Copyright (c) 2026 テクニカル諏訪子
5
6Permission is hereby granted to any person obtaining a copy of the software
7Hexagon (the "Software") to use, modify, merge, copy, publish, distribute,
8sublicense, and/or sell copies of the Software, subject to the following conditions:
9
10 1. **Origin Attribution**:
11 - You must not misrepresent the origin of the Software; you must not claim
12 you created the original Software.
13 - If the Software is used in a product, you must either:
14 a. Provide clear attribution in the product's documentation, user interface,
15 or other visible areas, **OR**
16 b. Pay the original developers a fee they specify in writing.
17 2. **Usage Restriction**:
18 - The Software, or any derivative works, dependencies, or libraries
19 incorporating it, must not be used for censorship or to suppress freedom of
20 speech, expression, or creativity. Prohibited uses include, but are not
21 limited to:
22 - Censorship of so-called "hate speech", visuals, non-mainstream opinions,
23 ideas, or objective reality.
24 - Tools or systems designed to restrict access to information or
25 artistic works.
26 3. **Notice Preservation**:
27 - This license and the above copyright notice must remain intact in all copies
28 of the source code.
29
30THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
31INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
32PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
33HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
34CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
35OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
36**************************************************************************************/
37#include <algorithm>
38#include <csignal>
39#include <fstream>
40#include <iomanip>
41#include <iostream>
42#include <set>
43#include <sstream>
44
45#include <locale.h>
46#include <wchar.h>
47#ifdef _WIN32
48#include <windows.h>
49#endif
50
51#include "hexeditor.hh"
52
53#ifdef _WIN32
54static BOOL WINAPI ConsoleCtrlHandler(DWORD dwCtrlType) {
55 if (dwCtrlType == CTRL_C_EVENT) return TRUE;
56 return FALSE;
57}
58#endif
59
60const std::vector<HexEditor::FileSignature> HexEditor::signatures = {
61 {{ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, "PNG"},
62 {{ 0xFF, 0xD8 }, "JPEG"},
63 {{ 0x7F, 0x45, 0x4C, 0x46 }, "ELF"},
64 {{ 0x4D, 0x5A }, "PE"},
65 {{ 0x25, 0x50, 0x44, 0x46 }, "PDF"},
66 {{ 0x67, 0x69, 0x6D, 0x70, 0x20, 0x78, 0x63, 0x66 }, "GIMP XCF"},
67 {{ 0x1A, 0x45, 0xDF, 0xA3, 0x9F, 0x42, 0x86, 0x81, 0x01, 0x42,
68 0xF7, 0x81, 0x01, 0x42, 0xF2, 0x81, 0x04, 0x42, 0xF3, 0x81,
69 0x08, 0x42, 0x82, 0x84, 0x77, 0x65, 0x62, 0x6D, 0x42, 0x87,
70 0x81, 0x04, 0x42, 0x85, 0x81, 0x02, 0x18, 0x53, 0x80, 0x67,
71 0x01, 0x00, 0x00, 0x00, 0x01 }, "WEBM"},
72 {{ 0x1A, 0x45, 0xDF, 0xA3, 0xA3, 0x42, 0x86, 0x81, 0x01, 0x42,
73 0xF7, 0x81, 0x01, 0x42, 0xF2, 0x81, 0x04, 0x42, 0xF3, 0x81,
74 0x08, 0x42, 0x82, 0x88, 0x6D, 0x61, 0x74, 0x72, 0x6F, 0x73,
75 0x6B, 0x61, 0x42, 0x87, 0x81, 0x04, 0x42, 0x85, 0x81, 0x02,
76 0x18, 0x53, 0x80, 0x67, 0x01, 0x00, 0x00, 0x00}, "Matroska"},
77 {{ 0x52, 0x49, 0x46, 0x46 }, "Waveform"},
78};
79
80HexEditor::HexEditor(const std::string &filename)
81 : curPos(0), dpOffset(0), bpr(16),
82 statusMode(Status_Normal), modified(false),
83 running(true), hasFile(!filename.empty()),
84 lastSearchDir(Direction_Forward),
85 headerLen(0), headerType("") {
86 // SIGINT (CTRL + C)を無効に
87#ifdef _WIN32
88 SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE);
89#else
90 std::signal(SIGINT, SIG_IGN);
91#endif
92
93 setlocale(LC_ALL, "");
94
95 // ncurses
96 initscr();
97#ifdef _WIN32
98 HWND console = GetConsoleWindow();
99 if (console) {
100 LONG style = GetWindowLong(console, GWL_STYLE);
101 style |= WS_MAXIMIZEBOX | WS_THICKFRAME;
102 SetWindowLong(console, GWL_STYLE, style);
103
104 const int MIN_WIDTH = 100;
105 const int MIN_HEIGHT = 30;
106
107 HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
108 CONSOLE_SCREEN_BUFFER_INFO csbi;
109 GetConsoleScreenBufferInfo(hOut, &csbi);
110
111 RECT rect;
112 GetWindowRect(console, &rect);
113
114 int cw = csbi.srWindow.Right - csbi.srWindow.Left + 1;
115 int ch = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
116
117 MINMAXINFO mmi = {};
118 mmi.ptMinTrackSize.x = MIN_WIDTH * 8;
119 mmi.ptMinTrackSize.y = MIN_HEIGHT * 16;
120
121 COORD minBuf = { MIN_WIDTH, MIN_HEIGHT };
122 SetConsoleScreenBufferSize(hOut, minBuf);
123
124 SMALL_RECT rectWin = { 0, 0, MIN_WIDTH - 1, MIN_HEIGHT - 1 };
125 SetConsoleWindowInfo(hOut, TRUE, &rectWin);
126 }
127
128 resize_term(40, 120);
129#endif
130 getmaxyx(stdscr, rows, cols);
131 clearok(stdscr, TRUE);
132 cbreak();
133 noecho();
134 keypad(stdscr, TRUE);
135 raw();
136 start_color();
137
138 // マウス対応
139#ifdef _WIN32
140 mousemask(ALL_MOUSE_EVENTS, nullptr);
141 mouse_set(ALL_MOUSE_EVENTS);
142 request_mouse_pos();
143#else
144 mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, nullptr);
145#endif
146 isMouse = true;
147
148 init_pair(1, COLOR_BLACK, COLOR_WHITE); // カーソル
149 init_pair(2, COLOR_BLACK, COLOR_GREEN); // 変更
150 init_pair(3, COLOR_BLACK, COLOR_MAGENTA); // 普通
151 init_pair(4, COLOR_BLACK, COLOR_CYAN); // コマンド
152 init_pair(5, COLOR_BLACK, COLOR_YELLOW); // 検索
153 init_pair(6, COLOR_BLACK, COLOR_YELLOW); // 検索ハイライト
154 init_pair(7, COLOR_BLACK, COLOR_RED); // エラー
155 init_pair(8, COLOR_WHITE, COLOR_BLACK); // デフォルト
156 init_pair(9, COLOR_CYAN, COLOR_BLACK); // ファイルヘッダー
157 refresh();
158
159 // ファイルをバッファーに読み込む
160 if (hasFile) {
161 std::ifstream file(filename, std::ios::binary);
162 if (!file) {
163 endwin();
164 throw std::runtime_error("ファイルを開くに失敗");
165 }
166
167 buf.assign((std::istreambuf_iterator<char>(file)), {});
168 file.close();
169
170 if (buf.empty()) {
171 endwin();
172 throw std::runtime_error("ファイルが空です");
173 }
174
175 fname = filename;
176 hasFile = true;
177
178 // ファイルヘッダー
179 for (const auto &sig : signatures) {
180 if (buf.size() >= sig.signature.size()) {
181 bool match = true;
182 for (size_t i = 0; i < sig.signature.size(); ++i) {
183 if (buf[i] != sig.signature[i]) {
184 match = false;
185 break;
186 }
187 }
188
189 if (match) {
190 headerLen = sig.signature.size();
191 headerType = sig.type;
192 break;
193 }
194 }
195 }
196 } else {
197 fname = "[No File]";
198 buf.clear();
199 hasFile = false;
200 }
201
202 // ウィンドウを作成する
203 getmaxyx(stdscr, rows, cols);
204 if (rows < 4 || cols < 20) {
205 endwin();
206 throw std::runtime_error("ターミナルが小さ過ぎます");
207 }
208
209 bpr = std::min<size_t>(16, (cols / 2 - 10) / 4);
210 hexPanel = newwin(rows - 3, cols / 2, 1, 0);
211 asciiPanel = newwin(rows - 3, cols / 2, 1, cols / 2);
212 scrollok(hexPanel, TRUE);
213 scrollok(asciiPanel, TRUE);
214 wattrset(hexPanel, COLOR_PAIR(8));
215 wattrset(asciiPanel, COLOR_PAIR(8));
216}
217
218HexEditor::~HexEditor() {
219 delwin(hexPanel);
220 delwin(asciiPanel);
221 endwin();
222}
223
224void HexEditor::highlightcol(size_t i, size_t row, uint8_t byte) {
225 wattron(hexPanel, COLOR_PAIR(1));
226 mvwprintw(hexPanel, row, 10 + i * 3, "%02x", byte);
227 wattroff(hexPanel, COLOR_PAIR(1));
228
229 wattron(asciiPanel, COLOR_PAIR(1));
230 mvwaddch(asciiPanel, row, i, std::isprint(byte) ? byte : '.');
231 wattroff(asciiPanel, COLOR_PAIR(1));
232}
233
234void HexEditor::topbar() {
235 int colorPair = 3;
236 std::string status = "Hexagon 1.2.0";
237 if (!headerType.empty()) status += " | FILETYPE: " + headerType;
238
239 std::wstring wstatus(status.begin(), status.end());
240 wattron(stdscr, COLOR_PAIR(colorPair));
241 mvprintw(0, 0, "%s", status.c_str());
242 for (int i = status.length(); i < cols; ++i) {
243 mvaddch(0, i, ' ');
244 }
245
246 wattroff(stdscr, COLOR_PAIR(colorPair));
247}
248
249void HexEditor::statusbar() {
250 int colorPair;
251 switch (statusMode) {
252 case Status_Replace: colorPair = 2; break;
253 case Status_Normal: colorPair = 3; break;
254 case Status_Command: colorPair = 4; break;
255 case Status_Search: colorPair = 5; break;
256 case Status_Error: colorPair = 7; break;
257 }
258
259 std::string status;
260 if (statusMode == Status_Normal || statusMode == Status_Error) {
261 status = "FILE: " + fname;
262 status += " | OFFSET: 0x" + std::to_string(curPos);
263 status += " | FILE SIZE: " + std::to_string(buf.size()) + " B";
264 if (!lastSearch.empty()) {
265 status += " | SEARCH: " + lastSearch;
266 } else if (!lastHexSearch.empty()) {
267 std::ostringstream hexSearch;
268 for (size_t i = 0; i < lastHexSearch.size(); ++i) {
269 if (i > 0) hexSearch << " ";
270 hexSearch << std::hex << std::setfill('0') << std::setw(2)
271 << (int)lastHexSearch[i];
272 }
273 status += " | SEARCH: " + hexSearch.str();
274 }
275 status += (modified ? " [+]" : "");
276 if (statusMode == Status_Error) status += " | ERROR: " + statusText;
277 else status += !statusText.empty() ? " | " + statusText : "";
278 } else if (statusMode == Status_Command) {
279 status = ":" + statusText;
280 } else if (statusMode == Status_Search) {
281 status = (lastSearchDir == Direction_Forward ? "/" : "?") + statusText;
282 } else if (statusMode == Status_Replace) {
283 status = "-- REPLACE --";
284 }
285
286 std::wstring wstatus(status.begin(), status.end());
287 wattron(stdscr, COLOR_PAIR(colorPair));
288 mvprintw(rows - 1, 0, "%s", status.c_str());
289 for (int i = status.length(); i < cols; ++i) {
290 mvaddch(rows - 1, i, ' ');
291 }
292
293 wattroff(stdscr, COLOR_PAIR(colorPair));
294}
295
296void HexEditor::render() {
297 werase(hexPanel);
298 werase(asciiPanel);
299
300 getmaxyx(stdscr, rows, cols);
301 size_t maxRows = rows - 3;
302
303 topbar();
304
305 std::set<size_t> matchBytes;
306 if (!lastSearch.empty()) {
307 for (size_t i = 0; i <= buf.size() - lastSearch.size(); ++i) {
308 bool match = true;
309 for (size_t j = 0; j < lastSearch.size(); ++j) {
310 if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
311 match = false;
312 break;
313 }
314 }
315
316 if (match) {
317 for (size_t j = 0; j < lastSearch.size(); ++j) {
318 matchBytes.insert(i + j);
319 }
320 }
321 }
322 } else if (!lastHexSearch.empty()) {
323 for (size_t i = 0; i <= buf.size() - lastHexSearch.size(); ++i) {
324 bool match = true;
325 for (size_t j = 0; j < lastHexSearch.size(); ++j) {
326 if (buf[i + j] != lastHexSearch[j]) {
327 match = false;
328 break;
329 }
330 }
331
332 if (match) {
333 for (size_t j = 0; j < lastHexSearch.size(); ++j) {
334 matchBytes.insert(i + j);
335 }
336 }
337 }
338 }
339
340 // HEXとASCIIの表示
341 for (size_t row = 0; row < maxRows; ++row) {
342 size_t offset = dpOffset + row * bpr;
343 if (offset >= buf.size()) break;
344
345 std::ostringstream hexLine, asciiLine;
346 hexLine << std::hex << std::setfill('0') << std::setw(8) << offset << ": ";
347 mvwprintw(hexPanel, row, 0, "%s", hexLine.str().c_str());
348
349 // HEXとASCIIのデータ
350 for (size_t i = 0; i < bpr && (offset + i) < buf.size(); ++i) {
351 uint8_t byte = buf[offset + i];
352 bool isMatch = matchBytes.count(offset + i) > 0;
353 bool isHeader = (offset + i) < headerLen;
354
355 if (offset + i == curPos) {
356 highlightcol(i, row, byte);
357 } else if (isHeader) {
358 wattron(hexPanel, COLOR_PAIR(9));
359 mvwprintw(hexPanel, row, 10 + i * 3, "%02x", byte);
360 wattroff(hexPanel, COLOR_PAIR(9));
361
362 wattron(asciiPanel, COLOR_PAIR(9));
363 mvwaddch(asciiPanel, row, i, std::isprint(byte) ? byte : '.');
364 wattroff(asciiPanel, COLOR_PAIR(9));
365 } else if (isMatch) {
366 wattron(hexPanel, COLOR_PAIR(6));
367 mvwprintw(hexPanel, row, 10 + i * 3, "%02x", byte);
368 wattroff(hexPanel, COLOR_PAIR(6));
369
370 wattron(asciiPanel, COLOR_PAIR(6));
371 mvwaddch(asciiPanel, row, i, std::isprint(byte) ? byte : '.');
372 wattroff(asciiPanel, COLOR_PAIR(6));
373 } else {
374 wattron(hexPanel, COLOR_PAIR(8));
375 mvwprintw(hexPanel, row, 10 + i * 3, "%02x", byte);
376 wattroff(hexPanel, COLOR_PAIR(8));
377
378 wattron(asciiPanel, COLOR_PAIR(8));
379 mvwaddch(asciiPanel, row, i, std::isprint(byte) ? byte : '.');
380 wattroff(asciiPanel, COLOR_PAIR(8));
381 }
382 }
383 }
384
385 statusbar();
386
387 redrawwin(hexPanel);
388 redrawwin(asciiPanel);
389 wrefresh(hexPanel);
390 wrefresh(asciiPanel);
391 refresh();
392}
393
394void HexEditor::findNextMatch() {
395 if (lastSearch.empty() && lastHexSearch.empty()) return;
396
397 size_t searchSize = lastSearch.empty() ? lastHexSearch.size() : lastSearch.size();
398 size_t startPos = curPos + 1;
399
400 for (size_t i = startPos; i <= buf.size() - searchSize; ++i) {
401 bool match = true;
402 if (!lastSearch.empty()) {
403 for (size_t j = 0; j < lastSearch.size(); ++j) {
404 if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
405 match = false;
406 break;
407 }
408 }
409 } else {
410 for (size_t j = 0; j < lastHexSearch.size(); ++j) {
411 if (buf[i + j] != lastHexSearch[j]) {
412 match = false;
413 break;
414 }
415 }
416 }
417 if (match) {
418 curPos = i;
419 dpOffset = curPos - (curPos % bpr);
420 render();
421 return;
422 }
423 }
424
425 for (size_t i = 0; i < startPos && i <= buf.size() - searchSize; ++i) {
426 bool match = true;
427 if (!lastSearch.empty()) {
428 for (size_t j = 0; j < lastSearch.size(); ++j) {
429 if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
430 match = false;
431 break;
432 }
433 }
434 } else {
435 for (size_t j = 0; j < lastHexSearch.size(); ++j) {
436 if (buf[i + j] != lastHexSearch[j]) {
437 match = false;
438 break;
439 }
440 }
441 }
442 if (match) {
443 curPos = i;
444 dpOffset = curPos - (curPos % bpr);
445 render();
446 return;
447 }
448 }
449}
450
451void HexEditor::findPrevMatch() {
452 if (lastSearch.empty() && lastHexSearch.empty()) return;
453
454 size_t searchSize = lastSearch.empty() ? lastHexSearch.size() : lastSearch.size();
455 size_t startPos = curPos > 0 ? curPos - 1 : buf.size() - searchSize;
456
457 for (size_t i = startPos + 1; i > 0; --i) {
458 size_t pos = i - 1;
459 if (pos > buf.size() - searchSize) continue;
460 bool match = true;
461 if (!lastSearch.empty()) {
462 for (size_t j = 0; j < lastSearch.size(); ++j) {
463 if (buf[pos + j] != static_cast<uint8_t>(lastSearch[j])) {
464 match = false;
465 break;
466 }
467 }
468 } else {
469 for (size_t j = 0; j < lastHexSearch.size(); ++j) {
470 if (buf[pos + j] != lastHexSearch[j]) {
471 match = false;
472 break;
473 }
474 }
475 }
476 if (match) {
477 curPos = pos;
478 dpOffset = curPos - (curPos % bpr);
479 render();
480 return;
481 }
482 }
483
484 for (size_t i = buf.size() - searchSize; i > startPos; --i) {
485 bool match = true;
486 if (!lastSearch.empty()) {
487 for (size_t j = 0; j < lastSearch.size(); ++j) {
488 if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
489 match = false;
490 break;
491 }
492 }
493 } else {
494 for (size_t j = 0; j < lastHexSearch.size(); ++j) {
495 if (buf[i + j] != lastHexSearch[j]) {
496 match = false;
497 break;
498 }
499 }
500 }
501 if (match) {
502 curPos = i;
503 dpOffset = curPos - (curPos % bpr);
504 render();
505 return;
506 }
507 }
508}
509
510void HexEditor::handleCommand() {
511 statusMode = Status_Command;
512 statusText.clear();
513 render();
514
515 int ch;
516 while ((ch = getch()) != '\n' && ch != 27) {
517#ifdef _WIN32
518 if (ch == KEY_BACKSPACE || ch == 8) {
519#else
520 if (ch == KEY_BACKSPACE || ch == 127) {
521#endif
522 if (!statusText.empty()) {
523 statusText.pop_back();
524 }
525 } else if (ch >= 32 && ch <= 126) { // 書き込める文字
526 statusText += ch;
527 }
528
529 render();
530 }
531
532 if (ch == 27) { // Esc
533 statusMode = Status_Normal;
534 statusText.clear();
535 render();
536 return;
537 }
538
539 bool isCommand = false;
540 if (statusText == "w") {
541 handleSave();
542 isCommand = true;
543 } else if (statusText == "q") {
544 handleQuit(false);
545 isCommand = true;
546 } else if (statusText == "wq") {
547 handleSave();
548 handleQuit(false);
549 isCommand = true;
550 } else if (statusText == "q!") {
551 handleQuit(true);
552 isCommand = true;
553 } else if (statusText == "wq!") {
554 handleSave();
555 handleQuit(true);
556 isCommand = true;
557 } else if (statusText == "noh") {
558 lastSearch = "";
559 isCommand = true;
560 } else if (statusText.rfind("o ", 0) == 0 || statusText.rfind("open ", 0) == 0) {
561 std::string path = statusText.substr(statusText.find(' ') + 1);
562 handleOpen(path);
563 isCommand = true;
564 } else {
565 statusMode = Status_Error;
566 statusText = "不正なコマンド。";
567 }
568
569 if (isCommand) {
570 statusMode = Status_Normal;
571 statusText.clear();
572 }
573 render();
574}
575
576void HexEditor::handleQuit(bool force) {
577 if (force || (!force && !modified)) {
578 running = false;
579 } else {
580 statusMode = Status_Error;
581 statusText.clear();
582 render();
583 }
584}
585
586void HexEditor::handleOpen(const std::string &path) {
587 if (path.empty()) {
588 statusMode = Status_Error;
589 statusText = "利用方法: :o <filename>";
590 render();
591 return;
592 }
593
594 std::ifstream file(path, std::ios::binary);
595 if (!file) {
596 statusMode = Status_Error;
597 statusText = "ファイルを開くに失敗: " + path;
598 render();
599 return;
600 }
601
602 std::vector<uint8_t> newbuf((std::istreambuf_iterator<char>(file)), {});
603 file.close();
604
605 if (newbuf.empty()) {
606 statusMode = Status_Error;
607 statusText = "ファイルは空です。";
608 render();
609 return;
610 }
611
612 buf = std::move(newbuf);
613 fname = path;
614 hasFile = true;
615 curPos = 0;
616 dpOffset = 0;
617 modified = false;
618 undoStack.clear();
619 redoStack.clear();
620 lastSearch.clear();
621 lastHexSearch.clear();
622 headerLen = 0;
623 headerType = "";
624
625 for (const auto &sig : signatures) {
626 if (buf.size() >= sig.signature.size()) {
627 bool match = true;
628 for (size_t i = 0; i < sig.signature.size(); ++i) {
629 if (buf[i] != sig.signature[i]) {
630 match = false;
631 break;
632 }
633 }
634 if (match) {
635 headerLen = sig.signature.size();
636 headerType = sig.type;
637 break;
638 }
639 }
640 }
641
642 statusMode = Status_Normal;
643 statusText = path + " を開きました。";
644 render();
645}
646
647void HexEditor::handleSave() {
648 std::ofstream file(fname, std::ios::binary);
649 if (file) {
650 file.write(reinterpret_cast<const char *>(buf.data()), buf.size());
651 file.close();
652 modified = false;
653 statusText = "保存済み";
654 }
655}
656
657void HexEditor::handleSearch() {
658 statusMode = Status_Search;
659 lastSearchDir = Direction_Forward;
660 statusText.clear();
661 render();
662
663 int ch;
664 while ((ch = getch()) != '\n' && ch != 27) {
665#ifdef _WIN32
666 if (ch == KEY_BACKSPACE || ch == 8) {
667#else
668 if (ch == KEY_BACKSPACE || ch == 127) {
669#endif
670 if (!statusText.empty()) statusText.pop_back();
671 } else if (ch >= 32 && ch <= 126) { // 書き込める文字
672 statusText += ch;
673 }
674
675 render();
676 }
677
678 if (ch == 27) { // Esc
679 statusMode = Status_Normal;
680 statusText.clear();
681 render();
682 return;
683 }
684
685 lastSearch.clear();
686 lastHexSearch.clear();
687 matchOff.clear();
688
689 std::istringstream iss(statusText);
690 std::string hexByte;
691 bool isHexSearch = true;
692 std::vector<uint8_t> hexSearch;
693 while (iss >> hexByte) {
694 if (hexByte.size() != 2 || !std::all_of(hexByte.begin(), hexByte.end(), [](char c) { return std::isxdigit(c); })) {
695 isHexSearch = false;
696 break;
697 }
698 try {
699 hexSearch.push_back(static_cast<uint8_t>(std::stoul(hexByte, nullptr, 16)));
700 } catch (...) {
701 isHexSearch = false;
702 break;
703 }
704 }
705
706 if (isHexSearch && !hexSearch.empty()) { // HEX検索
707 lastHexSearch = hexSearch;
708 for (size_t i = curPos + 1; i <= buf.size() - lastHexSearch.size(); ++i) {
709 bool match = true;
710 for (size_t j = 0; j < lastHexSearch.size(); ++j) {
711 if (buf[i + j] != lastHexSearch[j]) {
712 match = false;
713 break;
714 }
715 }
716 if (match) {
717 matchOff.push_back(i);
718 curPos = i;
719 dpOffset = curPos - (curPos % bpr);
720 break;
721 }
722 }
723 } else { // ASCII検索
724 lastSearch = statusText;
725 for (size_t i = curPos + 1; i <= buf.size() - lastSearch.size(); ++i) {
726 bool match = true;
727 for (size_t j = 0; j < lastSearch.size(); ++j) {
728 if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
729 match = false;
730 break;
731 }
732 }
733 if (match) {
734 matchOff.push_back(i);
735 curPos = i;
736 dpOffset = curPos - (curPos % bpr);
737 break;
738 }
739 }
740 }
741
742 statusMode = Status_Normal;
743 statusText.clear();
744 render();
745}
746
747void HexEditor::handleReverseSearch() {
748 statusMode = Status_Search;
749 lastSearchDir = Direction_Reverse;
750 statusText.clear();
751 render();
752
753 int ch;
754 while ((ch = getch()) != '\n' && ch != 27) {
755#ifdef _WIN32
756 if (ch == KEY_BACKSPACE || ch == 8) {
757#else
758 if (ch == KEY_BACKSPACE || ch == 127) {
759#endif
760 if (!statusText.empty()) statusText.pop_back();
761 } else if (ch >= 32 && ch <= 126) {
762 statusText += ch;
763 }
764 render();
765 }
766
767 if (ch == 27) { // Esc
768 statusMode = Status_Normal;
769 statusText.clear();
770 render();
771 return;
772 }
773
774 lastSearch.clear();
775 lastHexSearch.clear();
776 matchOff.clear();
777
778 std::istringstream iss(statusText);
779 std::string hexByte;
780 bool isHexSearch = true;
781 std::vector<uint8_t> hexSearch;
782 while (iss >> hexByte) {
783 if (hexByte.size() != 2 || !std::all_of(hexByte.begin(), hexByte.end(), [](char c) { return std::isxdigit(c); })) {
784 isHexSearch = false;
785 break;
786 }
787 try {
788 hexSearch.push_back(static_cast<uint8_t>(std::stoul(hexByte, nullptr, 16)));
789 } catch (...) {
790 isHexSearch = false;
791 break;
792 }
793 }
794
795 if (isHexSearch && !hexSearch.empty()) {
796 lastHexSearch = hexSearch;
797 size_t startPos = curPos > 0 ? curPos - 1 : buf.size() - lastHexSearch.size();
798 for (size_t i = startPos + 1; i > 0; --i) {
799 size_t pos = i - 1;
800 if (pos > buf.size() - lastHexSearch.size()) continue;
801 bool match = true;
802 for (size_t j = 0; j < lastHexSearch.size(); ++j) {
803 if (buf[pos + j] != lastHexSearch[j]) {
804 match = false;
805 break;
806 }
807 }
808 if (match) {
809 matchOff.push_back(pos);
810 curPos = pos;
811 dpOffset = curPos - (curPos % bpr);
812 break;
813 }
814 }
815 } else {
816 lastSearch = statusText;
817 size_t startPos = curPos > 0 ? curPos - 1 : buf.size() - lastSearch.size();
818 for (size_t i = startPos + 1; i > 0; --i) {
819 size_t pos = i - 1;
820 if (pos > buf.size() - lastSearch.size()) continue;
821 bool match = true;
822 for (size_t j = 0; j < lastSearch.size(); ++j) {
823 if (buf[pos + j] != static_cast<uint8_t>(lastSearch[j])) {
824 match = false;
825 break;
826 }
827 }
828 if (match) {
829 matchOff.push_back(pos);
830 curPos = pos;
831 dpOffset = curPos - (curPos % bpr);
832 break;
833 }
834 }
835 }
836
837 statusMode = Status_Normal;
838 statusText.clear();
839 render();
840}
841
842void HexEditor::handleReplace() {
843 statusMode = Status_Replace;
844 statusText.clear();
845 render();
846
847 std::string hexInput;
848 int ch;
849 while ((ch = getch()) != 27) {
850 if (std::isxdigit(ch) && hexInput.size() < 2) {
851 hexInput += ch;
852 statusText = "REPLACE: " + hexInput;
853 render();
854 }
855
856 if (hexInput.size() == 2) {
857 uint8_t byte = std::stoul(hexInput, nullptr, 16);
858 if (curPos < buf.size()) {
859 undoStack.push_back({curPos, buf[curPos], byte});
860 redoStack.clear();
861 buf[curPos] = byte;
862 modified = true;
863 curPos = std::min(curPos + 1, buf.size() - 1);
864 }
865
866 hexInput.clear();
867 statusText.clear();
868 render();
869 }
870 }
871
872 statusMode = Status_Normal;
873 statusText.clear();
874 render();
875}
876
877void HexEditor::handleMouse() {
878#ifdef _WIN32
879 request_mouse_pos();
880
881 int mx = Mouse_status.x;
882 int my = Mouse_status.y;
883
884 bool click = false;
885 for (int i = 0; i < 3; ++i) {
886 if ((Mouse_status.button[i] & BUTTON1_PRESSED)
887 || (Mouse_status.button[i] & BUTTON1_CLICKED)) {
888 click = true;
889 break;
890 }
891 }
892 if (!click) return;
893#else
894 MEVENT event;
895 if (getmouse(&event) != OK) return;
896 if (!(event.bstate & (BUTTON1_PRESSED | BUTTON1_CLICKED))) return;
897
898 int mx = event.x;
899 int my = event.y;
900#endif
901 if (my == 0 || my == rows - 1) return;
902
903 if (my >= 1 && my < rows - 2 && mx < cols / 2) {
904 // HEXパネル
905 int panelRow = my - 1;
906 int panelCol = mx;
907 if (panelCol < 10) return;
908
909 int i = (panelCol - 10) / 3;
910 size_t offset = dpOffset + panelRow * bpr + i;
911 if (offset < buf.size()) {
912 curPos = offset;
913 render();
914 }
915 } else if (my >= 1 && my < rows - 2 && mx >= cols / 2) {
916 // ASCIIパネル
917 int panelRow = my - 1;
918 int i = mx - cols / 2;
919 size_t offset = dpOffset + panelRow * bpr + i;
920 if (offset < buf.size()) {
921 curPos = offset;
922 render();
923 }
924 }
925}
926
927void HexEditor::undo() {
928 if (undoStack.empty()) {
929 statusMode = Status_Error;
930 statusText = "既に一番古い変更です";
931 render();
932 return;
933 }
934
935 Edit edit = undoStack.back();
936 undoStack.pop_back();
937 redoStack.push_back({edit.offset, edit.newByte, edit.oldByte});
938 buf[edit.offset] = edit.oldByte;
939 if (undoStack.empty()) modified = false;
940 else modified = true;
941 curPos = edit.offset;
942 dpOffset = curPos - (curPos % bpr);
943 render();
944}
945
946void HexEditor::redo() {
947 if (redoStack.empty()) {
948 statusMode = Status_Error;
949 statusText = "既に一番新しい変更です";
950 render();
951 return;
952 }
953
954 Edit edit = redoStack.back();
955 redoStack.pop_back();
956 undoStack.push_back({edit.offset, edit.newByte, edit.oldByte});
957 buf[edit.offset] = edit.newByte;
958 modified = true;
959 curPos = edit.offset;
960 dpOffset = curPos - (curPos % bpr);
961 render();
962}
963
964void HexEditor::input() {
965 int ch;
966
967 while (running) {
968 if (statusMode != Status_Normal && statusMode != Status_Error) continue;
969
970 ch = getch();
971
972 if (ch == KEY_RESIZE) {
973 getmaxyx(stdscr, rows, cols);
974 delwin(hexPanel);
975 delwin(asciiPanel);
976
977 bpr = std::min<size_t>(16, (cols / 2 - 10) / 4);
978 if (bpr < 4) bpr = 4;
979
980 hexPanel = newwin(rows - 3, cols / 2, 1, 0);
981 asciiPanel = newwin(rows - 3, cols / 2, 1, cols / 2);
982
983 scrollok(hexPanel, TRUE);
984 scrollok(asciiPanel, TRUE);
985 wattrset(hexPanel, COLOR_PAIR(8));
986 wattrset(asciiPanel, COLOR_PAIR(8));
987
988 render();
989 continue;
990 }
991
992 if (!hasFile && ch != ':' && ch != 'Z') {
993 statusMode = Status_Error;
994 statusText = "ファイルを読み込んでいません。「:o <file>」を御利用下さい。";
995 render();
996 continue;
997 }
998
999 if ((ch == 'j' || ch == KEY_DOWN) && curPos + bpr < buf.size()) {
1000 curPos += bpr; // 下
1001 if (statusMode == Status_Error) statusMode = Status_Normal;
1002 } else if ((ch == 'k' || ch == KEY_UP) && curPos >= bpr) {
1003 curPos -= bpr; // 上
1004 if (statusMode == Status_Error) statusMode = Status_Normal;
1005 } else if ((ch == 'l' || ch == KEY_RIGHT) && curPos + 1 < buf.size()) {
1006 curPos += 1; // 右
1007 if (statusMode == Status_Error) statusMode = Status_Normal;
1008 } else if ((ch == 'h' || ch == KEY_LEFT) && curPos > 0) {
1009 curPos -= 1; // 左
1010 if (statusMode == Status_Error) statusMode = Status_Normal;
1011 } else if ((ch == 'J' || ch == KEY_PPAGE) && curPos >= bpr) {
1012 size_t pageSize = (rows - 3) * bpr;
1013 if (curPos >= pageSize) curPos -= pageSize;
1014 else curPos = 0;
1015 if (statusMode == Status_Error) statusMode = Status_Normal;
1016 } else if ((ch == 'K' || ch == KEY_NPAGE) && curPos + bpr < buf.size()) {
1017 size_t pageSize = (rows - 3) * bpr;
1018 if (curPos + pageSize < buf.size()) {
1019 curPos += pageSize;
1020 dpOffset = curPos - ((rows - 4) * bpr);
1021 }
1022 else {
1023 curPos = buf.size() - 1;
1024 dpOffset = (buf.size() - 1) - ((rows - 4) * bpr);
1025 if (dpOffset > buf.size()) dpOffset = 0;
1026 }
1027 if (statusMode == Status_Error) statusMode = Status_Normal;
1028 } else if (ch == '^' || ch == '0' || ch == KEY_HOME) {
1029 curPos = (curPos / bpr) * bpr;
1030 if (statusMode == Status_Error) statusMode = Status_Normal;
1031 } else if (ch == '$' || ch == KEY_END) {
1032 size_t rowStart = (curPos / bpr) * bpr;
1033 curPos = std::min(rowStart + bpr - 1, buf.size() - 1);
1034 if (statusMode == Status_Error) statusMode = Status_Normal;
1035 } else if (ch == 'i') {
1036 if (curPos - 1 > 0) curPos--;
1037 handleReplace();
1038 } else if (ch == 'r') {
1039 handleReplace();
1040 } else if (ch == 'a') {
1041 if (curPos + 1 < buf.size()) curPos++;
1042 handleReplace();
1043 } else if (ch == ':') {
1044 handleCommand();
1045 } else if (ch == '/') {
1046 handleSearch();
1047 } else if (ch == '?') {
1048 handleReverseSearch();
1049 } else if (ch == 'n') {
1050 if (lastSearchDir == Direction_Forward) findNextMatch();
1051 else findPrevMatch();
1052 } else if (ch == 'N') {
1053 if (lastSearchDir == Direction_Forward) findPrevMatch();
1054 else findNextMatch();
1055 } else if (ch == 'u') {
1056 undo();
1057 } else if (ch == 'R') {
1058 redo();
1059 } else if (ch == 'g') {
1060 if (getch() == 'g') {
1061 curPos = 0; // ファイルの一番上
1062 if (statusMode == Status_Error) statusMode = Status_Normal;
1063 }
1064 } else if (ch == 'G') {
1065 curPos = buf.size() - 1; // ファイルの一番下
1066 if (statusMode == Status_Error) statusMode = Status_Normal;
1067 } else if (ch == 'Z') {
1068 int next = getch();
1069 if (next == 'Q') {
1070 handleQuit(true);
1071 break;
1072 } else if (next == 'Z') {
1073 handleSave();
1074 handleQuit(true);
1075 break;
1076 } else if (next == 'S') {
1077 handleSave();
1078 if (statusMode == Status_Error) statusMode = Status_Normal;
1079 }
1080 } else if (ch == KEY_MOUSE) {
1081 handleMouse();
1082 continue;
1083 }
1084
1085 // 画面の動き
1086 int rows, cols;
1087 getmaxyx(stdscr, rows, cols);
1088 if (curPos < dpOffset) {
1089 dpOffset = curPos - (curPos % bpr);
1090 } else if (curPos >= dpOffset + (rows - 3) * bpr) {
1091 dpOffset = curPos - ((rows - 4) * bpr);
1092 }
1093
1094 render();
1095 }
1096}
1097
1098void HexEditor::run() {
1099 render();
1100 input();
1101}