Projects STRLCPY termdash Commits 9ed37136
🤬
  • Release Termdash v0.16.0. (#302)

    ## v0.16.0 - 03-Apr-2021
    
    ### Added
    
    - The `Text` widget has a new option `MaxTextCells` which can be used to limit
      the maximum number of cells the widget keeps in memory.
    
    ### Changed
    
    - Bump github.com/mattn/go-runewidth from 0.0.10 to 0.0.12.
  • Loading...
  • Jakub Sobon committed with GitHub 3 years ago
    9ed37136
    1 parent fc131ea4
  • ■ ■ ■ ■ ■ ■
    CHANGELOG.md
    skipped 6 lines
    7 7   
    8 8  ## [Unreleased]
    9 9   
     10 +## [0.16.0] - 03-Apr-2021
     11 + 
     12 +### Added
     13 + 
     14 +- The `Text` widget has a new option `MaxTextCells` which can be used to limit
     15 + the maximum number of cells the widget keeps in memory.
     16 + 
     17 +### Changed
     18 + 
     19 +- Bump github.com/mattn/go-runewidth from 0.0.10 to 0.0.12.
     20 + 
    10 21  ## [0.15.0] - 06-Mar-2021
    11 22   
    12 23  ### Changed
    skipped 440 lines
    453 464  - The Gauge widget.
    454 465  - The Text widget.
    455 466   
    456  -[unreleased]: https://github.com/mum4k/termdash/compare/v0.15.0...devel
     467 +[unreleased]: https://github.com/mum4k/termdash/compare/v0.16.0...devel
     468 +[0.16.0]: https://github.com/mum4k/termdash/compare/v0.15.0...v0.16.0
    457 469  [0.15.0]: https://github.com/mum4k/termdash/compare/v0.14.0...v0.15.0
    458 470  [0.14.0]: https://github.com/mum4k/termdash/compare/v0.13.0...v0.14.0
    459 471  [0.13.0]: https://github.com/mum4k/termdash/compare/v0.12.2...v0.13.0
    skipped 18 lines
  • ■ ■ ■ ■
    go.mod
    skipped 4 lines
    5 5  require (
    6 6   github.com/gdamore/tcell/v2 v2.2.0
    7 7   github.com/kylelemons/godebug v1.1.0
    8  - github.com/mattn/go-runewidth v0.0.10
     8 + github.com/mattn/go-runewidth v0.0.12
    9 9   github.com/nsf/termbox-go v0.0.0-20201107200903-9b52a5faed9e
    10 10   golang.org/x/text v0.3.4 // indirect
    11 11  )
    skipped 1 lines
  • ■ ■ ■ ■ ■
    go.sum
    skipped 5 lines
    6 6  github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
    7 7  github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
    8 8  github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
    9  -github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
     9 +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
    10 10  github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
     11 +github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
     12 +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
    11 13  github.com/nsf/termbox-go v0.0.0-20201107200903-9b52a5faed9e h1:T8/SzSWIDoWV9trslLNfUdJ5yHrIXXuODEy5M0vou4U=
    12 14  github.com/nsf/termbox-go v0.0.0-20201107200903-9b52a5faed9e/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
    13 15  github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
    skipped 10 lines
  • ■ ■ ■ ■ ■ ■
    private/runewidth/runewidth.go
    skipped 17 lines
    18 18   
    19 19  import runewidth "github.com/mattn/go-runewidth"
    20 20   
     21 +// Option is used to provide options.
     22 +type Option interface {
     23 + // set sets the provided option.
     24 + set(*options)
     25 +}
     26 + 
     27 +// options stores the provided options.
     28 +type options struct {
     29 + runeWidths map[rune]int
     30 +}
     31 + 
     32 +// newOptions create a new instance of options.
     33 +func newOptions() *options {
     34 + return &options{
     35 + runeWidths: map[rune]int{},
     36 + }
     37 +}
     38 + 
     39 +// option implements Option.
     40 +type option func(*options)
     41 + 
     42 +// set implements Option.set.
     43 +func (o option) set(opts *options) {
     44 + o(opts)
     45 +}
     46 + 
     47 +// CountAsWidth overrides the default behavior, counting the specified runes as
     48 +// the specified width. Can be provided multiple times to specify an override
     49 +// for multiple runes.
     50 +func CountAsWidth(r rune, width int) Option {
     51 + return option(func(opts *options) {
     52 + opts.runeWidths[r] = width
     53 + })
     54 +}
     55 + 
    21 56  // RuneWidth returns the number of cells needed to draw r.
    22 57  // Background in http://www.unicode.org/reports/tr11/.
    23 58  //
    skipped 5 lines
    29 64  // This should be safe, since even in locales where these runes have ambiguous
    30 65  // width, we still place all the character content around them so they should
    31 66  // have be half-width.
    32  -func RuneWidth(r rune) int {
     67 +func RuneWidth(r rune, opts ...Option) int {
     68 + o := newOptions()
     69 + for _, opt := range opts {
     70 + opt.set(o)
     71 + }
     72 + 
     73 + if w, ok := o.runeWidths[r]; ok {
     74 + return w
     75 + }
     76 + 
    33 77   if inTable(r, exceptions) {
    34 78   return 1
    35 79   }
    skipped 2 lines
    38 82   
    39 83  // StringWidth is like RuneWidth, but returns the number of cells occupied by
    40 84  // all the runes in the string.
    41  -func StringWidth(s string) int {
     85 +func StringWidth(s string, opts ...Option) int {
    42 86   var width int
    43 87   for _, r := range []rune(s) {
    44  - width += RuneWidth(r)
     88 + width += RuneWidth(r, opts...)
    45 89   }
    46 90   return width
    47 91  }
    skipped 52 lines
  • ■ ■ ■ ■ ■
    private/runewidth/runewidth_test.go
    skipped 23 lines
    24 24   tests := []struct {
    25 25   desc string
    26 26   runes []rune
     27 + opts []Option
    27 28   eastAsian bool
    28 29   want int
    29 30   }{
    skipped 4 lines
    34 35   },
    35 36   {
    36 37   desc: "non-printable characters from mattn/runewidth/runewidth_test",
    37  - runes: []rune{'\x00', '\x01', '\u0300', '\u2028', '\u2029'},
     38 + runes: []rune{'\x00', '\x01', '\u0300', '\u2028', '\u2029', '\n'},
    38 39   want: 0,
     40 + },
     41 + {
     42 + desc: "override rune width with an option",
     43 + runes: []rune{'\n'},
     44 + opts: []Option{
     45 + CountAsWidth('\n', 3),
     46 + },
     47 + want: 3,
    39 48   },
    40 49   {
    41 50   desc: "half-width runes from mattn/runewidth/runewidth_test",
    skipped 65 lines
    107 116   }()
    108 117   
    109 118   for _, r := range tc.runes {
    110  - if got := RuneWidth(r); got != tc.want {
     119 + if got := RuneWidth(r, tc.opts...); got != tc.want {
    111 120   t.Errorf("RuneWidth(%c, %#x) => %v, want %v", r, r, got, tc.want)
    112 121   }
    113 122   }
    skipped 5 lines
    119 128   tests := []struct {
    120 129   desc string
    121 130   str string
     131 + opts []Option
    122 132   eastAsian bool
    123 133   want int
    124 134   }{
    skipped 1 lines
    126 136   desc: "ascii characters",
    127 137   str: "hello",
    128 138   want: 5,
     139 + },
     140 + {
     141 + desc: "override rune widths with an option",
     142 + str: "hello",
     143 + opts: []Option{
     144 + CountAsWidth('h', 5),
     145 + CountAsWidth('e', 5),
     146 + },
     147 + want: 13,
    129 148   },
    130 149   {
    131 150   desc: "string from mattn/runewidth/runewidth_test",
    skipped 26 lines
    158 177   runewidth.DefaultCondition.EastAsianWidth = false
    159 178   }()
    160 179   
    161  - if got := StringWidth(tc.str); got != tc.want {
     180 + if got := StringWidth(tc.str, tc.opts...); got != tc.want {
    162 181   t.Errorf("StringWidth(%q) => %v, want %v", tc.str, got, tc.want)
    163 182   }
    164 183   })
    skipped 3 lines
  • ■ ■ ■ ■ ■ ■
    widgets/text/options.go
    skipped 35 lines
    36 36   scrollDown rune
    37 37   wrapMode wrap.Mode
    38 38   rollContent bool
     39 + maxTextCells int
    39 40   disableScrolling bool
    40 41   mouseUpButton mouse.Button
    41 42   mouseDownButton mouse.Button
    skipped 14 lines
    56 57   keyDown: DefaultScrollKeyDown,
    57 58   keyPgUp: DefaultScrollKeyPageUp,
    58 59   keyPgDown: DefaultScrollKeyPageDown,
     60 + maxTextCells: DefaultMaxTextCells,
    59 61   }
    60 62   for _, o := range opts {
    61 63   o.set(opt)
    skipped 14 lines
    76 78   }
    77 79   if o.mouseUpButton == o.mouseDownButton {
    78 80   return fmt.Errorf("invalid ScrollMouseButtons(up:%v, down:%v), the buttons must be unique", o.mouseUpButton, o.mouseDownButton)
     81 + }
     82 + if o.maxTextCells < 0 {
     83 + return fmt.Errorf("invalid MaxTextCells(%d), must be zero or a positive integer", o.maxTextCells)
    79 84   }
    80 85   return nil
    81 86  }
    skipped 93 lines
    175 180   })
    176 181  }
    177 182   
     183 +// The default value for the MaxTextCells option.
     184 +// Use zero as no limit, for logs you may wish to try 10,000 or higher.
     185 +const (
     186 + DefaultMaxTextCells = 0
     187 +)
     188 + 
     189 +// MaxTextCells limits the text content to this number of terminal cells.
     190 +// This is useful when sending large amounts of text to the Text widget, e.g.
     191 +// when tailing logs as it will limit the memory usage.
     192 +// When the newly added content goes over this number of cells, the Text widget
     193 +// behaves as a circular buffer and drops earlier content to accommodate the
     194 +// new one.
     195 +// Note the count is in cells, not runes, some wide runes can take multiple
     196 +// terminal cells.
     197 +func MaxTextCells(max int) Option {
     198 + return option(func(opts *options) {
     199 + opts.maxTextCells = max
     200 + })
     201 +}
     202 + 
  • ■ ■ ■ ■ ■ ■
    widgets/text/text.go
    skipped 17 lines
    18 18  import (
    19 19   "fmt"
    20 20   "image"
     21 + "strings"
    21 22   "sync"
    22 23   
    23 24   "github.com/mum4k/termdash/private/canvas"
    24 25   "github.com/mum4k/termdash/private/canvas/buffer"
     26 + "github.com/mum4k/termdash/private/runewidth"
    25 27   "github.com/mum4k/termdash/private/wrap"
    26 28   "github.com/mum4k/termdash/terminal/terminalapi"
    27 29   "github.com/mum4k/termdash/widgetapi"
    skipped 62 lines
    90 92   t.contentChanged = true
    91 93  }
    92 94   
     95 +// contentCells calculates the number of cells the content takes to display on
     96 +// terminal.
     97 +func (t *Text) contentCells() int {
     98 + cells := 0
     99 + for _, c := range t.content {
     100 + cells += runewidth.RuneWidth(c.Rune, runewidth.CountAsWidth('\n', 1))
     101 + }
     102 + return cells
     103 +}
     104 + 
    93 105  // Write writes text for the widget to display. Multiple calls append
    94 106  // additional text. The text contain cannot control characters
    95 107  // (unicode.IsControl) or space character (unicode.IsSpace) other than:
    skipped 12 lines
    108 120   if opts.replace {
    109 121   t.reset()
    110 122   }
    111  - for _, r := range text {
     123 + 
     124 + truncated := truncateToCells(text, t.opts.maxTextCells)
     125 + textCells := runewidth.StringWidth(truncated, runewidth.CountAsWidth('\n', 1))
     126 + contentCells := t.contentCells()
     127 + // If MaxTextCells has been set, limit the content if needed.
     128 + if t.opts.maxTextCells > 0 && contentCells+textCells > t.opts.maxTextCells {
     129 + diff := contentCells + textCells - t.opts.maxTextCells
     130 + t.content = t.content[diff:]
     131 + }
     132 + 
     133 + for _, r := range truncated {
    112 134   t.content = append(t.content, buffer.NewCell(r, opts.cellOpts))
    113 135   }
    114 136   t.contentChanged = true
    skipped 170 lines
    285 307   }
    286 308  }
    287 309   
     310 +// truncateToCells truncates the beginning of text, so that it can be displayed
     311 +// in at most maxCells. Setting maxCells to zero disables truncating.
     312 +func truncateToCells(text string, maxCells int) string {
     313 + textCells := runewidth.StringWidth(text, runewidth.CountAsWidth('\n', 1))
     314 + if maxCells == 0 || textCells <= maxCells {
     315 + return text
     316 + }
     317 + 
     318 + haveCells := 0
     319 + textRunes := []rune(text)
     320 + i := len(textRunes) - 1
     321 + for ; i >= 0; i-- {
     322 + haveCells += runewidth.RuneWidth(textRunes[i], runewidth.CountAsWidth('\n', 1))
     323 + if haveCells > maxCells {
     324 + break
     325 + }
     326 + }
     327 + 
     328 + var b strings.Builder
     329 + for j := i + 1; j < len(textRunes); j++ {
     330 + b.WriteRune(textRunes[j])
     331 + }
     332 + return b.String()
     333 +}
     334 + 
  • ■ ■ ■ ■ ■ ■
    widgets/text/text_test.go
    skipped 54 lines
    55 55   wantErr: true,
    56 56   },
    57 57   {
     58 + desc: "fails when MaxTextCells is negative",
     59 + opts: []Option{
     60 + MaxTextCells(-1),
     61 + },
     62 + canvas: image.Rect(0, 0, 1, 1),
     63 + want: func(size image.Point) *faketerm.Terminal {
     64 + return faketerm.MustNew(size)
     65 + },
     66 + wantErr: true,
     67 + },
     68 + {
    58 69   desc: "fails when scroll mouse buttons aren't unique",
    59 70   opts: []Option{
    60 71   ScrollMouseButtons(mouse.ButtonLeft, mouse.ButtonLeft),
    skipped 748 lines
    809 820   return ft
    810 821   },
    811 822   },
     823 + {
     824 + desc: "tests maxTextCells length being applied - multiline",
     825 + canvas: image.Rect(0, 0, 10, 3),
     826 + opts: []Option{
     827 + MaxTextCells(10),
     828 + RollContent(),
     829 + },
     830 + writes: func(widget *Text) error {
     831 + return widget.Write("line0\nline1\nline2\nline3\nline4")
     832 + },
     833 + want: func(size image.Point) *faketerm.Terminal {
     834 + ft := faketerm.MustNew(size)
     835 + c := testcanvas.MustNew(ft.Area())
     836 + // \n still counts as a chacter in the string length
     837 + testdraw.MustText(c, "ine3", image.Point{0, 0})
     838 + testdraw.MustText(c, "line4", image.Point{0, 1})
     839 + testcanvas.MustApply(c, ft)
     840 + return ft
     841 + },
     842 + },
     843 + {
     844 + desc: "tests maxTextCells - multiple writes - first one fits",
     845 + canvas: image.Rect(0, 0, 10, 3),
     846 + opts: []Option{
     847 + MaxTextCells(10),
     848 + RollContent(),
     849 + },
     850 + writes: func(widget *Text) error {
     851 + if err := widget.Write("line0\nline"); err != nil {
     852 + return err
     853 + }
     854 + return widget.Write("1\nline2\nline3\nline4")
     855 + },
     856 + want: func(size image.Point) *faketerm.Terminal {
     857 + ft := faketerm.MustNew(size)
     858 + c := testcanvas.MustNew(ft.Area())
     859 + // \n still counts as a chacter in the string length
     860 + testdraw.MustText(c, "ine3", image.Point{0, 0})
     861 + testdraw.MustText(c, "line4", image.Point{0, 1})
     862 + testcanvas.MustApply(c, ft)
     863 + return ft
     864 + },
     865 + },
     866 + {
     867 + desc: "tests maxTextCells - multiple writes - first one does not fit",
     868 + canvas: image.Rect(0, 0, 10, 3),
     869 + opts: []Option{
     870 + MaxTextCells(10),
     871 + RollContent(),
     872 + },
     873 + writes: func(widget *Text) error {
     874 + if err := widget.Write("line0\nline123"); err != nil {
     875 + return err
     876 + }
     877 + return widget.Write("1\nline2\nline3\nline4")
     878 + },
     879 + want: func(size image.Point) *faketerm.Terminal {
     880 + ft := faketerm.MustNew(size)
     881 + c := testcanvas.MustNew(ft.Area())
     882 + testdraw.MustText(c, "ine3", image.Point{0, 0})
     883 + testdraw.MustText(c, "line4", image.Point{0, 1})
     884 + testcanvas.MustApply(c, ft)
     885 + return ft
     886 + },
     887 + },
     888 + {
     889 + desc: "tests maxTextCells - accounts for pre-existing full-width runes on the content",
     890 + canvas: image.Rect(0, 0, 10, 3),
     891 + opts: []Option{
     892 + MaxTextCells(3),
     893 + RollContent(),
     894 + },
     895 + writes: func(widget *Text) error {
     896 + if err := widget.Write("界"); err != nil {
     897 + return err
     898 + }
     899 + return widget.Write("ab")
     900 + },
     901 + want: func(size image.Point) *faketerm.Terminal {
     902 + ft := faketerm.MustNew(size)
     903 + c := testcanvas.MustNew(ft.Area())
     904 + testdraw.MustText(c, "ab", image.Point{0, 0})
     905 + testcanvas.MustApply(c, ft)
     906 + return ft
     907 + },
     908 + },
     909 + {
     910 + desc: "tests maxTextCells exact length of 5",
     911 + canvas: image.Rect(0, 0, 10, 1),
     912 + opts: []Option{
     913 + RollContent(),
     914 + MaxTextCells(5),
     915 + },
     916 + writes: func(widget *Text) error {
     917 + return widget.Write("12345")
     918 + },
     919 + want: func(size image.Point) *faketerm.Terminal {
     920 + ft := faketerm.MustNew(size)
     921 + c := testcanvas.MustNew(ft.Area())
     922 + // Line return (\n) counts as one character
     923 + testdraw.MustText(
     924 + c,
     925 + "12345",
     926 + image.Point{0, 0},
     927 + )
     928 + testcanvas.MustApply(c, ft)
     929 + return ft
     930 + },
     931 + },
     932 + {
     933 + desc: "tests maxTextCells partial bufffer replacement",
     934 + canvas: image.Rect(0, 0, 10, 1),
     935 + opts: []Option{
     936 + RollContent(),
     937 + MaxTextCells(10),
     938 + },
     939 + writes: func(widget *Text) error {
     940 + return widget.Write("hello wor你12345678")
     941 + },
     942 + want: func(size image.Point) *faketerm.Terminal {
     943 + ft := faketerm.MustNew(size)
     944 + c := testcanvas.MustNew(ft.Area())
     945 + testdraw.MustText(
     946 + c,
     947 + "你12345678",
     948 + image.Point{0, 0},
     949 + )
     950 + testcanvas.MustApply(c, ft)
     951 + return ft
     952 + },
     953 + },
     954 + {
     955 + desc: "tests maxTextCells length not being limited",
     956 + canvas: image.Rect(0, 0, 72, 1),
     957 + opts: []Option{
     958 + RollContent(),
     959 + },
     960 + writes: func(widget *Text) error {
     961 + return widget.Write("1234567890abcdefghijklmnopqrstuvwxyz")
     962 + },
     963 + want: func(size image.Point) *faketerm.Terminal {
     964 + ft := faketerm.MustNew(size)
     965 + c := testcanvas.MustNew(ft.Area())
     966 + testdraw.MustText(
     967 + c,
     968 + "1234567890abcdefghijklmnopqrstuvwxyz",
     969 + image.Point{0, 0},
     970 + )
     971 + testcanvas.MustApply(c, ft)
     972 + return ft
     973 + },
     974 + },
     975 + {
     976 + desc: "tests maxTextCells length being applied - single line",
     977 + canvas: image.Rect(0, 0, 10, 3),
     978 + opts: []Option{
     979 + MaxTextCells(5),
     980 + RollContent(),
     981 + },
     982 + writes: func(widget *Text) error {
     983 + return widget.Write("1234567890abcdefghijklmnopqrstuvwxyz")
     984 + },
     985 + want: func(size image.Point) *faketerm.Terminal {
     986 + ft := faketerm.MustNew(size)
     987 + c := testcanvas.MustNew(ft.Area())
     988 + testdraw.MustText(c, "vwxyz", image.Point{0, 0})
     989 + testcanvas.MustApply(c, ft)
     990 + return ft
     991 + },
     992 + },
    812 993   }
    813 994   
    814 995   for _, tc := range tests {
    skipped 87 lines
    902 1083   }
    903 1084  }
    904 1085   
     1086 +func TestTruncateToCells(t *testing.T) {
     1087 + tests := []struct {
     1088 + desc string
     1089 + text string
     1090 + maxCells int
     1091 + want string
     1092 + }{
     1093 + {
     1094 + desc: "returns empty on empty text",
     1095 + text: "",
     1096 + maxCells: 0,
     1097 + want: "",
     1098 + },
     1099 + {
     1100 + desc: "no need to truncate, length matches max",
     1101 + text: "a",
     1102 + maxCells: 1,
     1103 + want: "a",
     1104 + },
     1105 + {
     1106 + desc: "no need to truncate, shorter than max",
     1107 + text: "a",
     1108 + maxCells: 2,
     1109 + want: "a",
     1110 + },
     1111 + {
     1112 + desc: "no need to truncate, maxCells set to zero",
     1113 + text: "a",
     1114 + maxCells: 0,
     1115 + want: "a",
     1116 + },
     1117 + {
     1118 + desc: "truncates single rune to enforce max cells",
     1119 + text: "abc",
     1120 + maxCells: 2,
     1121 + want: "bc",
     1122 + },
     1123 + {
     1124 + desc: "truncates multiple runes to enforce max cells",
     1125 + text: "abcde",
     1126 + maxCells: 3,
     1127 + want: "cde",
     1128 + },
     1129 + {
     1130 + desc: "accounts for cells taken by newline characters",
     1131 + text: "a\ncde",
     1132 + maxCells: 3,
     1133 + want: "cde",
     1134 + },
     1135 + {
     1136 + desc: "truncates full-width rune on its edge",
     1137 + text: "世界",
     1138 + maxCells: 2,
     1139 + want: "界",
     1140 + },
     1141 + {
     1142 + desc: "truncates full-width rune because only half of it fits",
     1143 + text: "世界",
     1144 + maxCells: 3,
     1145 + want: "界",
     1146 + },
     1147 + {
     1148 + desc: "full-width runes - truncating not needed",
     1149 + text: "世界",
     1150 + maxCells: 4,
     1151 + want: "世界",
     1152 + },
     1153 + }
     1154 + 
     1155 + for _, tc := range tests {
     1156 + t.Run(tc.desc, func(t *testing.T) {
     1157 + got := truncateToCells(tc.text, tc.maxCells)
     1158 + if diff := pretty.Compare(tc.want, got); diff != "" {
     1159 + t.Errorf("truncateToCells => unexpected diff (-want, +got):\n%s", diff)
     1160 + }
     1161 + })
     1162 + }
     1163 +}
     1164 + 
Please wait...
Page is in error, reload to recover