1// Copyright 2019-2020 Graham Clark. All rights reserved. Use of this source 2// code is governed by the MIT license that can be found in the LICENSE 3// file. 4 5// Package hexdumper2 provides a widget which displays selectable hexdump-like 6// output. Because it's built for termshark, it also allows styling to be 7// applied to ranges of data intended to correspond to packet structure selected 8// in another termshark view. 9package hexdumper2 10 11import ( 12 "encoding/hex" 13 "fmt" 14 "io" 15 16 "github.com/gcla/gowid" 17 "github.com/gcla/gowid/gwutil" 18 "github.com/gcla/gowid/widgets/list" 19 "github.com/gcla/gowid/widgets/styled" 20 "github.com/gcla/termshark/v2/format" 21 "github.com/gdamore/tcell" 22) 23 24//====================================================================== 25 26type LayerStyler struct { 27 Start int 28 End int 29 ColUnselected string 30 ColSelected string 31} 32 33type PositionChangedCB struct{} 34 35//====================================================================== 36 37type Options struct { 38 StyledLayers []LayerStyler 39 CursorUnselected string 40 CursorSelected string 41 LineNumUnselected string 42 LineNumSelected string 43 PaletteIfCopying string 44} 45 46type Widget struct { 47 data []byte 48 layers []LayerStyler 49 position int 50 cursorUnselected string 51 cursorSelected string 52 lineNumUnselected string 53 lineNumSelected string 54 offset int // scroll position, bit of a hack 55 paletteIfCopying string 56 gowid.AddressProvidesID 57 styled.UsePaletteIfSelectedForCopy 58 Callbacks *gowid.Callbacks 59 gowid.IsSelectable 60} 61 62var _ gowid.IWidget = (*Widget)(nil) 63var _ gowid.IIdentityWidget = (*Widget)(nil) 64 65var _ gowid.IClipboard = (*Widget)(nil) 66var _ gowid.IClipboardSelected = (*Widget)(nil) 67 68func New(data []byte, opts ...Options) *Widget { 69 70 var opt Options 71 if len(opts) > 0 { 72 opt = opts[0] 73 } 74 75 res := &Widget{ 76 data: data, 77 layers: opt.StyledLayers, 78 cursorUnselected: opt.CursorUnselected, 79 cursorSelected: opt.CursorSelected, 80 lineNumUnselected: opt.LineNumUnselected, 81 lineNumSelected: opt.LineNumSelected, 82 paletteIfCopying: opt.PaletteIfCopying, 83 UsePaletteIfSelectedForCopy: styled.UsePaletteIfSelectedForCopy{Entry: opt.PaletteIfCopying}, 84 Callbacks: gowid.NewCallbacks(), 85 } 86 87 return res 88} 89 90func (w *Widget) OnPositionChanged(f gowid.IWidgetChangedCallback) { 91 gowid.AddWidgetCallback(w.Callbacks, PositionChangedCB{}, f) 92} 93 94func (w *Widget) RemoveOnPositionChanged(f gowid.IIdentity) { 95 gowid.RemoveWidgetCallback(w.Callbacks, PositionChangedCB{}, f) 96} 97 98func (w *Widget) String() string { 99 return "hexdump2" 100} 101 102func (w *Widget) CursorUnselected() string { 103 return w.cursorUnselected 104} 105func (w *Widget) CursorSelected() string { 106 return w.cursorSelected 107} 108func (w *Widget) LineNumUnselected() string { 109 return w.lineNumUnselected 110} 111func (w *Widget) LineNumSelected() string { 112 return w.lineNumSelected 113} 114 115func (w *Widget) Layers() []LayerStyler { 116 return w.layers 117} 118 119func (w *Widget) SetLayers(layers []LayerStyler, app gowid.IApp) { 120 w.layers = layers 121} 122 123func (w *Widget) Data() []byte { 124 return w.data 125} 126 127func (w *Widget) SetData(data []byte, app gowid.IApp) { 128 w.data = data 129} 130 131func (w *Widget) Position() int { 132 return w.position 133} 134 135func (w *Widget) SetPosition(pos int, app gowid.IApp) { 136 curpos := w.Position() 137 w.position = pos 138 if curpos != pos { 139 gowid.RunWidgetCallbacks(w.Callbacks, PositionChangedCB{}, app, w) 140 } 141} 142 143func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { 144 // 1<-4><3><----------(8 * 3)-1---<2<-------(8 * 3)-1-----><3><---8-->1<---8--> 145 var cols int 146 if sz, ok := size.(gowid.IColumns); ok { 147 cols = sz.Columns() 148 } else { 149 cols = 1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 + 8 + 1 + 8 150 } 151 152 var rows int 153 if box, ok := size.(gowid.IRows); ok { 154 rows = box.Rows() 155 } else { 156 rows = (len(w.data) + 15) / 16 157 } 158 return gowid.MakeRenderBox(cols, rows) 159} 160 161// 1<-4><3><----------(8 * 3)-1---<2<-------(8 * 3)-1-----><3><---8-->1<---8--> 162// 0660 72 6f 72 73 2e 57 69 74 68 53 74 61 63 6b 28 67 rors.Wit hStack(g 163// 0670 6f 77 69 64 2e 57 69 74 68 4b 56 73 28 4f 70 65 owid.Wit hKVs(Ope 164// 0680 6e 45 72 72 6f 72 2c 20 6d 61 70 5b 73 74 72 69 nError, map[stri 165// 0690 6e 67 5d 69 6e 74 65 72 66 61 63 65 7b 7d 7b 0a ng]inter face{}{. 166// 06a0 09 09 09 09 22 64 65 73 63 72 69 70 74 6f 72 22 ...."des criptor" 167// 06b0 3a 20 6e 65 77 73 74 64 69 6e 2c 0a 09 09 09 09 : newstd in,..... 168// 169func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { 170 var canvasRows int 171 if box, ok := size.(gowid.IRows); ok { 172 canvasRows = box.Rows() 173 } else { 174 canvasRows = (len(w.data) + 15) / 16 175 } 176 177 if canvasRows == 0 { 178 return gowid.NewCanvas() 179 } 180 181 // -1 means not copy mode. 0 means the whole hexdump. 2 means the smallest layer, 1 the next biggest 182 diff := -1 183 if app.InCopyMode() && app.CopyModeClaimedBy().ID() == w.ID() && focus.Focus { 184 diff = app.CopyModeClaimedAt() - app.CopyLevel() 185 } 186 187 cols := 1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 + 8 + 1 + 8 188 ccols := cols 189 if sz, ok := size.(gowid.IColumns); ok { 190 ccols = gwutil.Max(ccols, sz.Columns()) 191 } 192 c := gowid.NewCanvasOfSize(ccols, canvasRows) 193 194 // Adjust in case it's been set too high e.g. via a scrollbar 195 drows := (len(w.data) + 15) / 16 196 if w.offset >= drows { 197 w.offset = drows 198 } 199 rows := gwutil.Min(canvasRows, drows) 200 201 if w.Position() < w.offset*16 { 202 w.SetPosition(w.Position()%16+(w.offset*16), app) 203 } 204 205 if w.Position() >= ((canvasRows + w.offset) * 16) { 206 w.SetPosition(w.Position()%16+((canvasRows+w.offset-1)*16), app) 207 } 208 209 var lineNumStyle convertedStyle 210 var cursorStyle convertedStyle 211 var copyModeStyle convertedStyle 212 213 if focus.Focus { 214 lineNumStyle = convertStyle(gowid.MakePaletteRef(w.LineNumSelected()), app) 215 cursorStyle = convertStyle(gowid.MakePaletteRef(w.CursorSelected()), app) 216 } else { 217 lineNumStyle = convertStyle(gowid.MakePaletteRef(w.LineNumUnselected()), app) 218 cursorStyle = convertStyle(gowid.MakePaletteRef(w.CursorUnselected()), app) 219 } 220 copyModeStyle = convertStyle(gowid.MakePaletteRef(w.Entry), app) 221 222 twoByteWriter := CanvasSlice{ 223 C: c, 224 } 225 226 asciiWriter := CanvasSlice{ 227 C: c, 228 } 229 230 var active *convertedStyle // for styling the hex data "41" and the ascii "A" 231 var spactive *convertedStyle // for styling the spaces between data e.g. "41 42" 232 233 // nil 234 // [1, 5] 235 // [2, 3] 236 // 237 // Deliberately add a blank layer at the beginning for index 0 238 layers := w.Layers() 239 var layer *LayerStyler 240 241 layerStyles := make([]convertedLayer, len(layers)) 242 for i := 0; i < len(layers); i++ { 243 layerStyles[i].u = convertStyle(gowid.MakePaletteRef(layers[i].ColUnselected), app) 244 layerStyles[i].s = convertStyle(gowid.MakePaletteRef(layers[i].ColSelected), app) 245 } 246 247 var i int 248Loop: 249 for row := w.offset; row < rows+w.offset; row++ { 250 twoByteWriter.XOffset = 1 + 4 + 3 251 asciiWriter.XOffset = 1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 252 253 for col := 0; col < 16; col++ { 254 i = (row * 16) + col 255 256 if i == len(w.data) { 257 break Loop 258 } 259 260 active = nil 261 spactive = nil 262 263 if w.Position() == i { 264 if diff != -1 { 265 active = ©ModeStyle 266 } else { 267 active = &cursorStyle 268 } 269 } else { 270 for j := 0; j < len(layers); j++ { 271 layer = &layers[j] 272 if i >= layer.Start && i < layer.End { 273 if j+1 == diff { 274 active = ©ModeStyle 275 break 276 } else { 277 if focus.Focus { 278 active = &layerStyles[j].s 279 } else { 280 active = &layerStyles[j].u 281 } 282 } 283 } 284 } 285 } 286 287 for j := 0; j < len(layers); j++ { 288 layer = &layers[j] 289 if i >= layer.Start && i < layer.End-1 { 290 if j+1 == diff { 291 spactive = ©ModeStyle 292 break 293 } else { 294 if focus.Focus { 295 spactive = &layerStyles[j].s 296 } else { 297 spactive = &layerStyles[j].u 298 } 299 } 300 } 301 } 302 303 fmt.Fprintf(twoByteWriter, "%02x", w.data[i]) 304 305 if active != nil { 306 styleAt(c, twoByteWriter.XOffset, twoByteWriter.YOffset, *active) 307 styleAt(c, twoByteWriter.XOffset+1, twoByteWriter.YOffset, *active) 308 } 309 if spactive != nil { 310 styleAt(c, twoByteWriter.XOffset+2, twoByteWriter.YOffset, *spactive) 311 if col == 7 { 312 styleAt(c, twoByteWriter.XOffset+3, twoByteWriter.YOffset, *spactive) 313 } 314 } 315 316 twoByteWriter.XOffset += 3 317 if col == 7 { 318 twoByteWriter.XOffset += 1 319 } 320 321 ch := '.' 322 r := w.data[i] 323 if r >= 32 && r <= 126 { 324 ch = rune(byte(r)) 325 } 326 327 fmt.Fprintf(asciiWriter, "%c", ch) 328 329 if active != nil { 330 styleAt(c, asciiWriter.XOffset, asciiWriter.YOffset, *active) 331 } 332 if spactive != nil && col == 7 { 333 styleAt(c, asciiWriter.XOffset+1, asciiWriter.YOffset, *spactive) 334 } 335 336 asciiWriter.XOffset += 1 337 if col == 7 { 338 asciiWriter.XOffset += 1 339 } 340 341 } 342 343 twoByteWriter.YOffset += 1 344 asciiWriter.YOffset += 1 345 } 346 347 lineNumWriter := CanvasSlice{ 348 C: c, 349 XOffset: 1, 350 YOffset: 0, 351 } 352 353Loop2: 354 for k, rk := w.offset, 0; k < rows+w.offset; k, rk = k+1, rk+1 { 355 if k*16 >= len(w.data) { 356 break Loop2 357 } 358 359 fmt.Fprintf(lineNumWriter, "%04x", k*16) 360 361 lineNumWriter.YOffset += 1 362 363 active := false 364 for _, layer := range layers { 365 if (k+1)*16 >= layer.Start && k*16 < layer.End { 366 active = true 367 break 368 } 369 } 370 if active { 371 for x := 0; x < 6; x++ { 372 styleAt(c, x, rk, lineNumStyle) 373 } 374 } 375 } 376 377 if sz, ok := size.(gowid.IColumns); ok { 378 c.TrimRight(sz.Columns()) 379 } 380 381 if diff == 0 { 382 gowid.RangeOverCanvas(c, gowid.CellRangeFunc(func(cell gowid.Cell) gowid.Cell { 383 return cell.WithBackgroundColor(copyModeStyle.b).WithForegroundColor(copyModeStyle.f).WithStyle(copyModeStyle.s) 384 })) 385 } 386 387 return c 388} 389 390func clipsForBytes(data []byte, start int, end int) []gowid.ICopyResult { 391 dump := hex.Dump(data[start:end]) 392 dump2 := format.MakeEscapedString(data[start:end]) 393 dump3 := format.MakePrintableString(data[start:end]) 394 dump4 := format.MakeHexStream(data[start:end]) 395 396 return []gowid.ICopyResult{ 397 gowid.CopyResult{ 398 Name: fmt.Sprintf("Copy bytes %d-%d as hex + ascii", start, end), 399 Val: dump, 400 }, 401 gowid.CopyResult{ 402 Name: fmt.Sprintf("Copy bytes %d-%d as escaped string", start, end), 403 Val: dump2, 404 }, 405 gowid.CopyResult{ 406 Name: fmt.Sprintf("Copy bytes %d-%d as printable string", start, end), 407 Val: dump3, 408 }, 409 gowid.CopyResult{ 410 Name: fmt.Sprintf("Copy bytes %d-%d as hex stream", start, end), 411 Val: dump4, 412 }, 413 } 414} 415 416func (w *Widget) CopyModeLevels() int { 417 return len(w.layers) 418} 419 420func (w *Widget) Clips(app gowid.IApp) []gowid.ICopyResult { 421 422 diff := app.CopyModeClaimedAt() - app.CopyLevel() 423 if diff == 0 { 424 return clipsForBytes(w.Data(), 0, len(w.Data())) 425 } else { 426 return clipsForBytes(w.Data(), w.layers[diff-1].Start, w.layers[diff-1].End) 427 } 428} 429 430func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 431 return gowid.CopyModeUserInput(forCopyModeWidget{forUserInputWidget{Widget: w}}, ev, size, focus, app) 432} 433 434type forCopyModeWidget struct { 435 forUserInputWidget 436} 437 438// CopyModeUserInput calls UserInput() on w.SubWidget() - which is this. Then... 439func (w forCopyModeWidget) SubWidget() gowid.IWidget { 440 return w.forUserInputWidget 441} 442 443type forUserInputWidget struct { 444 *Widget 445} 446 447// ... UserInput is sent to the hexdumper's UserInput logic. 448func (w forUserInputWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 449 return w.Widget.realUserInput(ev, size, focus, app) 450} 451 452// Reject tab because I want it to switch views. Not intended to be transferable. 453func (w *Widget) realUserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 454 res := false 455 456 scrollDown := false 457 scrollUp := false 458 459 switch ev := ev.(type) { 460 case *tcell.EventKey: 461 462 switch ev.Key() { 463 case tcell.KeyRight, tcell.KeyCtrlF: 464 //res = Scroll(w, 1, w.Wrap(), app) 465 pos := w.Position() 466 if pos < len(w.data) { 467 w.SetPosition(pos+1, app) 468 res = true 469 } 470 case tcell.KeyLeft, tcell.KeyCtrlB: 471 pos := w.Position() 472 if pos > 0 { 473 w.SetPosition(pos-1, app) 474 res = true 475 } 476 case tcell.KeyDown, tcell.KeyCtrlN: 477 scrollDown = true 478 case tcell.KeyUp, tcell.KeyCtrlP: 479 scrollUp = true 480 } 481 482 case *tcell.EventMouse: 483 switch ev.Buttons() { 484 case tcell.WheelDown: 485 scrollDown = true 486 case tcell.WheelUp: 487 scrollUp = true 488 case tcell.Button1: 489 xp := -1 490 mx, my := ev.Position() 491 // 1<-4><3><----------(8 * 3)-1---<2<-------(8 * 3)-1-----><3><---8-->1<---8--> 492 // 0660 72 6f 72 73 2e 57 69 74 68 53 74 61 63 6b 28 67 rors.Wit hStack(g 493 // 0670 6f 77 69 64 2e 57 69 74 68 4b 56 73 28 4f 70 65 owid.Wit hKVs(Ope 494 switch { 495 case mx >= 1+4+3 && mx < 1+4+3+((8*3)-1): 496 xp = (mx - (1 + 4 + 3)) / 3 497 case mx >= 1+4+3+((8*3)-1)+2 && mx < 1+4+3+((8*3)-1)+2+((8*3)-1): 498 xp = ((mx - (1 + 4 + 3 + ((8 * 3) - 1) + 2)) / 3) + 8 499 case mx >= 1+4+3+((8*3)-1)+2+((8*3)-1)+3 && mx < 1+4+3+((8*3)-1)+2+((8*3)-1)+3+8: 500 xp = mx - (1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3) 501 case mx >= 1+4+3+((8*3)-1)+2+((8*3)-1)+3+8+1 && mx < 1+4+3+((8*3)-1)+2+((8*3)-1)+3+8+1+8: 502 xp = mx - (1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 + 8 + 1) + 8 503 } 504 if xp != -1 { 505 pos := (my * 16) + xp 506 if pos < len(w.data) { 507 w.SetPosition(pos, app) 508 res = true 509 } 510 } 511 512 } 513 } 514 515 pos := w.Position() 516 atBottom := false 517 atTop := false 518 519 var canvasRows int 520 if box, ok := size.(gowid.IRows); ok { 521 canvasRows = box.Rows() 522 } else { 523 canvasRows = (len(w.data) + 15) / 16 524 } 525 526 atTop = ((pos / 16) == w.offset) 527 atBottom = ((pos / 16) == (w.offset + canvasRows - 1)) 528 529 if scrollDown { 530 if pos+16 < len(w.data) { 531 w.SetPosition(pos+16, app) 532 if atBottom { 533 w.offset += 1 534 } 535 res = true 536 } 537 } else if scrollUp { 538 if pos-16 >= 0 { 539 w.SetPosition(pos-16, app) 540 if atTop { 541 w.offset -= 1 542 } 543 res = true 544 } 545 } 546 547 return res 548} 549 550// Implement withscrollbar.IScrollValues 551func (t *Widget) ScrollLength() int { 552 return (len(t.data) + 15) / 16 553} 554 555// Implement withscrollbar.IScrollValues 556func (t *Widget) ScrollPosition() int { 557 return t.Position() / 16 558} 559 560// Implements withscrollbar.iSetPosition. Note that position is a row. 561func (t *Widget) SetPos(pos list.IBoundedWalkerPosition, app gowid.IApp) { 562 t.offset = pos.ToInt() 563} 564 565// Can leave the cursor out of sight 566func (t *Widget) Up(lines int, size gowid.IRenderSize, app gowid.IApp) { 567 t.offset -= lines 568 if t.offset < 0 { 569 t.offset = 0 570 } 571} 572 573// Adjusts cursor pos too 574func (t *Widget) GoHome(size gowid.IRenderSize, app gowid.IApp) { 575 t.offset = 0 576 t.SetPosition(0, app) 577} 578 579func (t *Widget) GoToEnd(size gowid.IRenderSize, app gowid.IApp) { 580 var canvasRows int 581 if box, ok := size.(gowid.IRows); ok { 582 canvasRows = box.Rows() 583 } else { 584 canvasRows = (len(t.data) + 15) / 16 585 } 586 587 dataRows := (len(t.data) + 15) / 16 588 t.offset += gwutil.Max(0, dataRows-(canvasRows+t.offset)) 589 590 t.SetPosition(len(t.data)-1, app) 591} 592 593// Can leave the cursor out of sight 594func (t *Widget) Down(lines int, size gowid.IRenderSize, app gowid.IApp) { 595 var canvasRows int 596 if box, ok := size.(gowid.IRows); ok { 597 canvasRows = box.Rows() 598 } else { 599 canvasRows = (len(t.data) + 15) / 16 600 } 601 602 dataRows := (len(t.data) + 15) / 16 603 t.offset += gwutil.Max(0, gwutil.Min(lines, dataRows-(canvasRows+t.offset))) 604} 605 606func (t *Widget) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) { 607 units := 1 608 if size, ok := size.(gowid.IRows); ok { 609 units = size.Rows() 610 } 611 612 for i := 0; i < num; i++ { 613 t.Down(units, size, app) 614 } 615} 616 617func (t *Widget) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) { 618 units := 1 619 if size, ok := size.(gowid.IRows); ok { 620 units = size.Rows() 621 } 622 623 for i := 0; i < num; i++ { 624 t.Up(units, size, app) 625 } 626} 627 628//====================================================================== 629 630// Optimization - convert the styles for use in the canvas once per call 631// to Render() 632type convertedStyle struct { 633 f gowid.TCellColor 634 b gowid.TCellColor 635 s gowid.StyleAttrs 636} 637 638type convertedLayer struct { 639 u convertedStyle 640 s convertedStyle 641} 642 643func convertStyle(style gowid.ICellStyler, app gowid.IApp) convertedStyle { 644 f, b, s := style.GetStyle(app) 645 f1 := gowid.IColorToTCell(f, gowid.ColorNone, app.GetColorMode()) 646 b1 := gowid.IColorToTCell(b, gowid.ColorNone, app.GetColorMode()) 647 return convertedStyle{ 648 f: f1, 649 b: b1, 650 s: s, 651 } 652} 653 654//====================================================================== 655 656type CanvasSlice struct { 657 C *gowid.Canvas 658 XOffset int 659 YOffset int 660} 661 662var _ io.Writer = CanvasSlice{} 663var _ gowid.IRangeOverCanvas = CanvasSlice{} 664 665func NewCanvasSlice(c *gowid.Canvas, xoff, yoff int) CanvasSlice { 666 res := CanvasSlice{ 667 C: c, 668 XOffset: xoff, 669 YOffset: yoff, 670 } 671 return res 672} 673 674func (c CanvasSlice) Write(p []byte) (n int, err error) { 675 return gowid.WriteToCanvas(c, p) 676} 677 678func (c CanvasSlice) ImplementsWidgetDimension() {} 679 680func (c CanvasSlice) BoxColumns() int { 681 return c.C.BoxColumns() 682} 683 684func (c CanvasSlice) BoxRows() int { 685 return c.C.BoxRows() 686} 687 688func (c CanvasSlice) CellAt(col, row int) gowid.Cell { 689 return c.C.CellAt(col+c.XOffset, row+c.YOffset) 690} 691 692func (c CanvasSlice) SetCellAt(col, row int, cell gowid.Cell) { 693 c.C.SetCellAt(col+c.XOffset, row+c.YOffset, cell) 694} 695 696func styleAt(canvas gowid.ICanvas, col int, row int, st convertedStyle) { 697 c := canvas.CellAt(col, row) 698 c = c.MergeDisplayAttrsUnder(c.WithForegroundColor(st.f).WithBackgroundColor(st.b).WithStyle(st.s)) 699 canvas.SetCellAt(col, row, c) 700} 701 702//====================================================================== 703// Local Variables: 704// mode: Go 705// fill-column: 110 706// End: 707