// // HexView.cs: A hexadecimal viewer // // TODO: // - Support searching and highlighting of the search result // - Bug showing the last line // using System; using System.Collections.Generic; using System.IO; namespace Terminal.Gui { /// /// An hex viewer and editor over a /// /// /// /// provides a hex editor on top of a seekable with the left side showing an hex /// dump of the values in the and the right side showing the contents (filtered to /// non-control sequence ASCII characters). /// /// /// Users can switch from one side to the other by using the tab key. /// /// /// To enable editing, set to true. When is true /// the user can make changes to the hexadecimal values of the . Any changes are tracked /// in the property (a ) indicating /// the position where the changes were made and the new values. A convenience method, /// will apply the edits to the . /// /// /// Control the first byte shown by setting the property /// to an offset in the stream. /// /// public class HexView : View { SortedDictionary edits = new SortedDictionary (); Stream source; long displayStart, position; bool firstNibble, leftSide; /// /// Initialzies a class using layout. /// /// The to view and edit as hex, this must support seeking, or an exception will be thrown. public HexView (Stream source) : base () { Source = source; this.source = source; CanFocus = true; leftSide = true; firstNibble = true; } /// /// Initialzies a class using layout. /// public HexView () : this (source: new MemoryStream ()) { } /// /// Sets or gets the the is operating on; the stream must support seeking ( == true). /// /// The source. public Stream Source { get => source; set { if (value == null) throw new ArgumentNullException ("source"); if (!value.CanSeek) throw new ArgumentException ("The source stream must be seekable (CanSeek property)", "source"); source = value; SetNeedsDisplay (); } } internal void SetDisplayStart (long value) { if (value >= source.Length) displayStart = source.Length - 1; else if (value < 0) displayStart = 0; else displayStart = value; SetNeedsDisplay (); } /// /// Sets or gets the offset into the that will displayed at the top of the /// /// The display start. public long DisplayStart { get => displayStart; set { position = value; SetDisplayStart (value); } } const int displayWidth = 9; const int bsize = 4; int bytesPerLine; /// public override Rect Frame { get => base.Frame; set { base.Frame = value; // Small buffers will just show the position, with 4 bytes bytesPerLine = 4; if (value.Width - displayWidth > 17) bytesPerLine = 4 * ((value.Width - displayWidth) / 18); } } // // 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 bounds) { Attribute currentAttribute; var current = ColorScheme.Focus; Driver.SetAttribute (current); Move (0, 0); var frame = Frame; var nblocks = bytesPerLine / 4; var data = new byte [nblocks * 4 * frame.Height]; 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 (!bounds.Contains (lineRect)) continue; Move (0, line); Driver.SetAttribute (ColorScheme.HotNormal); Driver.AddStr (string.Format ("{0:x8} ", displayStart + line * nblocks * 4)); currentAttribute = ColorScheme.HotNormal; SetAttribute (ColorScheme.Normal); for (int block = 0; block < nblocks; block++) { for (int b = 0; b < 4; b++) { var offset = (line * nblocks * 4) + block * 4 + b; 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}", 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; bool edited = false; Rune c = ' '; if (offset >= n) c = ' '; else { var b = GetData (data, offset, out edited); if (b < 32) c = '.'; else if (b > 127) c = '.'; else c = b; } if (offset + displayStart == position || edited) SetAttribute (leftSide ? trackingColor : activeColor); else SetAttribute (ColorScheme.Normal); Driver.AddRune (c); } } void SetAttribute (Attribute attribute) { if (currentAttribute != attribute) { currentAttribute = attribute; Driver.SetAttribute (attribute); } } } /// public override void PositionCursor () { var delta = (int)(position - displayStart); var line = delta / bytesPerLine; var item = delta % bytesPerLine; var block = item / 4; var column = (item % 4) * 3; 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); } void MoveUp (int bytes) { RedisplayLine (position); position -= bytes; if (position < 0) position = 0; if (position < DisplayStart) { SetDisplayStart (DisplayStart - bytes); SetNeedsDisplay (); } else RedisplayLine (position); } void MoveDown (int bytes) { RedisplayLine (position); if (position + bytes < source.Length) position += bytes; if (position >= (DisplayStart + bytesPerLine * Frame.Height)) { SetDisplayStart (DisplayStart + bytes); 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: CursorRight (); break; case Key.CursorDown: MoveDown (bytesPerLine); break; case Key.CursorUp: MoveUp (bytesPerLine); break; case Key.Enter: leftSide = !leftSide; RedisplayLine (position); firstNibble = true; break; case ((int)'v' + Key.AltMask): case Key.PageUp: MoveUp (bytesPerLine * Frame.Height); break; case Key.V | Key.CtrlMask: case Key.PageDown: MoveDown (bytesPerLine * Frame.Height); break; case Key.Home: DisplayStart = 0; SetNeedsDisplay (); break; default: 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; } PositionCursor (); return true; } /// /// Gets or sets whether this allow editing of the /// of the underlying . /// /// true if allow edits; otherwise, false. public bool AllowEdits { get; set; } /// /// Gets a describing the edits done to the . /// Each Key indicates an offset where an edit was made and the Value is the changed byte. /// /// The edits. public IReadOnlyDictionary Edits => edits; /// /// This method applies andy edits made to the and resets the /// contents of the property /// public void ApplyEdits () { foreach (var kv in edits) { source.Position = kv.Key; source.WriteByte (kv.Value); } edits = new SortedDictionary (); } } }