1
2
3
4
5 package term
6
7 import (
8 "bytes"
9 "fmt"
10 "io"
11 "runtime"
12 "strconv"
13 "sync"
14 "unicode/utf8"
15 )
16
17
18
19 type EscapeCodes struct {
20
21 Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte
22
23
24 Reset []byte
25 }
26
27 var vt100EscapeCodes = EscapeCodes{
28 Black: []byte{keyEscape, '[', '3', '0', 'm'},
29 Red: []byte{keyEscape, '[', '3', '1', 'm'},
30 Green: []byte{keyEscape, '[', '3', '2', 'm'},
31 Yellow: []byte{keyEscape, '[', '3', '3', 'm'},
32 Blue: []byte{keyEscape, '[', '3', '4', 'm'},
33 Magenta: []byte{keyEscape, '[', '3', '5', 'm'},
34 Cyan: []byte{keyEscape, '[', '3', '6', 'm'},
35 White: []byte{keyEscape, '[', '3', '7', 'm'},
36
37 Reset: []byte{keyEscape, '[', '0', 'm'},
38 }
39
40
41 type History interface {
42
43
44
45
46
47
48 Add(entry string)
49
50
51 Len() int
52
53
54
55
56
57 At(idx int) string
58 }
59
60
61
62 type Terminal struct {
63
64
65
66
67
68
69
70 AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool)
71
72
73
74
75 Escape *EscapeCodes
76
77
78
79 lock sync.Mutex
80
81 c io.ReadWriter
82 prompt []rune
83
84
85 line []rune
86
87 pos int
88
89 echo bool
90
91
92 pasteActive bool
93
94
95
96
97 cursorX, cursorY int
98
99 maxLine int
100
101 termWidth, termHeight int
102
103
104 outBuf []byte
105
106
107 remainder []byte
108 inBuf [256]byte
109
110
111
112
113
114
115
116
117 History History
118
119
120 historyIndex int
121
122
123
124 historyPending string
125 }
126
127
128
129
130
131 func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
132 return &Terminal{
133 Escape: &vt100EscapeCodes,
134 c: c,
135 prompt: []rune(prompt),
136 termWidth: 80,
137 termHeight: 24,
138 echo: true,
139 historyIndex: -1,
140 History: &stRingBuffer{},
141 }
142 }
143
144 const (
145 keyCtrlC = 3
146 keyCtrlD = 4
147 keyCtrlU = 21
148 keyEnter = '\r'
149 keyLF = '\n'
150 keyEscape = 27
151 keyBackspace = 127
152 keyUnknown = 0xd800 + iota
153 keyUp
154 keyDown
155 keyLeft
156 keyRight
157 keyAltLeft
158 keyAltRight
159 keyHome
160 keyEnd
161 keyDeleteWord
162 keyDeleteLine
163 keyClearScreen
164 keyPasteStart
165 keyPasteEnd
166 )
167
168 var (
169 crlf = []byte{'\r', '\n'}
170 pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'}
171 pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'}
172 )
173
174
175
176 func bytesToKey(b []byte, pasteActive bool) (rune, []byte) {
177 if len(b) == 0 {
178 return utf8.RuneError, nil
179 }
180
181 if !pasteActive {
182 switch b[0] {
183 case 1:
184 return keyHome, b[1:]
185 case 2:
186 return keyLeft, b[1:]
187 case 5:
188 return keyEnd, b[1:]
189 case 6:
190 return keyRight, b[1:]
191 case 8:
192 return keyBackspace, b[1:]
193 case 11:
194 return keyDeleteLine, b[1:]
195 case 12:
196 return keyClearScreen, b[1:]
197 case 23:
198 return keyDeleteWord, b[1:]
199 case 14:
200 return keyDown, b[1:]
201 case 16:
202 return keyUp, b[1:]
203 }
204 }
205
206 if b[0] != keyEscape {
207 if !utf8.FullRune(b) {
208 return utf8.RuneError, b
209 }
210 r, l := utf8.DecodeRune(b)
211 return r, b[l:]
212 }
213
214 if !pasteActive && len(b) >= 3 && b[0] == keyEscape && b[1] == '[' {
215 switch b[2] {
216 case 'A':
217 return keyUp, b[3:]
218 case 'B':
219 return keyDown, b[3:]
220 case 'C':
221 return keyRight, b[3:]
222 case 'D':
223 return keyLeft, b[3:]
224 case 'H':
225 return keyHome, b[3:]
226 case 'F':
227 return keyEnd, b[3:]
228 }
229 }
230
231 if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' {
232 switch b[5] {
233 case 'C':
234 return keyAltRight, b[6:]
235 case 'D':
236 return keyAltLeft, b[6:]
237 }
238 }
239
240 if !pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteStart) {
241 return keyPasteStart, b[6:]
242 }
243
244 if pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) {
245 return keyPasteEnd, b[6:]
246 }
247
248
249
250
251
252 for i, c := range b[0:] {
253 if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' {
254 return keyUnknown, b[i+1:]
255 }
256 }
257
258 return utf8.RuneError, b
259 }
260
261
262 func (t *Terminal) queue(data []rune) {
263 t.outBuf = append(t.outBuf, []byte(string(data))...)
264 }
265
266 var space = []rune{' '}
267
268 func isPrintable(key rune) bool {
269 isInSurrogateArea := key >= 0xd800 && key <= 0xdbff
270 return key >= 32 && !isInSurrogateArea
271 }
272
273
274
275 func (t *Terminal) moveCursorToPos(pos int) {
276 if !t.echo {
277 return
278 }
279
280 x := visualLength(t.prompt) + pos
281 y := x / t.termWidth
282 x = x % t.termWidth
283
284 up := 0
285 if y < t.cursorY {
286 up = t.cursorY - y
287 }
288
289 down := 0
290 if y > t.cursorY {
291 down = y - t.cursorY
292 }
293
294 left := 0
295 if x < t.cursorX {
296 left = t.cursorX - x
297 }
298
299 right := 0
300 if x > t.cursorX {
301 right = x - t.cursorX
302 }
303
304 t.cursorX = x
305 t.cursorY = y
306 t.move(up, down, left, right)
307 }
308
309 func (t *Terminal) move(up, down, left, right int) {
310 m := []rune{}
311
312
313
314
315 if up == 1 {
316 m = append(m, keyEscape, '[', 'A')
317 } else if up > 1 {
318 m = append(m, keyEscape, '[')
319 m = append(m, []rune(strconv.Itoa(up))...)
320 m = append(m, 'A')
321 }
322
323 if down == 1 {
324 m = append(m, keyEscape, '[', 'B')
325 } else if down > 1 {
326 m = append(m, keyEscape, '[')
327 m = append(m, []rune(strconv.Itoa(down))...)
328 m = append(m, 'B')
329 }
330
331 if right == 1 {
332 m = append(m, keyEscape, '[', 'C')
333 } else if right > 1 {
334 m = append(m, keyEscape, '[')
335 m = append(m, []rune(strconv.Itoa(right))...)
336 m = append(m, 'C')
337 }
338
339 if left == 1 {
340 m = append(m, keyEscape, '[', 'D')
341 } else if left > 1 {
342 m = append(m, keyEscape, '[')
343 m = append(m, []rune(strconv.Itoa(left))...)
344 m = append(m, 'D')
345 }
346
347 t.queue(m)
348 }
349
350 func (t *Terminal) clearLineToRight() {
351 op := []rune{keyEscape, '[', 'K'}
352 t.queue(op)
353 }
354
355 const maxLineLength = 4096
356
357 func (t *Terminal) setLine(newLine []rune, newPos int) {
358 if t.echo {
359 t.moveCursorToPos(0)
360 t.writeLine(newLine)
361 for i := len(newLine); i < len(t.line); i++ {
362 t.writeLine(space)
363 }
364 t.moveCursorToPos(newPos)
365 }
366 t.line = newLine
367 t.pos = newPos
368 }
369
370 func (t *Terminal) advanceCursor(places int) {
371 t.cursorX += places
372 t.cursorY += t.cursorX / t.termWidth
373 if t.cursorY > t.maxLine {
374 t.maxLine = t.cursorY
375 }
376 t.cursorX = t.cursorX % t.termWidth
377
378 if places > 0 && t.cursorX == 0 {
379
380
381
382
383
384
385
386
387
388
389 t.outBuf = append(t.outBuf, '\r', '\n')
390 }
391 }
392
393 func (t *Terminal) eraseNPreviousChars(n int) {
394 if n == 0 {
395 return
396 }
397
398 if t.pos < n {
399 n = t.pos
400 }
401 t.pos -= n
402 t.moveCursorToPos(t.pos)
403
404 copy(t.line[t.pos:], t.line[n+t.pos:])
405 t.line = t.line[:len(t.line)-n]
406 if t.echo {
407 t.writeLine(t.line[t.pos:])
408 for i := 0; i < n; i++ {
409 t.queue(space)
410 }
411 t.advanceCursor(n)
412 t.moveCursorToPos(t.pos)
413 }
414 }
415
416
417
418 func (t *Terminal) countToLeftWord() int {
419 if t.pos == 0 {
420 return 0
421 }
422
423 pos := t.pos - 1
424 for pos > 0 {
425 if t.line[pos] != ' ' {
426 break
427 }
428 pos--
429 }
430 for pos > 0 {
431 if t.line[pos] == ' ' {
432 pos++
433 break
434 }
435 pos--
436 }
437
438 return t.pos - pos
439 }
440
441
442
443 func (t *Terminal) countToRightWord() int {
444 pos := t.pos
445 for pos < len(t.line) {
446 if t.line[pos] == ' ' {
447 break
448 }
449 pos++
450 }
451 for pos < len(t.line) {
452 if t.line[pos] != ' ' {
453 break
454 }
455 pos++
456 }
457 return pos - t.pos
458 }
459
460
461 func visualLength(runes []rune) int {
462 inEscapeSeq := false
463 length := 0
464
465 for _, r := range runes {
466 switch {
467 case inEscapeSeq:
468 if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
469 inEscapeSeq = false
470 }
471 case r == '\x1b':
472 inEscapeSeq = true
473 default:
474 length++
475 }
476 }
477
478 return length
479 }
480
481
482 func (t *Terminal) historyAt(idx int) (string, bool) {
483 t.lock.Unlock()
484 defer t.lock.Lock()
485 if idx < 0 || idx >= t.History.Len() {
486 return "", false
487 }
488 return t.History.At(idx), true
489 }
490
491
492 func (t *Terminal) historyAdd(entry string) {
493 t.lock.Unlock()
494 defer t.lock.Lock()
495 t.History.Add(entry)
496 }
497
498
499
500 func (t *Terminal) handleKey(key rune) (line string, ok bool) {
501 if t.pasteActive && key != keyEnter && key != keyLF {
502 t.addKeyToLine(key)
503 return
504 }
505
506 switch key {
507 case keyBackspace:
508 if t.pos == 0 {
509 return
510 }
511 t.eraseNPreviousChars(1)
512 case keyAltLeft:
513
514 t.pos -= t.countToLeftWord()
515 t.moveCursorToPos(t.pos)
516 case keyAltRight:
517
518 t.pos += t.countToRightWord()
519 t.moveCursorToPos(t.pos)
520 case keyLeft:
521 if t.pos == 0 {
522 return
523 }
524 t.pos--
525 t.moveCursorToPos(t.pos)
526 case keyRight:
527 if t.pos == len(t.line) {
528 return
529 }
530 t.pos++
531 t.moveCursorToPos(t.pos)
532 case keyHome:
533 if t.pos == 0 {
534 return
535 }
536 t.pos = 0
537 t.moveCursorToPos(t.pos)
538 case keyEnd:
539 if t.pos == len(t.line) {
540 return
541 }
542 t.pos = len(t.line)
543 t.moveCursorToPos(t.pos)
544 case keyUp:
545 entry, ok := t.historyAt(t.historyIndex + 1)
546 if !ok {
547 return "", false
548 }
549 if t.historyIndex == -1 {
550 t.historyPending = string(t.line)
551 }
552 t.historyIndex++
553 runes := []rune(entry)
554 t.setLine(runes, len(runes))
555 case keyDown:
556 switch t.historyIndex {
557 case -1:
558 return
559 case 0:
560 runes := []rune(t.historyPending)
561 t.setLine(runes, len(runes))
562 t.historyIndex--
563 default:
564 entry, ok := t.historyAt(t.historyIndex - 1)
565 if ok {
566 t.historyIndex--
567 runes := []rune(entry)
568 t.setLine(runes, len(runes))
569 }
570 }
571 case keyEnter, keyLF:
572 t.moveCursorToPos(len(t.line))
573 t.queue([]rune("\r\n"))
574 line = string(t.line)
575 ok = true
576 t.line = t.line[:0]
577 t.pos = 0
578 t.cursorX = 0
579 t.cursorY = 0
580 t.maxLine = 0
581 case keyDeleteWord:
582
583 t.eraseNPreviousChars(t.countToLeftWord())
584 case keyDeleteLine:
585
586
587 for i := t.pos; i < len(t.line); i++ {
588 t.queue(space)
589 t.advanceCursor(1)
590 }
591 t.line = t.line[:t.pos]
592 t.moveCursorToPos(t.pos)
593 case keyCtrlD:
594
595
596
597 if t.pos < len(t.line) {
598 t.pos++
599 t.eraseNPreviousChars(1)
600 }
601 case keyCtrlU:
602 t.eraseNPreviousChars(t.pos)
603 case keyClearScreen:
604
605 t.queue([]rune("\x1b[2J\x1b[H"))
606 t.queue(t.prompt)
607 t.cursorX, t.cursorY = 0, 0
608 t.advanceCursor(visualLength(t.prompt))
609 t.setLine(t.line, t.pos)
610 default:
611 if t.AutoCompleteCallback != nil {
612 prefix := string(t.line[:t.pos])
613 suffix := string(t.line[t.pos:])
614
615 t.lock.Unlock()
616 newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key)
617 t.lock.Lock()
618
619 if completeOk {
620 t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos]))
621 return
622 }
623 }
624 if !isPrintable(key) {
625 return
626 }
627 if len(t.line) == maxLineLength {
628 return
629 }
630 t.addKeyToLine(key)
631 }
632 return
633 }
634
635
636
637 func (t *Terminal) addKeyToLine(key rune) {
638 if len(t.line) == cap(t.line) {
639 newLine := make([]rune, len(t.line), 2*(1+len(t.line)))
640 copy(newLine, t.line)
641 t.line = newLine
642 }
643 t.line = t.line[:len(t.line)+1]
644 copy(t.line[t.pos+1:], t.line[t.pos:])
645 t.line[t.pos] = key
646 if t.echo {
647 t.writeLine(t.line[t.pos:])
648 }
649 t.pos++
650 t.moveCursorToPos(t.pos)
651 }
652
653 func (t *Terminal) writeLine(line []rune) {
654 for len(line) != 0 {
655 remainingOnLine := t.termWidth - t.cursorX
656 todo := len(line)
657 if todo > remainingOnLine {
658 todo = remainingOnLine
659 }
660 t.queue(line[:todo])
661 t.advanceCursor(visualLength(line[:todo]))
662 line = line[todo:]
663 }
664 }
665
666
667 func writeWithCRLF(w io.Writer, buf []byte) (n int, err error) {
668 for len(buf) > 0 {
669 i := bytes.IndexByte(buf, '\n')
670 todo := len(buf)
671 if i >= 0 {
672 todo = i
673 }
674
675 var nn int
676 nn, err = w.Write(buf[:todo])
677 n += nn
678 if err != nil {
679 return n, err
680 }
681 buf = buf[todo:]
682
683 if i >= 0 {
684 if _, err = w.Write(crlf); err != nil {
685 return n, err
686 }
687 n++
688 buf = buf[1:]
689 }
690 }
691
692 return n, nil
693 }
694
695 func (t *Terminal) Write(buf []byte) (n int, err error) {
696 t.lock.Lock()
697 defer t.lock.Unlock()
698
699 if t.cursorX == 0 && t.cursorY == 0 {
700
701
702 return writeWithCRLF(t.c, buf)
703 }
704
705
706
707 t.move(0 , 0 , t.cursorX , 0 )
708 t.cursorX = 0
709 t.clearLineToRight()
710
711 for t.cursorY > 0 {
712 t.move(1 , 0, 0, 0)
713 t.cursorY--
714 t.clearLineToRight()
715 }
716
717 if _, err = t.c.Write(t.outBuf); err != nil {
718 return
719 }
720 t.outBuf = t.outBuf[:0]
721
722 if n, err = writeWithCRLF(t.c, buf); err != nil {
723 return
724 }
725
726 t.writeLine(t.prompt)
727 if t.echo {
728 t.writeLine(t.line)
729 }
730
731 t.moveCursorToPos(t.pos)
732
733 if _, err = t.c.Write(t.outBuf); err != nil {
734 return
735 }
736 t.outBuf = t.outBuf[:0]
737 return
738 }
739
740
741
742
743
744 func (t *Terminal) ReadPassword(prompt string) (line string, err error) {
745 t.lock.Lock()
746 defer t.lock.Unlock()
747
748 oldPrompt := t.prompt
749 t.prompt = []rune(prompt)
750 t.echo = false
751 oldAutoCompleteCallback := t.AutoCompleteCallback
752 t.AutoCompleteCallback = nil
753 defer func() {
754 t.AutoCompleteCallback = oldAutoCompleteCallback
755 }()
756
757 line, err = t.readLine()
758
759 t.prompt = oldPrompt
760 t.echo = true
761
762 return
763 }
764
765
766 func (t *Terminal) ReadLine() (line string, err error) {
767 t.lock.Lock()
768 defer t.lock.Unlock()
769
770 return t.readLine()
771 }
772
773 func (t *Terminal) readLine() (line string, err error) {
774
775
776 if t.cursorX == 0 && t.cursorY == 0 {
777 t.writeLine(t.prompt)
778 t.c.Write(t.outBuf)
779 t.outBuf = t.outBuf[:0]
780 }
781
782 lineIsPasted := t.pasteActive
783
784 for {
785 rest := t.remainder
786 lineOk := false
787 for !lineOk {
788 var key rune
789 key, rest = bytesToKey(rest, t.pasteActive)
790 if key == utf8.RuneError {
791 break
792 }
793 if !t.pasteActive {
794 if key == keyCtrlD {
795 if len(t.line) == 0 {
796 return "", io.EOF
797 }
798 }
799 if key == keyCtrlC {
800 return "", io.EOF
801 }
802 if key == keyPasteStart {
803 t.pasteActive = true
804 if len(t.line) == 0 {
805 lineIsPasted = true
806 }
807 continue
808 }
809 } else if key == keyPasteEnd {
810 t.pasteActive = false
811 continue
812 }
813 if !t.pasteActive {
814 lineIsPasted = false
815 }
816
817 if key == keyEnter && len(rest) > 0 && rest[0] == keyLF {
818 rest = rest[1:]
819 }
820 line, lineOk = t.handleKey(key)
821 }
822 if len(rest) > 0 {
823 n := copy(t.inBuf[:], rest)
824 t.remainder = t.inBuf[:n]
825 } else {
826 t.remainder = nil
827 }
828 t.c.Write(t.outBuf)
829 t.outBuf = t.outBuf[:0]
830 if lineOk {
831 if t.echo {
832 t.historyIndex = -1
833 t.historyAdd(line)
834 }
835 if lineIsPasted {
836 err = ErrPasteIndicator
837 }
838 return
839 }
840
841
842
843 readBuf := t.inBuf[len(t.remainder):]
844 var n int
845
846 t.lock.Unlock()
847 n, err = t.c.Read(readBuf)
848 t.lock.Lock()
849
850 if err != nil {
851 return
852 }
853
854 t.remainder = t.inBuf[:n+len(t.remainder)]
855 }
856 }
857
858
859 func (t *Terminal) SetPrompt(prompt string) {
860 t.lock.Lock()
861 defer t.lock.Unlock()
862
863 t.prompt = []rune(prompt)
864 }
865
866 func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) {
867
868 t.move(t.cursorY, 0, t.cursorX, 0)
869 t.cursorX, t.cursorY = 0, 0
870 t.clearLineToRight()
871 for t.cursorY < numPrevLines {
872
873 t.move(0, 1, 0, 0)
874 t.cursorY++
875 t.clearLineToRight()
876 }
877
878 t.move(t.cursorY, 0, 0, 0)
879 t.cursorX, t.cursorY = 0, 0
880
881 t.queue(t.prompt)
882 t.advanceCursor(visualLength(t.prompt))
883 t.writeLine(t.line)
884 t.moveCursorToPos(t.pos)
885 }
886
887 func (t *Terminal) SetSize(width, height int) error {
888 t.lock.Lock()
889 defer t.lock.Unlock()
890
891 if width == 0 {
892 width = 1
893 }
894
895 oldWidth := t.termWidth
896 t.termWidth, t.termHeight = width, height
897
898 switch {
899 case width == oldWidth:
900
901
902 return nil
903 case len(t.line) == 0 && t.cursorX == 0 && t.cursorY == 0:
904
905
906 return nil
907 case width < oldWidth:
908
909
910
911
912
913
914
915
916
917
918
919 if t.cursorX >= t.termWidth {
920 t.cursorX = t.termWidth - 1
921 }
922 t.cursorY *= 2
923 t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2)
924 case width > oldWidth:
925
926
927
928
929
930
931
932 t.clearAndRepaintLinePlusNPrevious(t.maxLine)
933 }
934
935 _, err := t.c.Write(t.outBuf)
936 t.outBuf = t.outBuf[:0]
937 return err
938 }
939
940 type pasteIndicatorError struct{}
941
942 func (pasteIndicatorError) Error() string {
943 return "terminal: ErrPasteIndicator not correctly handled"
944 }
945
946
947
948
949
950 var ErrPasteIndicator = pasteIndicatorError{}
951
952
953
954
955
956
957 func (t *Terminal) SetBracketedPasteMode(on bool) {
958 if on {
959 io.WriteString(t.c, "\x1b[?2004h")
960 } else {
961 io.WriteString(t.c, "\x1b[?2004l")
962 }
963 }
964
965
966 type stRingBuffer struct {
967
968 entries []string
969 max int
970
971 head int
972
973 size int
974 }
975
976 func (s *stRingBuffer) Add(a string) {
977 if s.entries == nil {
978 const defaultNumEntries = 100
979 s.entries = make([]string, defaultNumEntries)
980 s.max = defaultNumEntries
981 }
982
983 s.head = (s.head + 1) % s.max
984 s.entries[s.head] = a
985 if s.size < s.max {
986 s.size++
987 }
988 }
989
990 func (s *stRingBuffer) Len() int {
991 return s.size
992 }
993
994
995
996
997
998 func (s *stRingBuffer) At(n int) string {
999 if n < 0 || n >= s.size {
1000 panic(fmt.Sprintf("term: history index [%d] out of range [0,%d)", n, s.size))
1001 }
1002 index := s.head - n
1003 if index < 0 {
1004 index += s.max
1005 }
1006 return s.entries[index]
1007 }
1008
1009
1010
1011
1012
1013
1014 func readPasswordLine(reader io.Reader) ([]byte, error) {
1015 var buf [1]byte
1016 var ret []byte
1017
1018 for {
1019 n, err := reader.Read(buf[:])
1020 if n > 0 {
1021 switch buf[0] {
1022 case '\b':
1023 if len(ret) > 0 {
1024 ret = ret[:len(ret)-1]
1025 }
1026 case '\n':
1027 if runtime.GOOS != "windows" {
1028 return ret, nil
1029 }
1030
1031 case '\r':
1032 if runtime.GOOS == "windows" {
1033 return ret, nil
1034 }
1035
1036 default:
1037 ret = append(ret, buf[0])
1038 }
1039 continue
1040 }
1041 if err != nil {
1042 if err == io.EOF && len(ret) > 0 {
1043 return ret, nil
1044 }
1045 return ret, err
1046 }
1047 }
1048 }
1049
View as plain text