forked from krpors/hx
-
Notifications
You must be signed in to change notification settings - Fork 0
/
editor.c
1439 lines (1226 loc) · 43.7 KB
/
editor.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* This file is part of hx - a hex editor for the terminal.
*
* Copyright (c) 2016 Kevin Pors. See LICENSE for details.
*/
#include "editor.h"
#include "util.h"
#include "undo.h"
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/*
* This function looks convoluted as hell, but it works...
*/
void editor_move_cursor(struct editor* e, int dir, int amount) {
switch (dir) {
case KEY_UP: e->cursor_y-=amount; break;
case KEY_DOWN: e->cursor_y+=amount; break;
case KEY_LEFT: e->cursor_x-=amount; break;
case KEY_RIGHT: e->cursor_x+=amount; break;
}
// Did we hit the start of the file? If so, stop moving and place
// the cursor on the top-left of the hex display.
if (e->cursor_x <= 1 && e->cursor_y <= 1 && e->line <= 0) {
e->cursor_x = 1;
e->cursor_y = 1;
return;
}
// Move the cursor over the x (columns) axis.
if (e->cursor_x < 1) {
// Are we trying to move left on the leftmost boundary?
//
// 000000000: 4d49 5420 4c69 6365 6e73 650a 0a43 6f70 MIT License..Cop
// 000000010: 7972 6967 6874 2028 6329 2032 3031 3620 yright (c) 2016
// <--- [cursor goes to the left]
//
// Then we go up a row, cursor to the right. Like a text editor.
if (e->cursor_y >= 1) {
e->cursor_y--;
e->cursor_x = e->octets_per_line;
}
} else if (e->cursor_x > e->octets_per_line) {
// Moving to the rightmost boundary?
//
// 000000000: 4d49 5420 4c69 6365 6e73 650a 0a43 6f70 MIT License..Cop
// 000000010: 7972 6967 6874 2028 6329 2032 3031 3620 yright (c) 2016
// [cursor goes to the right] --->
//
// Then move a line down, position the cursor to the beginning of the row.
// Unless it's the end of file.
e->cursor_y++;
e->cursor_x = 1;
}
// Did we try to move up when there's nothing? For example
//
// [up here]
// --------------------------^
// 000000000: 4d49 5420 4c69 6365 6e73 650a 0a43 6f70 MIT License..Cop
//
// Then stop moving upwards, do not scroll, return.
if (e->cursor_y <= 1 && e->line <= 0) {
e->cursor_y = 1;
}
// Move the cursor over the y axis
if (e->cursor_y > e->screen_rows - 1) {
e->cursor_y = e->screen_rows - 1;
editor_scroll(e, 1);
} else if (e->cursor_y < 1 && e->line > 0) {
e->cursor_y = 1;
editor_scroll(e, -1);
}
// Did we hit the end of the file somehow? Set the cursor position
// to the maximum cursor position possible.
unsigned int offset = editor_offset_at_cursor(e);
if (offset >= e->content_length - 1) {
editor_cursor_at_offset(e, offset, &e->cursor_x, &e->cursor_y);
return;
}
}
void editor_newfile(struct editor* e, const char* filename) {
e->filename = malloc(strlen(filename) + 1);
if (e->filename == NULL) {
perror("Could not allocate memory for the filename");
abort();
}
strncpy(e->filename, filename, strlen(filename) + 1);
e->contents = malloc(0);
e->content_length = 0;
}
void editor_openfile(struct editor* e, const char* filename) {
FILE* fp = fopen(filename, "rb");
if (fp == NULL) {
if (errno == ENOENT) {
// file does not exist, open it as a new file and return.
editor_newfile(e, filename);
return;
}
// Other errors (i.e. permission denied). Exit prematurely,
// no use in continuing.
perror("Unable to open file");
exit(1);
}
// stat() the file.
struct stat statbuf;
if (stat(filename, &statbuf) == -1) {
perror("Cannot stat file");
exit(1);
}
// S_ISREG is a a POSIX macro to check whether the given st_mode denotes a
// regular file. See `man 2 stat'.
if (!S_ISREG(statbuf.st_mode)) {
fprintf(stderr, "File '%s' is not a regular file\n", filename);
exit(1);
}
// The content buffer. When stat() returns a non-zero length, this will
// be malloc'd. When <= 0, this will be assigned via a charbuf. This
// branching is done because 1) Otherwise /proc/ cannot be read, and 2)
// reading a large file just with fgetc() imposes a major negative performance
// impact.
char* contents;
int content_length = 0;
if (statbuf.st_size <= 0) {
// The stat() returned a (less than) zero size length. This may be
// because the user is trying to read a file from /proc/. In that
// case, read the data per-byte until EOF.
struct charbuf* buf = charbuf_create();
int c;
char tempbuf[1];
while ((c = fgetc(fp)) != EOF) {
tempbuf[0] = (char) c;
charbuf_append(buf, tempbuf, 1);
}
// Point contents to the charbuf's contents and set the length accordingly.
contents = buf->contents;
content_length = buf->len;
} else {
// stat() returned a size we can work with. Allocate memory for the
// buffer, No need for extra room for a null string terminator, since
// we're possibly reading binary data only anyway (which can contain 0x00).
contents = malloc(sizeof(char) * statbuf.st_size);
if (contents == NULL) {
perror("Could not allocate memory for the file specified");
abort();
}
content_length = statbuf.st_size;
// fread() has a massive performance improvement when reading large files.
if (fread(contents, 1, statbuf.st_size, fp) < (size_t) statbuf.st_size) {
perror("Unable to read file contents");
free(contents);
exit(1);
}
}
// duplicate string without using gnu99 strdup().
e->filename = malloc(strlen(filename) + 1);
if (e->filename == NULL) {
perror("Could not allocate memory for the filename");
abort();
}
strncpy(e->filename, filename, strlen(filename) + 1);
e->contents = contents;
e->content_length = content_length;
// Check if the file is readonly, and warn the user about that.
if (access(filename, W_OK) == -1) {
editor_statusmessage(e, STATUS_WARNING, "\"%s\" (%d bytes) [readonly]", e->filename, e->content_length);
} else {
editor_statusmessage(e, STATUS_INFO, "\"%s\" (%d bytes)", e->filename, e->content_length);
}
if (fclose(fp) != 0) {
perror("Could not close file properly");
abort();
}
}
void editor_writefile(struct editor* e) {
assert(e->filename != NULL);
FILE* fp = fopen(e->filename, "wb");
if (fp == NULL) {
editor_statusmessage(e, STATUS_ERROR, "Unable to open '%s' for writing: %s", e->filename, strerror(errno));
return;
}
size_t bw = fwrite(e->contents, sizeof(char), e->content_length, fp);
if (bw <= 0) {
editor_statusmessage(e, STATUS_ERROR, "Unable write to file: %s", strerror(errno));
return;
}
editor_statusmessage(e, STATUS_INFO, "\"%s\", %d bytes written", e->filename, e->content_length);
e->dirty = false;
if (fclose(fp) != 0) {
perror("Could not close file properly");
abort();
}
}
void editor_cursor_at_offset(struct editor* e, int offset, int* x, int* y) {
*x = offset % e->octets_per_line + 1;
*y = offset / e->octets_per_line - e->line + 1;
}
void editor_delete_char_at_cursor(struct editor* e) {
unsigned int offset = editor_offset_at_cursor(e);
unsigned int old_length = e->content_length;
if (e->content_length <= 0) {
editor_statusmessage(e, STATUS_WARNING, "Nothing to delete");
return;
}
unsigned char charat = e->contents[offset];
editor_delete_char_at_offset(e, offset);
e->dirty = true;
// if the deleted offset was the maximum offset, move the cursor to
// the left.
if (offset >= old_length - 1) {
editor_move_cursor(e, KEY_LEFT, 1);
}
action_list_add(e->undo_list, ACTION_DELETE, offset, charat);
}
void editor_delete_char_at_offset(struct editor* e, unsigned int offset) {
// Remove an element from the contents buffer by moving memory.
// The character at the current offset is supposed to be removed.
// Take the offset + 1, until the end of the buffer. Copy that
// part over the offset, reallocate the contents buffer with one
// character in size less.
memmove(e->contents + offset, e->contents + offset + 1 , e->content_length - offset - 1);
e->contents = realloc(e->contents, e->content_length - 1);
e->content_length--;
}
void editor_increment_byte(struct editor* e, int amount) {
unsigned int offset = editor_offset_at_cursor(e);
unsigned char prev = e->contents[offset];
e->contents[offset] += amount;
action_list_add(e->undo_list, ACTION_REPLACE, offset, prev);
}
inline int editor_offset_at_cursor(struct editor* e) {
// Calculate the offset based on the cursors' x and y coord (which is bound
// between (1 .. line width) and (1 .. max screen rows). Take the current displayed
// line into account (which is incremented when we are paging the content).
// Multiply it by octets_per_line since we're effectively addressing a one dimensional
// array.
unsigned int offset = (e->cursor_y - 1 + e->line) * e->octets_per_line + (e->cursor_x - 1);
// Safety measure. Since we're using the value of this function to
// index the content array, we must not go out of bounds.
if (offset <= 0) {
return 0;
}
if (offset >= e->content_length) {
return e->content_length - 1;
}
return offset;
}
void editor_scroll(struct editor* e, int units) {
e->line += units;
// If we wanted to scroll past the end of the file, calculate the line
// properly. Subtract the amount of screen rows (minus 2??) to prevent
// scrolling past the end of file.
int upper_limit = e->content_length / e->octets_per_line - (e->screen_rows - 2);
if (e->line >= upper_limit) {
e->line = upper_limit;
}
// If we scroll past the beginning of the file (offset 0 of course),
// set our line to zero and return. This particular condition is also
// necessary when the upper_limit calculated goes negative, because
// This is either some weird calculation failure from my part, but
// this seems to work. Failing to cap this will result in bad addressing
// of the content in render_contents().
if (e->line <= 0) {
e->line = 0;
}
}
void editor_scroll_to_offset(struct editor* e, unsigned int offset) {
if (offset > e->content_length) {
editor_statusmessage(e, STATUS_ERROR, "Out of range: 0x%09x (%u)", offset, offset);
return;
}
// Check if the offset is within range of the current display.
// Calculate the minimum offset visible, and the maximum. If
// the requested offset is within that range, do not update
// the e->line yet (i.e. do not scroll).
unsigned int offset_min = e->line * e->octets_per_line;
unsigned int offset_max = offset_min + (e->screen_rows * e->octets_per_line);
if (offset >= offset_min && offset <= offset_max) {
// We're within range! Update the cursor position, but
// do not scroll, and just return.
editor_cursor_at_offset(e, offset, &(e->cursor_x), &(e->cursor_y));
return;
}
// Determine what 'line' to set, by dividing the offset to
// be displayed by the number of octets per line. The line
// is subtracted with the number of rows in the screen, divided
// by 2 so the cursor can be centered on the screen.
e->line = offset / e->octets_per_line - (e->screen_rows / 2);
// TODO: editor_scroll uses this same limit. Probably better to refactor
// this part on way or another to prevent dupe.
int upper_limit = e->content_length / e->octets_per_line - (e->screen_rows - 2);
if (e->line >= upper_limit) {
e->line = upper_limit;
}
if (e->line <= 0) {
e->line = 0;
}
editor_cursor_at_offset(e, offset, &(e->cursor_x), &(e->cursor_y));
}
void editor_setmode(struct editor* e, enum editor_mode mode) {
e->mode = mode;
switch (e->mode) {
case MODE_NORMAL: editor_statusmessage(e, STATUS_INFO, ""); break;
case MODE_APPEND: editor_statusmessage(e, STATUS_INFO, "-- APPEND -- "); break;
case MODE_APPEND_ASCII: editor_statusmessage(e, STATUS_INFO, "-- APPEND ASCII --"); break;
case MODE_REPLACE_ASCII: editor_statusmessage(e, STATUS_INFO, "-- REPLACE ASCII --"); break;
case MODE_INSERT: editor_statusmessage(e, STATUS_INFO, "-- INSERT --"); break;
case MODE_INSERT_ASCII: editor_statusmessage(e, STATUS_INFO, "-- INSERT ASCII --"); break;
case MODE_REPLACE: editor_statusmessage(e, STATUS_INFO, "-- REPLACE --"); break;
case MODE_COMMAND: break;
case MODE_SEARCH: break;
}
}
int editor_statusmessage(struct editor* e, enum status_severity sev, const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
int x = vsnprintf(e->status_message, sizeof(e->status_message), fmt, ap);
if (x < 0) {
fprintf(stderr, "Unable to format string");
abort();
}
va_end(ap);
e->status_severity = sev;
return x;
}
void editor_render_ascii(struct editor* e, int rownum, unsigned int start_offset, struct charbuf* b) {
int cc = 0; // cursor counter, to check whether we should highlight the current offset.
for (unsigned int offset = start_offset; offset < start_offset + e->octets_per_line; offset++) {
// Make sure we do not go out of bounds.
if (offset >= e->content_length) {
return;
}
char c = e->contents[offset];
int thingy_length = 0;
const unsigned char* thingy = NULL;
// Our goal: find the longest thingy that corresponds to the current characters
if (e->thingies) {
int lk = thingy_table_longest_key(e->thingies);
//Make sure we don't do out of bounds here, either.
if (offset + lk > e->content_length) lk = (e->content_length - offset);
for (int i=1; i <= lk; i++) {
const unsigned char* v = thingy_table_search(e->thingies,i,
(const unsigned char*) e->contents + offset);
if (v) { thingy = v; thingy_length = i; }
}
}
cc+= (thingy ? thingy_length : 1);
bool hilight = false;
if (thingy) {
hilight = (rownum == e->cursor_y && cc >= e->cursor_x && cc < (e->cursor_x + thingy_length) );
} else {
hilight = (rownum == e->cursor_y && cc == e->cursor_x);
}
// If we need to highlight the cursor in the current iteration,
// do so by inverting the color (7m). In all other cases, reset (0m).
if (hilight) {
charbuf_append(b, "\x1b[7m", 4);
} else {
charbuf_append(b, "\x1b[0m", 4);
}
if (thingy) {
if ((int)((offset + thingy_length) - start_offset) > (e->octets_per_line)) {
charbuf_appendf(b, "\x1b[33m%s", (const char*) thingy);
//the different color indicates that the thingy goes past this line
} else {
charbuf_appendf(b, "\x1b[32m%s", (const char*) thingy);
}
offset += (thingy_length - 1);
} else {
// Printable characters use a different color from non-printable characters.
if (isprint(c)) {
charbuf_appendf(b, "\x1b[37m%c", c);
} else {
charbuf_append(b, "\x1b[36m.", 6);
}
}
}
// Clear formatting, erase until the end of the line: \x1b[K
charbuf_append(b, "\x1b[0m\x1b[K", 7);
}
void editor_render_contents(struct editor* e, struct charbuf* b) {
if (e->content_length <= 0) {
charbuf_append(b, "\x1b[2J", 4);
charbuf_appendf(b, "File is empty. Use 'i' to insert a hexadecimal value.");
return;
}
// FIXME: proper sizing of these arrays (malloc?)
char hex[ 32 + 1]; // example: 65
int hexlen = 0; // assigned by snprintf - we need to know the amount of chars written.
char asc[256 + 1]; // example: Hello.World!
// Counter to indicate how many chars have been written for the current
// row of data. This is used for later for padding, when the iteration
// is over, but there's still some ASCII to write.
int row_char_count = 0;
// start_offset is to determine where we should start reading from
// the buffer. This is dependent on where the cursor is, and on the
// octets which are visible per line.
unsigned int start_offset = e->line * e->octets_per_line;
if (start_offset >= e->content_length) {
start_offset = e->content_length - e->octets_per_line;
}
// Determine the end offset for displaying. There is only so much
// to be displayed 'per screen'. I.e. if you can only display 1024
// bytes, you only have to read a maximum of 1024 bytes.
int bytes_per_screen = e->screen_rows * e->octets_per_line;
unsigned int end_offset = bytes_per_screen + start_offset - e->octets_per_line;
if (end_offset > e->content_length) {
end_offset = e->content_length;
}
unsigned int offset;
int row = 0; // Row counter, from 0 to term height
int col = 0; // Col counter, from 0 to number of octets per line. Used to render
// a colored cursor per byte.
for (offset = start_offset; offset < end_offset; offset++) {
unsigned char curr_byte = e->contents[offset];
if (offset % e->octets_per_line == 0) {
// start of a new row, beginning with an offset address in hex.
charbuf_appendf(b, "\x1b[1;35m%09x\x1b[0m:", offset);
// Initialize the ascii buffer to all zeroes, and reset the row char count.
memset(asc, '\0', sizeof(asc));
row_char_count = 0;
col = 0;
row++;
}
col++;
// Format a hex string of the current character in the offset.
if (isprint(curr_byte)) {
// If the character is printable, use a different color.
hexlen = snprintf(hex, sizeof(hex), "\x1b[1;34m%02x", curr_byte);
} else {
// Non printable: use default color.
hexlen = snprintf(hex, sizeof(hex), "%02x", curr_byte);
}
// Every iteration, set the ascii value in the buffer, until
// 16 bytes are set. This will be written later when the hex
// values are drawn to screen.
if (isprint(curr_byte)) {
asc[offset % e->octets_per_line] = curr_byte;
} else {
// non-printable characters are represented by a dot.
asc[offset % e->octets_per_line] = '.';
}
// Every 'group' count, write a separator space.
if (offset % e->grouping == 0) {
charbuf_append(b, " ", 1);
row_char_count++;
}
// Cursor rendering.
if (e->cursor_y == row) {
// Render the selected byte with a different color. Easier
// to distinguish in the army of hexadecimal values.
if (e->cursor_x == col) {
charbuf_append(b, "\x1b[7m", 4);
}
}
// Write the hex value of the byte at the current offset, and reset attributes.
charbuf_append(b, hex, hexlen);
charbuf_append(b, "\x1b[0m", 4);
row_char_count += 2;
// If we reached the end of a 'row', start writing the ASCII equivalents.
if ((offset+1) % e->octets_per_line == 0) {
// Two spaces "gap" between the hexadecimal display, and the ASCII equiv.
charbuf_append(b, " ", 2);
// Calculate the 'start offset' of the ASCII part to write. Delegate
// this to the render_ascii function.
int the_offset = offset + 1 - e->octets_per_line;
editor_render_ascii(e, row, the_offset, b);
charbuf_append(b, "\r\n", 2);
}
}
// Check remainder of the last offset. If its bigger than zero,
// we got a last line to write (ASCII only).
unsigned int leftover = offset % e->octets_per_line;
if (leftover > 0) {
// Padding characters, to align the ASCII properly. For example, this
// could be the output at the end of the file:
// 000000420: 0a53 4f46 5457 4152 452e 0a .SOFTWARE..
// ^^^^^^^^^^^^
// padding chars
int padding_size = (e->octets_per_line * 2) + (e->octets_per_line / e->grouping) - row_char_count;
char* padding = malloc(padding_size * sizeof(char));
if (padding == NULL) {
perror("Could not allocate memory for padding");
abort();
}
memset(padding, ' ', padding_size);
charbuf_append(b, padding, padding_size);
charbuf_append(b, "\x1b[0m ", 6);
// render cursor on the ascii when applicable.
editor_render_ascii(e, row, offset - leftover, b);
free(padding);
}
// clear everything up until the end
charbuf_append(b, "\x1b[0K", 4);
#ifndef NDEBUG
charbuf_appendf(b, "\x1b[0m\x1b[1;35m\x1b[1;80HRows: %d", e->screen_rows);
charbuf_appendf(b, "\x1b[0K\x1b[2;80HOffset: %09x - %09x", start_offset, end_offset);
charbuf_appendf(b, "\x1b[0K\x1b[3;80H(y,x)=(%d,%d)", e->cursor_y, e->cursor_x);
unsigned int curr_offset = editor_offset_at_cursor(e);
charbuf_appendf(b, "\x1b[0K\x1b[5;80H\x1b[0KLine: %d, cursor offset: %d (hex: %02x)", e->line, curr_offset, (unsigned char) e->contents[curr_offset]);
#endif
}
void editor_render_help(struct editor* e) {
(void) e;
struct charbuf* b = charbuf_create();
clear_screen();
charbuf_append(b, "\x1b[?25l", 6); // hide cursor
charbuf_appendf(b, "This is hx, version %s\r\n\n", HX_VERSION);
charbuf_appendf(b,
"Available commands:\r\n"
"\r\n"
"CTRL+Q : Quit immediately without saving.\r\n"
"CTRL+S : Save (in place).\r\n"
"hjkl : Vim like cursor movement.\r\n"
"Arrows : Also moves the cursor around.\r\n"
"w : Skip one group of bytes to the right.\r\n"
"b : Skip one group of bytes to the left.\r\n"
"gg : Move to start of file.\r\n"
"G : Move to end of file.\r\n"
"x / DEL : Delete byte at cursor position.\r\n"
"/ : Start search input.\r\n"
"n : Search for next occurrence.\r\n"
"N : Search for previous occurrence.\r\n"
"u : Undo the last action.\r\n"
"CTRL+R : Redo the last undone action.\r\n"
"\r\n");
charbuf_appendf(b,
"a : Append mode. Appends a byte after the current cursor position.\r\n"
"A : Append mode. Appends the literal typed keys (except ESC).\r\n"
"i : Insert mode. Inserts a byte at the current cursor position.\r\n"
"I : Insert mode. Inserts the literal typed keys (except ESC).\r\n"
"r : Replace mode. Replaces the byte at the current cursor position.\r\n"
"R : Replace mode. Replaces the literal typed keys (except ESC).\r\n"
": : Command mode. Commands can be typed and executed.\r\n"
"ESC : Return to normal mode.\r\n"
"] : Increment byte at cursor position with 1.\r\n"
"[ : Decrement byte at cursor position with 1.\r\n"
"End : Move cursor to end of the offset line.\r\n"
"Home : Move cursor to the beginning of the offset line.\r\n"
"\r\n"
);
charbuf_appendf(b,
"Press any key to exit help.\r\n");
charbuf_draw(b);
read_key();
clear_screen();
}
void editor_render_ruler(struct editor* e, struct charbuf* b) {
// Nothing to see. No address, no byte, no percentage. It's all a plain
// dark void right now. Oblivion. No data to see here, move along.
if (e->content_length <= 0) {
return;
}
char rulermsg[80]; // buffer for the actual message.
char buf[20]; // buffer for the cursor positioning
unsigned int offset_at_cursor = editor_offset_at_cursor(e);
unsigned char val = e->contents[offset_at_cursor];
int percentage = (float)(offset_at_cursor + 1) / (float)e->content_length * 100;
// TODO: move cursor down etc to remain independent on the previous cursor
// movement in refresh_screen().
// Create a ruler string. We need to calculate the amount of bytes
// we've actually written, to subtract that from the screen_cols to
// align the string properly.
int rmbw = snprintf(rulermsg, sizeof(rulermsg),
"0x%09x,%d (%02x) %d%%",
offset_at_cursor, offset_at_cursor, val, percentage);
if (rmbw < 0) {
fprintf(stderr, "Could not create ruler string!");
return;
}
// Create a string for the buffer to position the cursor.
int cpbw = snprintf(buf, sizeof(buf), "\x1b[0m\x1b[%d;%dH", e->screen_rows, e->screen_cols - rmbw);
if (cpbw < 0) {
fprintf(stderr, "Could not create cursor position string!");
return;
}
// First write the cursor string, followed by the ruler message.
charbuf_append(b, buf, cpbw);
charbuf_append(b, rulermsg, rmbw);
}
void editor_render_status(struct editor* e, struct charbuf* b) {
charbuf_appendf(b, "\x1b[%d;0H", e->screen_rows);
// Set color, write status message, and reset the color after.
switch (e->status_severity) {
case STATUS_INFO: charbuf_append(b, "\x1b[0;30;47m", 10); break; // black on white
case STATUS_WARNING: charbuf_append(b, "\x1b[0;30;43m", 10); break; // black on yellow
case STATUS_ERROR: charbuf_append(b, "\x1b[1;37;41m", 10); break; // white on red
// bold/increased intensity__/ / /
// foreground color_______/ /
// background color______/
}
// When the status message is longer than the terminal width, it may span multiple
// lines. This is not wanted behaviour, since the statusline is meant to be 1 line
// only. To prevent this , we make sure vsnprintf only prints the minimum
// between the size of the status_message and screen_cols.
int maxchars = strlen(e->status_message);
if (e->screen_cols <= maxchars) {
maxchars = e->screen_cols;
}
charbuf_append(b, e->status_message, maxchars);
charbuf_append(b, "\x1b[0m\x1b[0K", 8);
}
void editor_refresh_screen(struct editor* e) {
struct charbuf* b = charbuf_create();
charbuf_append(b, "\x1b[?25l", 6);
charbuf_append(b, "\x1b[H", 3); // move the cursor top left
if (e->mode &
(MODE_REPLACE |
MODE_NORMAL |
MODE_APPEND |
MODE_APPEND_ASCII |
MODE_REPLACE_ASCII |
MODE_INSERT |
MODE_INSERT_ASCII)) {
editor_render_contents(e, b);
editor_render_status(e, b);
// Ruler: move to the right of the screen etc.
editor_render_ruler(e, b);
} else if (e->mode & MODE_COMMAND) {
// When in command mode, handle rendering different. For instance,
// the cursor is placed at the bottom. Ruler is not required.
// After moving the cursor, clear the entire line ([2K).
charbuf_appendf(b,
"\x1b[0m" // reset attributes
"\x1b[?25h" // display cursor
"\x1b[%d;1H" // move cursor down
"\x1b[2K:", // clear line, write a colon.
e->screen_rows);
charbuf_append(b, e->inputbuffer, e->inputbuffer_index);
} else if (e->mode & MODE_SEARCH) {
charbuf_appendf(b,
"\x1b[0m" // reset attributes
"\x1b[?25h" // display cursor
"\x1b[%d;1H" // mvoe cursor down
"\x1b[2K/", // clear line, write a slash.
e->screen_rows);
charbuf_append(b, e->inputbuffer, e->inputbuffer_index);
}
charbuf_draw(b);
charbuf_free(b);
}
void editor_insert_byte(struct editor* e, char x, bool after) {
int offset = editor_offset_at_cursor(e);
editor_insert_byte_at_offset(e, offset, x, after);
if (after) {
action_list_add(e->undo_list, ACTION_APPEND, offset, x);
} else {
action_list_add(e->undo_list, ACTION_INSERT, offset, x);
}
}
void editor_insert_byte_at_offset(struct editor* e, unsigned int offset, char x, bool after) {
// We are inserting a single character. Reallocate memory to contain
// this extra byte.
e->contents = realloc(e->contents, e->content_length + 1);
if (after && e->content_length) { // append is the same as insert when buffer is empty
offset++;
}
// v
// |t|e|s|t|b|y|t|e|s|...
// |t|e|s|t|_|b|y|t|e|s|...
memmove(e->contents + offset + 1, e->contents + offset, e->content_length - offset);
e->contents[offset] = x;
// Increase the content length since we inserted a character.
e->content_length++;
e->dirty = true;
}
void editor_replace_byte(struct editor* e, char x) {
unsigned int offset = editor_offset_at_cursor(e);
unsigned char prev = e->contents[offset];
e->contents[offset] = x;
editor_move_cursor(e, KEY_RIGHT, 1);
editor_statusmessage(e, STATUS_INFO, "Replaced byte at offset %09x with %02x", offset, (unsigned char) x);
e->dirty = true;
action_list_add(e->undo_list, ACTION_REPLACE, offset, prev);
}
void editor_process_command(struct editor* e, const char* cmd) {
// Command: go to base 10 offset
bool b = is_pos_num(cmd);
if (b) {
int offset = str2int(cmd, 0, e->content_length, e->content_length - 1);
editor_scroll_to_offset(e, offset);
editor_statusmessage(e, STATUS_INFO, "Positioned to offset 0x%09x (%d)", offset, offset);
return;
}
// Command: go to hex offset
if (cmd[0] == '0' && cmd[1] == 'x') {
const char* ptr = &cmd[2];
if (!is_hex(ptr)) {
editor_statusmessage(e, STATUS_ERROR, "Error: %s is not valid base 16", ptr);
return;
}
int offset = hex2int(ptr);
editor_scroll_to_offset(e, offset);
editor_statusmessage(e, STATUS_INFO, "Positioned to offset 0x%09x (%d)", offset, offset);
return;
}
if (strncmp(cmd, "w", INPUT_BUF_SIZE) == 0) {
editor_writefile(e);
return;
}
if (strncmp(cmd, "q", INPUT_BUF_SIZE) == 0) {
if (e->dirty) {
editor_statusmessage(e, STATUS_ERROR, "No write since last change (add ! to override)", cmd);
return;
} else {
exit(0);
}
}
if (strncmp(cmd, "wq", INPUT_BUF_SIZE) == 0) {
editor_writefile(e);
exit(0);
}
if (strncmp(cmd, "wq", INPUT_BUF_SIZE) == 0 || strncmp(cmd, "wq!", INPUT_BUF_SIZE) == 0) {
editor_writefile(e);
exit(0);
}
if (strncmp(cmd, "q!", INPUT_BUF_SIZE) == 0) {
exit(0);
return;
}
if (strncmp(cmd, "help", INPUT_BUF_SIZE) == 0) {
editor_render_help(e);
return;
}
if (strncmp(cmd, "thingy", 6) == 0) {
const char* thingy_def = cmd+6;
while (thingy_def[0] == ' ') thingy_def++;
int r = thingy_table_add_from_string(e->thingies, thingy_def);
if (r > 16) {
editor_statusmessage(e, STATUS_ERROR, "unable to delete this key");
} else if (r == 16) {
editor_statusmessage(e, STATUS_INFO, "Thingy definition deleted.");
} else if (r != 0) {
editor_statusmessage(e, STATUS_ERROR, "invalid thingy definition");
} else {
editor_statusmessage(e, STATUS_INFO, "New thingy definition added.");
}
return;
}
// Check if we want to set an option at runtime. The first three bytes are
// checked first, then the rest is parsed.
if (strncmp(cmd, "set", 3) == 0) {
char setcmd[INPUT_BUF_SIZE] = {0};
int setval = 0;
int items_read = sscanf(cmd, "set %[a-z]=%d", setcmd, &setval);
// command name_____________________/ /
// command value________________________/
if (items_read != 2) {
editor_statusmessage(e, STATUS_ERROR, "set command format: `set cmd=num`");
return;
}
if (strcmp(setcmd, "octets") == 0 || strcmp(setcmd, "o") == 0) {
int octets = clampi(setval, 16, 64);
clear_screen();
int offset = editor_offset_at_cursor(e);
e->octets_per_line = octets;
editor_scroll_to_offset(e, offset);
editor_statusmessage(e, STATUS_INFO, "Octets per line set to %d", octets);
return;
}
// Set the grouping of bytes to a different value.
if (strcmp(setcmd, "grouping") == 0 || strcmp(setcmd, "g") == 0) {
int grouping = clampi(setval, 4, 16);
clear_screen();
e->grouping = grouping;
editor_statusmessage(e, STATUS_INFO, "Byte grouping set to %d", grouping);
return;
}
editor_statusmessage(e, STATUS_ERROR, "Unknown option: %s", setcmd);
return;
}
editor_statusmessage(e, STATUS_ERROR, "Command not found: %s", cmd);
}
void editor_process_search(struct editor* e, const char* str, enum search_direction dir) {
// Empty search string, reset the searchstr to an empty one and
// stops searching anything.
if (strncmp(str, "", INPUT_BUF_SIZE) == 0) {
strncpy(e->searchstr, str, INPUT_BUF_SIZE);
return;
}
// new search query, update searchstr.
if (strncmp(str, e->searchstr, INPUT_BUF_SIZE) != 0) {
strncpy(e->searchstr, str, INPUT_BUF_SIZE);
}
// if we are already at the beginning of the file, no use for searching
// backwards any more.
if (dir == SEARCH_BACKWARD && editor_offset_at_cursor(e) == 0) {
editor_statusmessage(e, STATUS_INFO,
"Already at start of the file");
return;
}
struct charbuf *parsedstr = charbuf_create();
const char* parse_err;
int parse_errno = editor_parse_search_string(str, parsedstr,
&parse_err);
switch (parse_errno) {
case PARSE_INCOMPLETE_BACKSLASH:
editor_statusmessage(e, STATUS_ERROR,
"Nothing follows '\\' in search"
" string: %s", str);
break;
case PARSE_INCOMPLETE_HEX:
editor_statusmessage(e, STATUS_ERROR,
"Incomplete hex value at end"
" of search string: %s", str);
break;
case PARSE_INVALID_HEX:
editor_statusmessage(e, STATUS_ERROR,
"Invalid hex value (\\x%c%c)"
" in search string: %s",
*parse_err, *(parse_err + 1), str);
break;
case PARSE_INVALID_ESCAPE:
editor_statusmessage(e, STATUS_ERROR,
"Invalid character after \\ (%c)"
" in search string: %s",
*parse_err, str);
break;
case PARSE_SUCCESS:
// All good.
break;
}
if (parse_errno != PARSE_SUCCESS) {
// We printed an error message but we didn't return.
charbuf_free(parsedstr);
return;
}
unsigned int current_offset = editor_offset_at_cursor(e);
bool found = false;
if (dir == SEARCH_FORWARD) {
current_offset++;
for (; current_offset < e->content_length; current_offset++) {
if (memcmp(e->contents + current_offset,
parsedstr->contents, parsedstr->len) == 0) {
editor_statusmessage(e, STATUS_INFO, "");
editor_scroll_to_offset(e, current_offset);
found = true;
break;
}
}
} else if (dir == SEARCH_BACKWARD) {
// Decrement the offset once, or else we keep comparing the current offset
// position with an already found string, keeping us in the same position.
current_offset--;
// Since we are working with unsigned integers, do this trick in the for-statement
// to 'include' the zero offset with comparing.
for (; current_offset-- != 0; ) {
if (memcmp(e->contents + current_offset,
parsedstr->contents, parsedstr->len) == 0) {
editor_statusmessage(e, STATUS_INFO, "");