mirror of https://github.com/liamg/aminal.git
start scrolling
This commit is contained in:
parent
ada4e924f9
commit
8e5a9fb26d
|
@ -46,7 +46,7 @@ func (buffer *Buffer) SetReplaceMode() {
|
||||||
buffer.replaceMode = true
|
buffer.replaceMode = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) SetMargins(top uint, bottom uint) {
|
func (buffer *Buffer) SetVerticalMargins(top uint, bottom uint) {
|
||||||
buffer.topMargin = top
|
buffer.topMargin = top
|
||||||
buffer.bottomMargin = bottom
|
buffer.bottomMargin = bottom
|
||||||
}
|
}
|
||||||
|
@ -57,11 +57,36 @@ func (buffer *Buffer) GetScrollOffset() uint {
|
||||||
|
|
||||||
func (buffer *Buffer) ScrollDown(lines uint16) {
|
func (buffer *Buffer) ScrollDown(lines uint16) {
|
||||||
|
|
||||||
|
defer buffer.emitDisplayChange()
|
||||||
|
|
||||||
|
// scrollable region is enabled
|
||||||
|
if buffer.topMargin > 0 || buffer.bottomMargin < uint(buffer.ViewHeight())-1 {
|
||||||
|
|
||||||
|
for c := 0; c < int(lines); c++ {
|
||||||
|
|
||||||
|
for i := buffer.topMargin; i < buffer.bottomMargin; i++ {
|
||||||
|
above := buffer.getViewLine(uint16(i))
|
||||||
|
below := buffer.getViewLine(uint16(i + 1))
|
||||||
|
above.cells = below.cells
|
||||||
|
}
|
||||||
|
final := buffer.getViewLine(uint16(buffer.bottomMargin))
|
||||||
|
|
||||||
|
lineIndex := buffer.convertViewLineToRawLine(uint16(buffer.bottomMargin + 1))
|
||||||
|
|
||||||
|
if lineIndex < uint64(len(buffer.lines)) {
|
||||||
|
*final = buffer.lines[lineIndex]
|
||||||
|
} else {
|
||||||
|
*final = newLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if buffer.Height() < int(buffer.ViewHeight()) {
|
if buffer.Height() < int(buffer.ViewHeight()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer buffer.emitDisplayChange()
|
|
||||||
if uint(lines) > buffer.scrollLinesFromBottom {
|
if uint(lines) > buffer.scrollLinesFromBottom {
|
||||||
lines = uint16(buffer.scrollLinesFromBottom)
|
lines = uint16(buffer.scrollLinesFromBottom)
|
||||||
}
|
}
|
||||||
|
@ -70,11 +95,36 @@ func (buffer *Buffer) ScrollDown(lines uint16) {
|
||||||
|
|
||||||
func (buffer *Buffer) ScrollUp(lines uint16) {
|
func (buffer *Buffer) ScrollUp(lines uint16) {
|
||||||
|
|
||||||
if buffer.Height() < int(buffer.ViewHeight()) {
|
defer buffer.emitDisplayChange()
|
||||||
|
|
||||||
|
// scrollable region is enabled
|
||||||
|
if buffer.topMargin > 0 || buffer.bottomMargin < uint(buffer.ViewHeight())-1 {
|
||||||
|
|
||||||
|
for c := 0; c < int(lines); c++ {
|
||||||
|
|
||||||
|
for i := buffer.bottomMargin; i > buffer.topMargin+1; i-- {
|
||||||
|
below := buffer.getViewLine(uint16(i))
|
||||||
|
above := buffer.getViewLine(uint16(i - 1))
|
||||||
|
below.cells = above.cells
|
||||||
|
}
|
||||||
|
final := buffer.getViewLine(uint16(buffer.topMargin))
|
||||||
|
|
||||||
|
lineIndex := buffer.convertViewLineToRawLine(uint16(buffer.topMargin - 1))
|
||||||
|
|
||||||
|
if lineIndex >= 0 && lineIndex < uint64(len(buffer.lines)) {
|
||||||
|
*final = buffer.lines[lineIndex]
|
||||||
|
} else {
|
||||||
|
panic("hmm!?")
|
||||||
|
*final = newLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer buffer.emitDisplayChange()
|
if buffer.Height() < int(buffer.ViewHeight()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if uint(lines)+buffer.scrollLinesFromBottom >= (uint(buffer.Height()) - uint(buffer.ViewHeight())) {
|
if uint(lines)+buffer.scrollLinesFromBottom >= (uint(buffer.Height()) - uint(buffer.ViewHeight())) {
|
||||||
buffer.scrollLinesFromBottom = uint(buffer.Height()) - uint(buffer.ViewHeight())
|
buffer.scrollLinesFromBottom = uint(buffer.Height()) - uint(buffer.ViewHeight())
|
||||||
|
@ -279,12 +329,26 @@ func (buffer *Buffer) CarriageReturn() {
|
||||||
|
|
||||||
func (buffer *Buffer) NewLine() {
|
func (buffer *Buffer) NewLine() {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.emitDisplayChange()
|
||||||
|
|
||||||
|
buffer.cursorX = 0
|
||||||
|
|
||||||
|
if (buffer.topMargin > 0 || buffer.bottomMargin < uint(buffer.ViewHeight())-1) && uint(buffer.cursorY) == buffer.bottomMargin {
|
||||||
|
// scrollable region is enabled
|
||||||
|
for i := buffer.topMargin; i < buffer.bottomMargin; i++ {
|
||||||
|
above := buffer.getViewLine(uint16(i))
|
||||||
|
below := buffer.getViewLine(uint16(i + 1))
|
||||||
|
above.cells = below.cells
|
||||||
|
}
|
||||||
|
final := buffer.getViewLine(uint16(buffer.bottomMargin))
|
||||||
|
*final = newLine()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if buffer.cursorY >= buffer.ViewHeight()-1 {
|
if buffer.cursorY >= buffer.ViewHeight()-1 {
|
||||||
buffer.lines = append(buffer.lines, newLine())
|
buffer.lines = append(buffer.lines, newLine())
|
||||||
} else {
|
} else {
|
||||||
buffer.cursorY++
|
buffer.cursorY++
|
||||||
}
|
}
|
||||||
buffer.cursorX = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) MovePosition(x int16, y int16) {
|
func (buffer *Buffer) MovePosition(x int16, y int16) {
|
||||||
|
@ -344,23 +408,27 @@ func (buffer *Buffer) Clear() {
|
||||||
|
|
||||||
// creates if necessary
|
// creates if necessary
|
||||||
func (buffer *Buffer) getCurrentLine() *Line {
|
func (buffer *Buffer) getCurrentLine() *Line {
|
||||||
|
return buffer.getViewLine(buffer.cursorY)
|
||||||
|
}
|
||||||
|
|
||||||
if buffer.cursorY >= buffer.ViewHeight() { // @todo is this okay?
|
func (buffer *Buffer) getViewLine(index uint16) *Line {
|
||||||
|
|
||||||
|
if index >= buffer.ViewHeight() { // @todo is this okay?
|
||||||
return &buffer.lines[len(buffer.lines)-1]
|
return &buffer.lines[len(buffer.lines)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(buffer.lines) < int(buffer.ViewHeight()) {
|
if len(buffer.lines) < int(buffer.ViewHeight()) {
|
||||||
for int(buffer.cursorY) >= len(buffer.lines) {
|
for int(index) >= len(buffer.lines) {
|
||||||
buffer.lines = append(buffer.lines, newLine())
|
buffer.lines = append(buffer.lines, newLine())
|
||||||
}
|
}
|
||||||
return &buffer.lines[int(buffer.cursorY)]
|
return &buffer.lines[int(index)]
|
||||||
}
|
}
|
||||||
|
|
||||||
if int(buffer.RawLine()) < len(buffer.lines) {
|
if int(buffer.convertViewLineToRawLine(index)) < len(buffer.lines) {
|
||||||
return &buffer.lines[buffer.RawLine()]
|
return &buffer.lines[buffer.convertViewLineToRawLine(index)]
|
||||||
}
|
}
|
||||||
|
|
||||||
panic(fmt.Sprintf("Failed to retrieve line for %d %d", buffer.cursorX, buffer.cursorY))
|
panic(fmt.Sprintf("Failed to retrieve line for %d", index))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) EraseLine() {
|
func (buffer *Buffer) EraseLine() {
|
||||||
|
@ -547,5 +615,5 @@ func (buffer *Buffer) ResizeView(width uint16, height uint16) {
|
||||||
line = buffer.getCurrentLine()
|
line = buffer.getCurrentLine()
|
||||||
buffer.cursorX = uint16((len(line.cells) - cXFromEndOfLine) - 1)
|
buffer.cursorX = uint16((len(line.cells) - cXFromEndOfLine) - 1)
|
||||||
|
|
||||||
buffer.SetMargins(0, uint(buffer.viewHeight-1))
|
buffer.SetVerticalMargins(0, uint(buffer.viewHeight-1))
|
||||||
}
|
}
|
||||||
|
|
126
terminal/csi.go
126
terminal/csi.go
|
@ -15,12 +15,14 @@ var csiSequenceMap = map[rune]csiSequenceHandler{
|
||||||
't': csiWindowManipulation,
|
't': csiWindowManipulation,
|
||||||
'J': csiEraseInDisplayHandler,
|
'J': csiEraseInDisplayHandler,
|
||||||
'K': csiEraseInLineHandler,
|
'K': csiEraseInLineHandler,
|
||||||
|
'L': csiInsertLinesHandler,
|
||||||
'P': csiDeleteHandler,
|
'P': csiDeleteHandler,
|
||||||
'T': csiScrollHandler,
|
'S': csiScrollUpHandler,
|
||||||
|
'T': csiScrollDownHandler,
|
||||||
'X': csiEraseCharactersHandler,
|
'X': csiEraseCharactersHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
func csiScrollHandler(params []string, intermediate string, terminal *Terminal) error {
|
func csiScrollUpHandler(params []string, intermediate string, terminal *Terminal) error {
|
||||||
distance := 1
|
distance := 1
|
||||||
if len(params) > 1 {
|
if len(params) > 1 {
|
||||||
return fmt.Errorf("Not supported")
|
return fmt.Errorf("Not supported")
|
||||||
|
@ -32,6 +34,41 @@ func csiScrollHandler(params []string, intermediate string, terminal *Terminal)
|
||||||
distance = 1
|
distance = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
terminal.logger.Debugf("Scrolling up %d", distance)
|
||||||
|
terminal.ScrollUp(uint16(distance))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func csiInsertLinesHandler(params []string, intermediate string, terminal *Terminal) error {
|
||||||
|
count := 1
|
||||||
|
if len(params) > 1 {
|
||||||
|
return fmt.Errorf("Not supported")
|
||||||
|
}
|
||||||
|
if len(params) == 1 {
|
||||||
|
var err error
|
||||||
|
count, err = strconv.Atoi(params[0])
|
||||||
|
if err != nil {
|
||||||
|
count = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terminal.logger.Debugf("Inserting %d lines", count)
|
||||||
|
panic("Not supported")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func csiScrollDownHandler(params []string, intermediate string, terminal *Terminal) error {
|
||||||
|
distance := 1
|
||||||
|
if len(params) > 1 {
|
||||||
|
return fmt.Errorf("Not supported")
|
||||||
|
}
|
||||||
|
if len(params) == 1 {
|
||||||
|
var err error
|
||||||
|
distance, err = strconv.Atoi(params[0])
|
||||||
|
if err != nil {
|
||||||
|
distance = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terminal.logger.Debugf("Scrolling down %d", distance)
|
||||||
terminal.ScrollDown(uint16(distance))
|
terminal.ScrollDown(uint16(distance))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -61,90 +98,9 @@ func csiSetMarginsHandler(params []string, intermediate string, terminal *Termin
|
||||||
top--
|
top--
|
||||||
bottom--
|
bottom--
|
||||||
|
|
||||||
terminal.logger.Warnf("Request to set margins from line %d to %d", top, bottom)
|
terminal.ActiveBuffer().SetVerticalMargins(uint(top), uint(bottom))
|
||||||
|
|
||||||
terminal.ActiveBuffer().SetPosition(0, 0)
|
terminal.ActiveBuffer().SetPosition(0, 0)
|
||||||
|
|
||||||
return fmt.Errorf("Not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func csiSetMode(modeStr string, enabled bool, terminal *Terminal) error {
|
|
||||||
|
|
||||||
/*
|
|
||||||
Mouse support
|
|
||||||
|
|
||||||
#define SET_X10_MOUSE 9
|
|
||||||
#define SET_VT200_MOUSE 1000
|
|
||||||
#define SET_VT200_HIGHLIGHT_MOUSE 1001
|
|
||||||
#define SET_BTN_EVENT_MOUSE 1002
|
|
||||||
#define SET_ANY_EVENT_MOUSE 1003
|
|
||||||
|
|
||||||
#define SET_FOCUS_EVENT_MOUSE 1004
|
|
||||||
|
|
||||||
#define SET_EXT_MODE_MOUSE 1005
|
|
||||||
#define SET_SGR_EXT_MODE_MOUSE 1006
|
|
||||||
#define SET_URXVT_EXT_MODE_MOUSE 1015
|
|
||||||
|
|
||||||
#define SET_ALTERNATE_SCROLL 1007
|
|
||||||
*/
|
|
||||||
|
|
||||||
switch modeStr {
|
|
||||||
case "4":
|
|
||||||
if enabled { // @todo support replace mode
|
|
||||||
terminal.ActiveBuffer().SetInsertMode()
|
|
||||||
} else {
|
|
||||||
terminal.ActiveBuffer().SetReplaceMode()
|
|
||||||
}
|
|
||||||
case "?1":
|
|
||||||
terminal.modes.ApplicationCursorKeys = enabled
|
|
||||||
case "?7":
|
|
||||||
// auto-wrap mode
|
|
||||||
//DECAWM
|
|
||||||
terminal.ActiveBuffer().SetAutoWrap(enabled)
|
|
||||||
case "?9":
|
|
||||||
if enabled {
|
|
||||||
terminal.logger.Infof("Turning on X10 mouse mode")
|
|
||||||
terminal.SetMouseMode(MouseModeX10)
|
|
||||||
} else {
|
|
||||||
terminal.logger.Infof("Turning off X10 mouse mode")
|
|
||||||
terminal.SetMouseMode(MouseModeNone)
|
|
||||||
}
|
|
||||||
case "?12", "?13":
|
|
||||||
terminal.modes.BlinkingCursor = enabled
|
|
||||||
case "?25":
|
|
||||||
terminal.modes.ShowCursor = enabled
|
|
||||||
case "?47", "?1047":
|
|
||||||
if enabled {
|
|
||||||
terminal.UseAltBuffer()
|
|
||||||
} else {
|
|
||||||
terminal.UseMainBuffer()
|
|
||||||
}
|
|
||||||
case "?1000", "?1006;1000", "?10061000": // ?10061000 seen from htop
|
|
||||||
// enable mouse tracking
|
|
||||||
// 1000 refers to ext mode for extended mouse click area - otherwise only x <= 255-31
|
|
||||||
if enabled {
|
|
||||||
terminal.logger.Infof("Turning on VT200 mouse mode")
|
|
||||||
terminal.SetMouseMode(MouseModeVT200)
|
|
||||||
} else {
|
|
||||||
terminal.logger.Infof("Turning off VT200 mouse mode")
|
|
||||||
terminal.SetMouseMode(MouseModeNone)
|
|
||||||
}
|
|
||||||
case "?1048":
|
|
||||||
if enabled {
|
|
||||||
terminal.ActiveBuffer().SaveCursor()
|
|
||||||
} else {
|
|
||||||
terminal.ActiveBuffer().RestoreCursor()
|
|
||||||
}
|
|
||||||
case "?1049":
|
|
||||||
if enabled {
|
|
||||||
terminal.UseAltBuffer()
|
|
||||||
} else {
|
|
||||||
terminal.UseMainBuffer()
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Unsupported CSI %sl code", modeStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,7 +290,7 @@ CSI:
|
||||||
err = fmt.Errorf("Unknown CSI control sequence: 0x%02X (ESC[%s%s%s)", final, param, intermediate, string(final))
|
err = fmt.Errorf("Unknown CSI control sequence: 0x%02X (ESC[%s%s%s)", final, param, intermediate, string(final))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
terminal.logger.Debugf("Received CSI control sequence: 0x%02X (ESC[%s%s%s)", final, param, intermediate, string(final))
|
fmt.Printf("CSI 0x%02X (ESC[%s%s%s)\n", final, param, intermediate, string(final))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func csiSetMode(modeStr string, enabled bool, terminal *Terminal) error {
|
||||||
|
|
||||||
|
/*
|
||||||
|
Mouse support
|
||||||
|
|
||||||
|
#define SET_X10_MOUSE 9
|
||||||
|
#define SET_VT200_MOUSE 1000
|
||||||
|
#define SET_VT200_HIGHLIGHT_MOUSE 1001
|
||||||
|
#define SET_BTN_EVENT_MOUSE 1002
|
||||||
|
#define SET_ANY_EVENT_MOUSE 1003
|
||||||
|
|
||||||
|
#define SET_FOCUS_EVENT_MOUSE 1004
|
||||||
|
|
||||||
|
#define SET_EXT_MODE_MOUSE 1005
|
||||||
|
#define SET_SGR_EXT_MODE_MOUSE 1006
|
||||||
|
#define SET_URXVT_EXT_MODE_MOUSE 1015
|
||||||
|
|
||||||
|
#define SET_ALTERNATE_SCROLL 1007
|
||||||
|
*/
|
||||||
|
|
||||||
|
switch modeStr {
|
||||||
|
case "4":
|
||||||
|
if enabled { // @todo support replace mode
|
||||||
|
terminal.ActiveBuffer().SetInsertMode()
|
||||||
|
} else {
|
||||||
|
terminal.ActiveBuffer().SetReplaceMode()
|
||||||
|
}
|
||||||
|
case "?1":
|
||||||
|
terminal.modes.ApplicationCursorKeys = enabled
|
||||||
|
case "?7":
|
||||||
|
// auto-wrap mode
|
||||||
|
//DECAWM
|
||||||
|
terminal.ActiveBuffer().SetAutoWrap(enabled)
|
||||||
|
case "?9":
|
||||||
|
if enabled {
|
||||||
|
terminal.logger.Infof("Turning on X10 mouse mode")
|
||||||
|
terminal.SetMouseMode(MouseModeX10)
|
||||||
|
} else {
|
||||||
|
terminal.logger.Infof("Turning off X10 mouse mode")
|
||||||
|
terminal.SetMouseMode(MouseModeNone)
|
||||||
|
}
|
||||||
|
case "?12", "?13":
|
||||||
|
terminal.modes.BlinkingCursor = enabled
|
||||||
|
case "?25":
|
||||||
|
terminal.modes.ShowCursor = enabled
|
||||||
|
case "?47", "?1047":
|
||||||
|
if enabled {
|
||||||
|
terminal.UseAltBuffer()
|
||||||
|
} else {
|
||||||
|
terminal.UseMainBuffer()
|
||||||
|
}
|
||||||
|
case "?1000", "?1006;1000", "?10061000": // ?10061000 seen from htop
|
||||||
|
// enable mouse tracking
|
||||||
|
// 1000 refers to ext mode for extended mouse click area - otherwise only x <= 255-31
|
||||||
|
if enabled {
|
||||||
|
terminal.logger.Infof("Turning on VT200 mouse mode")
|
||||||
|
terminal.SetMouseMode(MouseModeVT200)
|
||||||
|
} else {
|
||||||
|
terminal.logger.Infof("Turning off VT200 mouse mode")
|
||||||
|
terminal.SetMouseMode(MouseModeNone)
|
||||||
|
}
|
||||||
|
case "?1048":
|
||||||
|
if enabled {
|
||||||
|
terminal.ActiveBuffer().SaveCursor()
|
||||||
|
} else {
|
||||||
|
terminal.ActiveBuffer().RestoreCursor()
|
||||||
|
}
|
||||||
|
case "?1049":
|
||||||
|
if enabled {
|
||||||
|
terminal.UseAltBuffer()
|
||||||
|
} else {
|
||||||
|
terminal.UseMainBuffer()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unsupported CSI %sl code", modeStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ var escapeSequenceMap = map[rune]escapeSequenceHandler{
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLineSequenceHandler(pty chan rune, terminal *Terminal) error {
|
func newLineSequenceHandler(pty chan rune, terminal *Terminal) error {
|
||||||
|
terminal.logger.Debugf("New line!")
|
||||||
terminal.ActiveBuffer().NewLine()
|
terminal.ActiveBuffer().NewLine()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -94,7 +95,7 @@ func (terminal *Terminal) processInput(ctx context.Context, pty chan rune) {
|
||||||
terminal.logger.Errorf("Error handling escape sequence: %s", err)
|
terminal.logger.Errorf("Error handling escape sequence: %s", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
terminal.logger.Debugf("Received character 0x%X: %q", b, string(b))
|
//terminal.logger.Debugf("Received character 0x%X: %q", b, string(b))
|
||||||
if b >= 0x20 {
|
if b >= 0x20 {
|
||||||
terminal.ActiveBuffer().Write(b)
|
terminal.ActiveBuffer().Write(b)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -133,10 +133,13 @@ func (terminal *Terminal) GetScrollOffset() uint {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) ScrollDown(lines uint16) {
|
func (terminal *Terminal) ScrollDown(lines uint16) {
|
||||||
|
terminal.logger.Infof("Scrolling down %d", lines)
|
||||||
terminal.ActiveBuffer().ScrollDown(lines)
|
terminal.ActiveBuffer().ScrollDown(lines)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) ScrollUp(lines uint16) {
|
func (terminal *Terminal) ScrollUp(lines uint16) {
|
||||||
|
terminal.logger.Infof("Scrolling up %d", lines)
|
||||||
terminal.ActiveBuffer().ScrollUp(lines)
|
terminal.ActiveBuffer().ScrollUp(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue