Add basic terminal widget
This commit is contained in:
parent
13ba53c9d0
commit
1170893e39
2
go.mod
2
go.mod
|
@ -1,6 +1,7 @@
|
||||||
module git.sr.ht/~sircmpwn/aerc2
|
module git.sr.ht/~sircmpwn/aerc2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.sr.ht/~sircmpwn/go-libvterm v0.0.0-20190316225658-2a4963dd9ec0
|
||||||
github.com/emersion/go-imap v1.0.0-beta.1
|
github.com/emersion/go-imap v1.0.0-beta.1
|
||||||
github.com/emersion/go-imap-idle v0.0.0-20180114101550-2af93776db6b
|
github.com/emersion/go-imap-idle v0.0.0-20180114101550-2af93776db6b
|
||||||
github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197 // indirect
|
github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197 // indirect
|
||||||
|
@ -8,6 +9,7 @@ require (
|
||||||
github.com/gdamore/tcell v1.0.0
|
github.com/gdamore/tcell v1.0.0
|
||||||
github.com/go-ini/ini v1.42.0
|
github.com/go-ini/ini v1.42.0
|
||||||
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf
|
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf
|
||||||
|
github.com/kr/pty v1.1.3
|
||||||
github.com/kyoh86/xdg v0.0.0-20171127140545-8db68a8ea76a
|
github.com/kyoh86/xdg v0.0.0-20171127140545-8db68a8ea76a
|
||||||
github.com/lucasb-eyer/go-colorful v0.0.0-20180531031333-d9cec903b20c
|
github.com/lucasb-eyer/go-colorful v0.0.0-20180531031333-d9cec903b20c
|
||||||
github.com/mattn/go-isatty v0.0.3
|
github.com/mattn/go-isatty v0.0.3
|
||||||
|
|
10
go.sum
10
go.sum
|
@ -1,3 +1,5 @@
|
||||||
|
git.sr.ht/~sircmpwn/go-libvterm v0.0.0-20190316225658-2a4963dd9ec0 h1:aIQh7m6L3uS8/lg021Cia2QtttUgZO0LuuxJ8wc57dQ=
|
||||||
|
git.sr.ht/~sircmpwn/go-libvterm v0.0.0-20190316225658-2a4963dd9ec0/go.mod h1:cp37LbiS1y4CrTOmKSF87ZMLwawWUF612RYKTi8vbDc=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/emersion/go-imap v1.0.0-beta.1 h1:bTCaVlUnb5mKoW9lEukusxguSYYZPer+q0g5t+vw5X0=
|
github.com/emersion/go-imap v1.0.0-beta.1 h1:bTCaVlUnb5mKoW9lEukusxguSYYZPer+q0g5t+vw5X0=
|
||||||
|
@ -16,14 +18,22 @@ github.com/go-ini/ini v1.42.0 h1:TWr1wGj35+UiWHlBA8er89seFXxzwFn11spilrrj+38=
|
||||||
github.com/go-ini/ini v1.42.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
github.com/go-ini/ini v1.42.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg=
|
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg=
|
||||||
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
|
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
|
||||||
|
github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk=
|
||||||
|
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kyoh86/xdg v0.0.0-20171127140545-8db68a8ea76a h1:vLFQnHOnCnmlySdpHAKF+mH7MhsthJgpBbfexVhHwxY=
|
github.com/kyoh86/xdg v0.0.0-20171127140545-8db68a8ea76a h1:vLFQnHOnCnmlySdpHAKF+mH7MhsthJgpBbfexVhHwxY=
|
||||||
github.com/kyoh86/xdg v0.0.0-20171127140545-8db68a8ea76a/go.mod h1:Z5mDqe0fxyxn3W2yTxsBAOQqIrXADQIh02wrTnaRM38=
|
github.com/kyoh86/xdg v0.0.0-20171127140545-8db68a8ea76a/go.mod h1:Z5mDqe0fxyxn3W2yTxsBAOQqIrXADQIh02wrTnaRM38=
|
||||||
github.com/lucasb-eyer/go-colorful v0.0.0-20180531031333-d9cec903b20c h1:b11Y3yxg40v2/9KUz76a4mSC1DMlgnPGAt+4pJSgmyU=
|
github.com/lucasb-eyer/go-colorful v0.0.0-20180531031333-d9cec903b20c h1:b11Y3yxg40v2/9KUz76a4mSC1DMlgnPGAt+4pJSgmyU=
|
||||||
github.com/lucasb-eyer/go-colorful v0.0.0-20180531031333-d9cec903b20c/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
|
github.com/lucasb-eyer/go-colorful v0.0.0-20180531031333-d9cec903b20c/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
|
||||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-libvterm v0.0.0-20190121020430-725de0572324 h1:0C5/KYb9AMSjg9VhXk0RxNMZN/4y3vztCYVNSHIkHlg=
|
||||||
|
github.com/mattn/go-libvterm v0.0.0-20190121020430-725de0572324/go.mod h1:E9ZjxjhK3K5YoeO/TCZVNsquRRZX2LeIX0+G33613Io=
|
||||||
|
github.com/mattn/go-pointer v0.0.0-20180825124634-49522c3f3791 h1:PfHMsLQJwoc0ccjK0sam6J0wQo4s8mOuAo2yQGw+T2U=
|
||||||
|
github.com/mattn/go-pointer v0.0.0-20180825124634-49522c3f3791/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
|
||||||
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
|
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
|
||||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/micromaomao/go-libvterm v0.0.0-20190126085614-2401b10ee7ed h1:SDQJB+uDFtSsq49UlzhnJJkFNXqoSG5CHdOnoN/fWF0=
|
||||||
|
github.com/micromaomao/go-libvterm v0.0.0-20190126085614-2401b10ee7ed/go.mod h1:TEYd4HSsUc2pZan5xJmjJQLA7c3d9dkV9lNsf8Xh3TY=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/nsf/termbox-go v0.0.0-20180129072728-88b7b944be8b/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
github.com/nsf/termbox-go v0.0.0-20180129072728-88b7b944be8b/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||||
|
|
|
@ -16,6 +16,8 @@ type Drawable interface {
|
||||||
type Interactive interface {
|
type Interactive interface {
|
||||||
// Returns true if the event was handled by this component
|
// Returns true if the event was handled by this component
|
||||||
Event(event tcell.Event) bool
|
Event(event tcell.Event) bool
|
||||||
|
// Indicates whether or not this control will receive input events
|
||||||
|
Focus(focus bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Simulator interface {
|
type Simulator interface {
|
||||||
|
|
|
@ -54,6 +54,7 @@ func Initialize(conf *config.AercConfig,
|
||||||
state.invalidations <- nil
|
state.invalidations <- nil
|
||||||
})()
|
})()
|
||||||
})
|
})
|
||||||
|
content.Focus(true)
|
||||||
return &state, nil
|
return &state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ type AccountView struct {
|
||||||
dirlist *DirectoryList
|
dirlist *DirectoryList
|
||||||
grid *ui.Grid
|
grid *ui.Grid
|
||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
interactive ui.Interactive
|
interactive []ui.Interactive
|
||||||
onInvalidate func(d ui.Drawable)
|
onInvalidate func(d ui.Drawable)
|
||||||
runCmd func(cmd string) error
|
runCmd func(cmd string) error
|
||||||
msglist *MessageList
|
msglist *MessageList
|
||||||
|
@ -116,6 +116,21 @@ func (acct *AccountView) Draw(ctx *ui.Context) {
|
||||||
acct.grid.Draw(ctx)
|
acct.grid.Draw(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (acct *AccountView) popInteractive() {
|
||||||
|
acct.interactive = acct.interactive[:len(acct.interactive)-1]
|
||||||
|
if len(acct.interactive) != 0 {
|
||||||
|
acct.interactive[len(acct.interactive)-1].Focus(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (acct *AccountView) pushInteractive(item ui.Interactive) {
|
||||||
|
if len(acct.interactive) != 0 {
|
||||||
|
acct.interactive[len(acct.interactive)-1].Focus(false)
|
||||||
|
}
|
||||||
|
acct.interactive = append(acct.interactive, item)
|
||||||
|
item.Focus(true)
|
||||||
|
}
|
||||||
|
|
||||||
func (acct *AccountView) beginExCommand() {
|
func (acct *AccountView) beginExCommand() {
|
||||||
exline := NewExLine(func(command string) {
|
exline := NewExLine(func(command string) {
|
||||||
err := acct.runCmd(command)
|
err := acct.runCmd(command)
|
||||||
|
@ -124,18 +139,18 @@ func (acct *AccountView) beginExCommand() {
|
||||||
Color(tcell.ColorRed, tcell.ColorWhite)
|
Color(tcell.ColorRed, tcell.ColorWhite)
|
||||||
}
|
}
|
||||||
acct.statusbar.Pop()
|
acct.statusbar.Pop()
|
||||||
acct.interactive = nil
|
acct.popInteractive()
|
||||||
}, func() {
|
}, func() {
|
||||||
acct.statusbar.Pop()
|
acct.statusbar.Pop()
|
||||||
acct.interactive = nil
|
acct.popInteractive()
|
||||||
})
|
})
|
||||||
acct.interactive = exline
|
acct.pushInteractive(exline)
|
||||||
acct.statusbar.Push(exline)
|
acct.statusbar.Push(exline)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acct *AccountView) Event(event tcell.Event) bool {
|
func (acct *AccountView) Event(event tcell.Event) bool {
|
||||||
if acct.interactive != nil {
|
if len(acct.interactive) != 0 {
|
||||||
return acct.interactive.Event(event)
|
return acct.interactive[len(acct.interactive)-1].Event(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch event := event.(type) {
|
switch event := event.(type) {
|
||||||
|
|
|
@ -66,6 +66,10 @@ func (aerc *Aerc) Invalidate() {
|
||||||
aerc.grid.Invalidate()
|
aerc.grid.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (aerc *Aerc) Focus(focus bool) {
|
||||||
|
// who cares
|
||||||
|
}
|
||||||
|
|
||||||
func (aerc *Aerc) Draw(ctx *libui.Context) {
|
func (aerc *Aerc) Draw(ctx *libui.Context) {
|
||||||
aerc.grid.Draw(ctx)
|
aerc.grid.Draw(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ type ExLine struct {
|
||||||
ctx *ui.Context
|
ctx *ui.Context
|
||||||
cancel func()
|
cancel func()
|
||||||
cells int
|
cells int
|
||||||
|
focus bool
|
||||||
index int
|
index int
|
||||||
scroll int
|
scroll int
|
||||||
|
|
||||||
|
@ -52,6 +53,14 @@ func (ex *ExLine) Draw(ctx *ui.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ex *ExLine) Focus(focus bool) {
|
||||||
|
ex.focus = focus
|
||||||
|
if focus && ex.ctx != nil {
|
||||||
|
cells := runewidth.StringWidth(string(ex.command[:ex.index]))
|
||||||
|
ex.ctx.SetCursor(cells+1, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (ex *ExLine) insert(ch rune) {
|
func (ex *ExLine) insert(ch rune) {
|
||||||
left := ex.command[:ex.index]
|
left := ex.command[:ex.index]
|
||||||
right := ex.command[ex.index:]
|
right := ex.command[ex.index:]
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/aerc2/lib/ui"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/go-libvterm"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/kr/pty"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Terminal struct {
|
||||||
|
closed bool
|
||||||
|
cmd *exec.Cmd
|
||||||
|
ctx *ui.Context
|
||||||
|
cursorPos vterm.Pos
|
||||||
|
cursorShown bool
|
||||||
|
damage []vterm.Rect
|
||||||
|
focus bool
|
||||||
|
onInvalidate func(d ui.Drawable)
|
||||||
|
pty *os.File
|
||||||
|
vterm *vterm.VTerm
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTerminal(cmd *exec.Cmd) (*Terminal, error) {
|
||||||
|
term := &Terminal{}
|
||||||
|
term.cmd = cmd
|
||||||
|
tty, err := pty.Start(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
term.pty = tty
|
||||||
|
rows, cols, err := pty.Getsize(term.pty)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
term.vterm = vterm.New(rows, cols)
|
||||||
|
term.vterm.SetUTF8(true)
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
for {
|
||||||
|
n, err := term.pty.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
term.Close()
|
||||||
|
}
|
||||||
|
n, err = term.vterm.Write(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
term.Close()
|
||||||
|
}
|
||||||
|
term.Invalidate()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
screen := term.vterm.ObtainScreen()
|
||||||
|
screen.OnDamage = term.onDamage
|
||||||
|
screen.OnMoveCursor = term.onMoveCursor
|
||||||
|
screen.Reset(true)
|
||||||
|
return term, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (term *Terminal) Close() {
|
||||||
|
if term.closed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
term.closed = true
|
||||||
|
term.vterm.Close()
|
||||||
|
term.pty.Close()
|
||||||
|
term.cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (term *Terminal) OnInvalidate(cb func(d ui.Drawable)) {
|
||||||
|
term.onInvalidate = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (term *Terminal) Invalidate() {
|
||||||
|
if term.onInvalidate != nil {
|
||||||
|
term.onInvalidate(term)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (term *Terminal) Draw(ctx *ui.Context) {
|
||||||
|
term.ctx = ctx // gross
|
||||||
|
if term.closed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, cols, err := pty.Getsize(term.pty)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.Width() != cols || ctx.Height() != rows {
|
||||||
|
winsize := pty.Winsize{
|
||||||
|
Cols: uint16(ctx.Width()),
|
||||||
|
Rows: uint16(ctx.Height()),
|
||||||
|
}
|
||||||
|
pty.Setsize(term.pty, &winsize)
|
||||||
|
term.vterm.SetSize(ctx.Height(), ctx.Width())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
screen := term.vterm.ObtainScreen()
|
||||||
|
screen.Flush()
|
||||||
|
|
||||||
|
type coords struct {
|
||||||
|
x int
|
||||||
|
y int
|
||||||
|
}
|
||||||
|
|
||||||
|
// naive optimization
|
||||||
|
visited := make(map[coords]interface{})
|
||||||
|
|
||||||
|
for _, rect := range term.damage {
|
||||||
|
for x := rect.StartCol(); x < rect.EndCol() && x < ctx.Width(); x += 1 {
|
||||||
|
|
||||||
|
for y := rect.StartCol(); y < rect.EndCol() && y < ctx.Height(); y += 1 {
|
||||||
|
|
||||||
|
coords := coords{x, y}
|
||||||
|
if _, ok := visited[coords]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visited[coords] = nil
|
||||||
|
|
||||||
|
cell, err := screen.GetCellAt(y, x)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
style := styleFromCell(cell)
|
||||||
|
ctx.Printf(x, y, style, "%s", string(cell.Chars()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (term *Terminal) Focus(focus bool) {
|
||||||
|
term.focus = focus
|
||||||
|
term.resetCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (term *Terminal) Event(event tcell.Event) bool {
|
||||||
|
// TODO
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func styleFromCell(cell *vterm.ScreenCell) tcell.Style {
|
||||||
|
background := cell.Bg()
|
||||||
|
br, bg, bb := background.GetRGB()
|
||||||
|
foreground := cell.Fg()
|
||||||
|
fr, fg, fb := foreground.GetRGB()
|
||||||
|
style := tcell.StyleDefault.
|
||||||
|
Background(tcell.NewRGBColor(int32(br), int32(bg), int32(bb))).
|
||||||
|
Foreground(tcell.NewRGBColor(int32(fr), int32(fg), int32(fb)))
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
func (term *Terminal) onDamage(rect *vterm.Rect) int {
|
||||||
|
term.damage = append(term.damage, *rect)
|
||||||
|
term.Invalidate()
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (term *Terminal) resetCursor() {
|
||||||
|
if term.ctx != nil && term.focus {
|
||||||
|
if !term.cursorShown {
|
||||||
|
term.ctx.HideCursor()
|
||||||
|
} else {
|
||||||
|
term.ctx.SetCursor(term.cursorPos.Col(), term.cursorPos.Row())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (term *Terminal) onMoveCursor(old *vterm.Pos,
|
||||||
|
pos *vterm.Pos, visible bool) int {
|
||||||
|
|
||||||
|
term.cursorShown = visible
|
||||||
|
term.cursorPos = *pos
|
||||||
|
term.resetCursor()
|
||||||
|
return 1
|
||||||
|
}
|
Loading…
Reference in New Issue