|
@@ -2,21 +2,43 @@
|
|
|
// HexView.cs: A hexadecimal viewer
|
|
|
//
|
|
|
// TODO:
|
|
|
-// - Support an operation to switch between hex and values
|
|
|
-// - Tab perhaps to switch?
|
|
|
-// - Support nibble-based navigation
|
|
|
-// - Support editing, perhaps via list of changes?
|
|
|
-// - Support selection with highlighting
|
|
|
-// - Redraw should support just repainted affected region
|
|
|
-// - Process Key needs to just queue affected region for cursor changes (as we repaint the text)
|
|
|
-
|
|
|
+// - Support searching and highlighting of the search result
|
|
|
+// - Support PageUp/PageDown/Home/End
|
|
|
+//
|
|
|
using System;
|
|
|
+using System.Collections.Generic;
|
|
|
using System.IO;
|
|
|
|
|
|
namespace Terminal.Gui {
|
|
|
+ /// <summary>
|
|
|
+ /// An Hex viewer an editor view over a System.IO.Stream
|
|
|
+ /// </summary>
|
|
|
+ /// <remarks>
|
|
|
+ /// <para>
|
|
|
+ /// This provides a hex editor on top of a seekable stream with the left side showing an hex
|
|
|
+ /// dump of the values in the stream and the right side showing the contents (filterd to
|
|
|
+ /// non-control sequence ascii characters).
|
|
|
+ /// </para>
|
|
|
+ /// <para>
|
|
|
+ /// Users can switch from one side to the other by using the tab key.
|
|
|
+ /// </para>
|
|
|
+ /// <para>
|
|
|
+ /// If you want to enable editing, set the AllowsEdits property, once that is done, the user
|
|
|
+ /// can make changes to the hexadecimal values of the stream. Any changes done are tracked
|
|
|
+ /// in the Edits property which is a sorted dictionary indicating the position where the
|
|
|
+ /// change was made and the new value. A convenience ApplyEdits method can be used to c
|
|
|
+ /// apply the methods to the underlying stream.
|
|
|
+ /// </para>
|
|
|
+ /// <para>
|
|
|
+ /// It is possible to control the first byte shown by setting the DisplayStart property
|
|
|
+ /// to the offset that you want to start viewing.
|
|
|
+ /// </para>
|
|
|
+ /// </remarks>
|
|
|
public class HexView : View {
|
|
|
+ SortedDictionary<long, byte> edits = new SortedDictionary<long, byte> ();
|
|
|
Stream source;
|
|
|
long displayStart, position;
|
|
|
+ bool firstNibble, leftSide;
|
|
|
|
|
|
/// <summary>
|
|
|
/// Creates and instance of the HexView that will render a seekable stream in hex on the allocated view region.
|
|
@@ -27,6 +49,8 @@ namespace Terminal.Gui {
|
|
|
Source = source;
|
|
|
this.source = source;
|
|
|
CanFocus = true;
|
|
|
+ leftSide = true;
|
|
|
+ firstNibble = true;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
@@ -86,6 +110,24 @@ namespace Terminal.Gui {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ //
|
|
|
+ // This is used to support editing of the buffer on a peer List<>,
|
|
|
+ // the offset corresponds to an offset relative to DisplayStart, and
|
|
|
+ // the buffer contains the contents of a screenful of data, so the
|
|
|
+ // offset is relative to the buffer.
|
|
|
+ //
|
|
|
+ //
|
|
|
+ byte GetData (byte [] buffer, int offset, out bool edited)
|
|
|
+ {
|
|
|
+ var pos = DisplayStart + offset;
|
|
|
+ if (edits.TryGetValue (pos, out byte v)) {
|
|
|
+ edited = true;
|
|
|
+ return v;
|
|
|
+ }
|
|
|
+ edited = false;
|
|
|
+ return buffer [offset];
|
|
|
+ }
|
|
|
+
|
|
|
public override void Redraw (Rect region)
|
|
|
{
|
|
|
Attribute currentAttribute;
|
|
@@ -100,7 +142,14 @@ namespace Terminal.Gui {
|
|
|
Source.Position = displayStart;
|
|
|
var n = source.Read (data, 0, data.Length);
|
|
|
|
|
|
+ int activeColor = ColorScheme.HotNormal;
|
|
|
+ int trackingColor = ColorScheme.HotFocus;
|
|
|
+
|
|
|
for (int line = 0; line < frame.Height; line++) {
|
|
|
+ var lineRect = new Rect (0, line, frame.Width, 1);
|
|
|
+ if (!region.Contains (lineRect))
|
|
|
+ continue;
|
|
|
+
|
|
|
Move (0, line);
|
|
|
Driver.SetAttribute (ColorScheme.HotNormal);
|
|
|
Driver.AddStr (string.Format ("{0:x8} ", displayStart + line * nblocks * 4));
|
|
@@ -111,28 +160,30 @@ namespace Terminal.Gui {
|
|
|
for (int block = 0; block < nblocks; block++) {
|
|
|
for (int b = 0; b < 4; b++) {
|
|
|
var offset = (line * nblocks * 4) + block * 4 + b;
|
|
|
- if (offset + displayStart == position)
|
|
|
- SetAttribute (ColorScheme.HotNormal);
|
|
|
+ bool edited;
|
|
|
+ var value = GetData (data, offset, out edited);
|
|
|
+ if (offset + displayStart == position || edited)
|
|
|
+ SetAttribute (leftSide ? activeColor : trackingColor);
|
|
|
else
|
|
|
SetAttribute (ColorScheme.Normal);
|
|
|
|
|
|
- Driver.AddStr (offset >= n ? " " : string.Format ("{0:x2} ", data [offset]));
|
|
|
+ Driver.AddStr (offset >= n ? " " : string.Format ("{0:x2}", value));
|
|
|
+ SetAttribute (ColorScheme.Normal);
|
|
|
+ Driver.AddRune (' ');
|
|
|
}
|
|
|
Driver.AddStr (block + 1 == nblocks ? " " : "| ");
|
|
|
}
|
|
|
+
|
|
|
+
|
|
|
for (int bitem = 0; bitem < nblocks * 4; bitem++) {
|
|
|
var offset = line * nblocks * 4 + bitem;
|
|
|
|
|
|
- if (offset + displayStart == position)
|
|
|
- SetAttribute (ColorScheme.HotFocus);
|
|
|
- else
|
|
|
- SetAttribute (ColorScheme.Normal);
|
|
|
-
|
|
|
+ bool edited = false;
|
|
|
Rune c = ' ';
|
|
|
if (offset >= n)
|
|
|
c = ' ';
|
|
|
else {
|
|
|
- var b = data [offset];
|
|
|
+ var b = GetData (data, offset, out edited);
|
|
|
if (b < 32)
|
|
|
c = '.';
|
|
|
else if (b > 127)
|
|
@@ -140,6 +191,11 @@ namespace Terminal.Gui {
|
|
|
else
|
|
|
c = b;
|
|
|
}
|
|
|
+ if (offset + displayStart == position || edited)
|
|
|
+ SetAttribute (leftSide ? trackingColor : activeColor);
|
|
|
+ else
|
|
|
+ SetAttribute (ColorScheme.Normal);
|
|
|
+
|
|
|
Driver.AddRune (c);
|
|
|
}
|
|
|
}
|
|
@@ -154,6 +210,9 @@ namespace Terminal.Gui {
|
|
|
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// Positions the cursor based for the hex view
|
|
|
+ /// </summary>
|
|
|
public override void PositionCursor ()
|
|
|
{
|
|
|
var delta = (int)(position - displayStart);
|
|
@@ -162,53 +221,148 @@ namespace Terminal.Gui {
|
|
|
var block = item / 4;
|
|
|
var column = (item % 4) * 3;
|
|
|
|
|
|
- Move (displayWidth + block * 14 + column, line);
|
|
|
+ if (leftSide)
|
|
|
+ Move (displayWidth + block * 14 + column + (firstNibble ? 0 : 1), line);
|
|
|
+ else
|
|
|
+ Move (displayWidth + (bytesPerLine / 4) * 14 + item - 1, line);
|
|
|
+ }
|
|
|
+
|
|
|
+ void RedisplayLine (long pos)
|
|
|
+ {
|
|
|
+ var delta = (int) (pos - DisplayStart);
|
|
|
+ var line = delta / bytesPerLine;
|
|
|
+
|
|
|
+ SetNeedsDisplay (new Rect (0, line, Frame.Width, 1));
|
|
|
+ }
|
|
|
+
|
|
|
+ void CursorRight ()
|
|
|
+ {
|
|
|
+ RedisplayLine (position);
|
|
|
+ if (leftSide) {
|
|
|
+ if (firstNibble) {
|
|
|
+ firstNibble = false;
|
|
|
+ return;
|
|
|
+ } else
|
|
|
+ firstNibble = true;
|
|
|
+ }
|
|
|
+ if (position < source.Length)
|
|
|
+ position++;
|
|
|
+ if (position >= (DisplayStart + bytesPerLine * Frame.Height)) {
|
|
|
+ SetDisplayStart (DisplayStart + bytesPerLine);
|
|
|
+ SetNeedsDisplay ();
|
|
|
+ } else
|
|
|
+ RedisplayLine (position);
|
|
|
}
|
|
|
|
|
|
public override bool ProcessKey (KeyEvent keyEvent)
|
|
|
{
|
|
|
switch (keyEvent.Key) {
|
|
|
case Key.CursorLeft:
|
|
|
+ RedisplayLine (position);
|
|
|
+ if (leftSide) {
|
|
|
+ if (!firstNibble) {
|
|
|
+ firstNibble = true;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ firstNibble = false;
|
|
|
+ }
|
|
|
if (position == 0)
|
|
|
return true;
|
|
|
if (position - 1 < DisplayStart) {
|
|
|
SetDisplayStart (displayStart - bytesPerLine);
|
|
|
SetNeedsDisplay ();
|
|
|
- }
|
|
|
+ } else
|
|
|
+ RedisplayLine (position);
|
|
|
position--;
|
|
|
break;
|
|
|
case Key.CursorRight:
|
|
|
- if (position < source.Length)
|
|
|
- position++;
|
|
|
- if (position >= (DisplayStart + bytesPerLine * Frame.Height)) {
|
|
|
- SetDisplayStart (DisplayStart + bytesPerLine);
|
|
|
- SetNeedsDisplay ();
|
|
|
- }
|
|
|
+ CursorRight ();
|
|
|
break;
|
|
|
case Key.CursorDown:
|
|
|
+ RedisplayLine (position);
|
|
|
if (position + bytesPerLine < source.Length)
|
|
|
position += bytesPerLine;
|
|
|
if (position >= (DisplayStart + bytesPerLine * Frame.Height)) {
|
|
|
SetDisplayStart (DisplayStart + bytesPerLine);
|
|
|
SetNeedsDisplay ();
|
|
|
- }
|
|
|
+ } else
|
|
|
+ RedisplayLine (position);
|
|
|
break;
|
|
|
case Key.CursorUp:
|
|
|
+ RedisplayLine (position);
|
|
|
position -= bytesPerLine;
|
|
|
if (position < 0)
|
|
|
position = 0;
|
|
|
if (position < DisplayStart) {
|
|
|
SetDisplayStart (DisplayStart - bytesPerLine);
|
|
|
SetNeedsDisplay ();
|
|
|
- }
|
|
|
+ } else
|
|
|
+ RedisplayLine (position);
|
|
|
+ break;
|
|
|
+ case Key.Tab:
|
|
|
+ leftSide = !leftSide;
|
|
|
+ RedisplayLine (position);
|
|
|
+ firstNibble = true;
|
|
|
break;
|
|
|
+
|
|
|
default:
|
|
|
- return false;
|
|
|
+ if (leftSide) {
|
|
|
+ int value = -1;
|
|
|
+ var k = (char)keyEvent.Key;
|
|
|
+ if (k >= 'A' && k <= 'F')
|
|
|
+ value = k - 'A' + 10;
|
|
|
+ else if (k >= 'a' && k <= 'f')
|
|
|
+ value = k - 'a' + 10;
|
|
|
+ else if (k >= '0' && k <= '9')
|
|
|
+ value = k - '0';
|
|
|
+ else
|
|
|
+ return false;
|
|
|
+
|
|
|
+ byte b;
|
|
|
+ if (!edits.TryGetValue (position, out b)) {
|
|
|
+ source.Position = position;
|
|
|
+ b = (byte)source.ReadByte ();
|
|
|
+ }
|
|
|
+ RedisplayLine (position);
|
|
|
+ if (firstNibble) {
|
|
|
+ firstNibble = false;
|
|
|
+ b = (byte)(b & 0xf | (value << 4));
|
|
|
+ edits [position] = b;
|
|
|
+ } else {
|
|
|
+ b = (byte)(b & 0xf0 | value);
|
|
|
+ edits [position] = b;
|
|
|
+ CursorRight ();
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ } else
|
|
|
+ return false;
|
|
|
}
|
|
|
- // TODO: just se the NeedDispay for the affected region, not all
|
|
|
- SetNeedsDisplay ();
|
|
|
PositionCursor ();
|
|
|
- return false;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.HexView"/> allow editing of the contents of the underlying stream.
|
|
|
+ /// </summary>
|
|
|
+ /// <value><c>true</c> if allow edits; otherwise, <c>false</c>.</value>
|
|
|
+ public bool AllowEdits { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets a list of the edits done to the buffer which is a sorted dictionary with the positions where the edit took place and the value that was set.
|
|
|
+ /// </summary>
|
|
|
+ /// <value>The edits.</value>
|
|
|
+ public IReadOnlyDictionary<long,byte> Edits => edits;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// This method applies the edits to the stream and resets the contents of the Edits property
|
|
|
+ /// </summary>
|
|
|
+ public void ApplyEdits ()
|
|
|
+ {
|
|
|
+ foreach (var kv in edits) {
|
|
|
+ source.Position = kv.Key;
|
|
|
+ source.WriteByte (kv.Value);
|
|
|
+ }
|
|
|
+ edits = new SortedDictionary<long, byte> ();
|
|
|
}
|
|
|
}
|
|
|
}
|