1
0
mirror of https://shylinux.com/x/icebergs synced 2025-04-26 01:24:05 +08:00
This commit is contained in:
harveyshao 2022-08-16 14:38:36 +08:00
parent 4bbdd8d9e4
commit bf34e9ceee
9 changed files with 101 additions and 141 deletions

View File

@ -62,6 +62,10 @@ func _hash_delete(m *ice.Message, prefix, chain, field, value string) {
defer Lock(m, prefix, chain)()
Richs(m, prefix, chain, value, func(key string, val Map) {
if target, ok := kit.GetMeta(val)[TARGET].(io.Closer); ok {
m.Logs("close", target)
target.Close()
}
m.Logs(DELETE, KEY, path.Join(prefix, chain), field, value, VALUE, kit.Format(val))
m.Conf(prefix, kit.Keys(chain, HASH, key), "")
})

View File

@ -182,9 +182,9 @@ func ZoneCreate(m *ice.Message, arg ...Any) {
m.Cmdy(INSERT, m.PrefixKey(), "", HASH, arg)
}
func ZoneRemove(m *ice.Message, arg ...Any) {
args := kit.Simple(arg)
args := kit.Simple(arg...)
if len(args) == 0 {
args = m.OptionSimple(ZoneShort(m))
args = m.OptionSimple(ZoneShort(m), HASH)
} else if len(args) == 1 {
args = []string{ZoneShort(m), args[0]}
}

View File

@ -87,6 +87,7 @@ const (
)
const DEFS = "defs"
const SAVE = "save"
const LOAD = "load"
const PUSH = "push"
const COPY = "copy"
const LINK = "link"

View File

@ -5,14 +5,15 @@ import (
"io"
"net"
"os"
"path"
"strings"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
ice "shylinux.com/x/icebergs"
"shylinux.com/x/icebergs/base/aaa"
"shylinux.com/x/icebergs/base/cli"
"shylinux.com/x/icebergs/base/ctx"
"shylinux.com/x/icebergs/base/gdb"
"shylinux.com/x/icebergs/base/mdb"
"shylinux.com/x/icebergs/base/nfs"
@ -44,10 +45,10 @@ func _ssh_open(m *ice.Message, arg ...string) {
}, arg...)
}
func _ssh_dial(m *ice.Message, cb func(net.Conn), arg ...string) {
p := path.Join(kit.Env(cli.HOME), ".ssh/", fmt.Sprintf("%s@%s:%s", m.Option(aaa.USERNAME), m.Option(tcp.HOST), m.Option(tcp.PORT)))
if nfs.FileExists(m, p) {
p := kit.HomePath(".ssh", fmt.Sprintf("%s@%s:%s", m.Option(aaa.USERNAME), m.Option(tcp.HOST), m.Option(tcp.PORT)))
if nfs.ExistsFile(m, p) {
if c, e := net.Dial("unix", p); e == nil {
cb(c) // 会话连接
cb(c) // 会话复用
return
}
nfs.Remove(m, p)
@ -75,28 +76,21 @@ func _ssh_dial(m *ice.Message, cb func(net.Conn), arg ...string) {
m.Go(func() {
defer c.Close()
session, e := client.NewSession()
s, e := client.NewSession()
if e != nil {
return
}
session.Stdin = c
session.Stdout = c
session.Stderr = c
session.RequestPty(kit.Env("TERM"), h, w, ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
})
s.Stdin, s.Stdout, s.Stderr = c, c, c
s.RequestPty(kit.Env(cli.TERM), h, w, ssh.TerminalModes{ssh.ECHO: 1, ssh.TTY_OP_ISPEED: 14400, ssh.TTY_OP_OSPEED: 14400})
defer s.Wait()
gdb.SignalNotify(m, 28, func() {
w, h, _ := terminal.GetSize(int(os.Stdin.Fd()))
session.WindowChange(h, w)
s.WindowChange(h, w)
})
session.Shell()
session.Wait()
s.Shell()
})
}(c)
}
@ -118,7 +112,6 @@ func _ssh_conn(m *ice.Message, cb func(*ssh.Client), arg ...string) {
if verify := m.Option("verify"); verify == "" {
fmt.Printf(q)
fmt.Scanf("%s\n", &verify)
res = append(res, verify)
} else {
res = append(res, aaa.TOTP_GET(verify, 6, 30))
@ -131,24 +124,21 @@ func _ssh_conn(m *ice.Message, cb func(*ssh.Client), arg ...string) {
return
}))
methods = append(methods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) {
key, err := ssh.ParsePrivateKey([]byte(m.Cmdx(nfs.CAT, path.Join(kit.Env(cli.HOME), m.Option(PRIVATE)))))
key, err := ssh.ParsePrivateKey([]byte(m.Cmdx(nfs.CAT, kit.HomePath(m.Option(PRIVATE)))))
return []ssh.Signer{key}, err
}))
methods = append(methods, ssh.PasswordCallback(func() (string, error) {
return m.Option(aaa.PASSWORD), nil
}))
m.OptionCB(tcp.CLIENT, func(c net.Conn) {
m.Cmdy(tcp.CLIENT, tcp.DIAL, mdb.TYPE, SSH, mdb.NAME, m.Option(tcp.HOST), m.OptionSimple(tcp.HOST, tcp.PORT), arg, func(c net.Conn) {
conn, chans, reqs, err := ssh.NewClientConn(c, m.Option(tcp.HOST)+":"+m.Option(tcp.PORT), &ssh.ClientConfig{
User: m.Option(aaa.USERNAME), Auth: methods, BannerCallback: func(message string) error { return nil },
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil },
})
m.Assert(err)
cb(ssh.NewClient(conn, chans, reqs))
})
m.Cmdy(tcp.CLIENT, tcp.DIAL, mdb.TYPE, SSH, mdb.NAME, m.Option(tcp.HOST),
tcp.PORT, m.Option(tcp.PORT), tcp.HOST, m.Option(tcp.HOST), arg)
}
const SSH = "ssh"
@ -160,52 +150,36 @@ func init() {
tcp.OPEN: {Name: "open authfile username=shy password verfiy host=shylinux.com port=22 private=.ssh/id_rsa", Help: "终端", Hand: func(m *ice.Message, arg ...string) {
aaa.UserRoot(m)
_ssh_open(nfs.OptionLoad(m, m.Option("authfile")), arg...)
m.Echo("exit %v@%v:%v\n", m.Option(aaa.USERNAME), m.Option(tcp.HOST), m.Option(tcp.PORT))
m.Echo("exit %s@%s:%s\n", m.Option(aaa.USERNAME), m.Option(tcp.HOST), m.Option(tcp.PORT))
}},
tcp.DIAL: {Name: "dial name=shylinux username=shy host=shylinux.com port=22 private=.ssh/id_rsa", Help: "添加", Hand: func(m *ice.Message, arg ...string) {
tcp.DIAL: {Name: "dial name=shylinux host=shylinux.com port=22 username=shy private=.ssh/id_rsa", Help: "添加", Hand: func(m *ice.Message, arg ...string) {
m.Go(func() {
_ssh_conn(m, func(client *ssh.Client) {
mdb.Rich(m, CONNECT, "", kit.Dict(
mdb.NAME, m.Option(mdb.NAME),
aaa.USERNAME, m.Option(aaa.USERNAME),
tcp.HOST, m.Option(tcp.HOST), tcp.PORT, m.Option(tcp.PORT),
mdb.STATUS, tcp.OPEN, CONNECT, client,
))
m.Cmd(CONNECT, SESSION, mdb.NAME, m.Option(mdb.NAME))
mdb.HashCreate(m.Spawn(), m.OptionSimple(mdb.NAME, tcp.HOST, tcp.PORT, aaa.USERNAME), mdb.STATUS, tcp.OPEN, kit.Dict(mdb.TARGET, client))
m.Cmd("", SESSION, m.OptionSimple(mdb.NAME))
}, arg...)
})
m.ProcessRefresh3s()
m.Sleep300ms()
}},
SESSION: {Name: "session name", Help: "会话", Hand: func(m *ice.Message, arg ...string) {
var client *ssh.Client
mdb.Richs(m, CONNECT, "", m.Option(mdb.NAME), func(key string, value ice.Map) {
client, _ = value[CONNECT].(*ssh.Client)
})
h := mdb.Rich(m, SESSION, "", kit.Data(mdb.NAME, m.Option(mdb.NAME), mdb.STATUS, tcp.OPEN, CONNECT, m.Option(mdb.NAME)))
if session, e := _ssh_session(m, h, client); m.Assert(e) {
session.Shell()
session.Wait()
SESSION: {Name: "session", Help: "会话", Hand: func(m *ice.Message, arg ...string) {
if c, e := _ssh_session(m, mdb.HashTarget(m, m.Option(mdb.NAME), nil).(*ssh.Client)); m.Assert(e) {
defer c.Wait()
c.Shell()
}
m.Echo(h)
}},
"command": {Name: "command cmd=pwd", Help: "命令", Hand: func(m *ice.Message, arg ...string) {
mdb.Richs(m, CONNECT, "", m.Option(mdb.NAME), func(key string, value ice.Map) {
if client, ok := value[CONNECT].(*ssh.Client); ok {
if session, e := client.NewSession(); m.Assert(e) {
defer session.Close()
if b, e := session.CombinedOutput(m.Option("cmd")); m.Assert(e) {
m.Echo(string(b))
}
}
ctx.COMMAND: {Name: "command cmd=pwd", Help: "命令", Hand: func(m *ice.Message, arg ...string) {
client := mdb.HashTarget(m, m.Option(mdb.NAME), nil).(*ssh.Client)
if s, e := client.NewSession(); m.Assert(e) {
defer s.Close()
if b, e := s.CombinedOutput(m.Option(ice.CMD)); m.Assert(e) {
m.Echo(string(b))
}
})
}
}},
}, mdb.HashStatusAction(mdb.SHORT, "name", mdb.FIELD, "time,name,status,username,host,port")), Hand: func(m *ice.Message, arg ...string) {
mdb.HashSelect(m, arg...).Tables(func(value ice.Maps) {
if mdb.HashSelect(m, arg...).Tables(func(value ice.Maps) {
m.PushButton(kit.Select("", "command,session", value[mdb.STATUS] == tcp.OPEN), mdb.REMOVE)
})
if len(arg) == 0 {
}); len(arg) == 0 {
m.Action(tcp.DIAL)
}
}},

View File

@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"net"
"path"
"strings"
"golang.org/x/crypto/ssh"
@ -17,6 +16,7 @@ import (
"shylinux.com/x/icebergs/base/nfs"
psh "shylinux.com/x/icebergs/base/ssh"
"shylinux.com/x/icebergs/base/tcp"
"shylinux.com/x/icebergs/base/web"
kit "shylinux.com/x/toolkits"
)
@ -27,11 +27,11 @@ func _ssh_config(m *ice.Message, h string) *ssh.ServerConfig {
config := &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
meta, err := _ssh_meta(conn), errors.New(ice.ErrNotRight)
if tcp.IsLocalHost(m, strings.Split(conn.RemoteAddr().String(), ":")[0]) {
if tcp.IsLocalHost(m, strings.Split(conn.RemoteAddr().String(), ice.DF)[0]) {
m.Logs(ice.LOG_AUTH, tcp.HOSTPORT, conn.RemoteAddr(), aaa.USERNAME, conn.User())
err = nil // 本机用户
} else {
m.Cmd(mdb.SELECT, SERVICE, kit.Keys(mdb.HASH, h), mdb.LIST, func(value ice.Maps) {
mdb.ZoneSelectCB(m, h, func(value ice.Maps) {
if !strings.HasPrefix(value[mdb.NAME], conn.User()+"@") {
return
}
@ -57,14 +57,13 @@ func _ssh_config(m *ice.Message, h string) *ssh.ServerConfig {
}
return &ssh.Permissions{Extensions: meta}, err
},
BannerCallback: func(conn ssh.ConnMetadata) string {
m.Logs(ice.LOG_AUTH, tcp.HOSTPORT, conn.RemoteAddr(), aaa.USERNAME, conn.User())
return m.Conf(SERVICE, kit.Keym(WELCOME))
return m.Config(WELCOME)
},
}
if key, err := ssh.ParsePrivateKey([]byte(m.Cmdx(nfs.CAT, path.Join(kit.Env(cli.HOME), m.Option(PRIVATE))))); m.Assert(err) {
if key, err := ssh.ParsePrivateKey([]byte(m.Cmdx(nfs.CAT, kit.HomePath(m.Option(PRIVATE))))); m.Assert(err) {
config.AddHostKey(key)
}
return config
@ -100,30 +99,30 @@ const SERVICE = "service"
func init() {
psh.Index.Merge(&ice.Context{Configs: ice.Configs{
SERVICE: {Name: SERVICE, Help: "服务", Value: kit.Data(
WELCOME, "\r\nwelcome to context world\r\n", GOODBYE, "\r\ngoodbye of context world\r\n",
mdb.SHORT, tcp.PORT, mdb.FIELD, "time,port,status,private,authkey,count",
WELCOME, "\r\nwelcome to contexts world\r\n", GOODBYE, "\r\ngoodbye of contexts world\r\n",
)},
}, Commands: ice.Commands{
SERVICE: {Name: "service port id auto listen prunes", Help: "服务", Actions: ice.MergeActions(ice.Actions{
ice.CTX_INIT: {Hand: func(m *ice.Message, arg ...string) {
mdb.Richs(m, SERVICE, "", mdb.FOREACH, func(key string, value ice.Map) {
if value = kit.GetMeta(value); kit.Value(value, mdb.STATUS) == tcp.OPEN {
mdb.HashSelect(m).Tables(func(value ice.Maps) {
if value[mdb.STATUS] == tcp.OPEN {
m.Cmd(SERVICE, tcp.LISTEN, tcp.PORT, value[tcp.PORT], value)
}
})
}},
tcp.LISTEN: {Name: "listen port=9030 private=.ssh/id_rsa authkey=.ssh/authorized_keys", Help: "添加", Hand: func(m *ice.Message, arg ...string) {
if mdb.Richs(m, SERVICE, "", m.Option(tcp.PORT), func(key string, value ice.Map) {
kit.Value(value, kit.Keym(mdb.STATUS), tcp.OPEN)
}) == nil {
m.Cmd(mdb.INSERT, SERVICE, "", mdb.HASH, tcp.PORT, m.Option(tcp.PORT),
PRIVATE, m.Option(PRIVATE), AUTHKEY, m.Option(AUTHKEY), mdb.STATUS, tcp.OPEN, arg)
m.Cmd(SERVICE, mdb.IMPORT, AUTHKEY, m.Option(AUTHKEY))
if mdb.HashSelect(m, m.Option(tcp.PORT)).Length() > 0 {
mdb.HashModify(m, m.Option(tcp.PORT), mdb.STATUS, tcp.OPEN)
} else {
mdb.HashCreate(m, mdb.STATUS, tcp.OPEN, arg)
m.Cmd("", nfs.LOAD, m.OptionSimple(AUTHKEY))
}
m.OptionCB(tcp.SERVER, func(c net.Conn) { m.Go(func() { _ssh_accept(m, kit.Hashs(m.Option(tcp.PORT)), c) }) })
m.Go(func() {
m.Cmdy(tcp.SERVER, tcp.LISTEN, mdb.TYPE, SSH, mdb.NAME, tcp.PORT, tcp.PORT, m.Option(tcp.PORT))
m.Cmdy(tcp.SERVER, tcp.LISTEN, mdb.TYPE, SSH, mdb.NAME, tcp.PORT, m.OptionSimple(tcp.PORT), func(c net.Conn) {
m.Go(func() { _ssh_accept(m, kit.Hashs(m.Option(tcp.PORT)), c) })
})
})
}},
@ -133,36 +132,29 @@ func init() {
mdb.TYPE, ls[0], mdb.NAME, ls[len(ls)-1], mdb.TEXT, strings.Join(ls[1:len(ls)-1], "+"))
}
}},
mdb.EXPORT: {Name: "export authkey=.ssh/authorized_keys", Help: "导出", Hand: func(m *ice.Message, arg ...string) {
nfs.LOAD: {Name: "load authkey=.ssh/authorized_keys", Help: "加载", Hand: func(m *ice.Message, arg ...string) {
m.Cmd(nfs.CAT, kit.HomePath(m.Option(AUTHKEY)), func(pub string) {
m.Cmd(SERVICE, mdb.INSERT, mdb.TEXT, pub)
})
}},
nfs.SAVE: {Name: "save authkey=.ssh/authorized_keys", Help: "保存", Hand: func(m *ice.Message, arg ...string) {
list := []string{}
m.Cmd(mdb.SELECT, SERVICE, kit.Keys(mdb.HASH, kit.Hashs(m.Option(tcp.PORT))), mdb.LIST, func(value ice.Maps) {
mdb.ZoneSelectCB(m, m.Option(tcp.PORT), func(value ice.Maps) {
list = append(list, fmt.Sprintf("%s %s %s", value[mdb.TYPE], value[mdb.TEXT], value[mdb.NAME]))
})
if len(list) > 0 {
m.Cmdy(nfs.SAVE, path.Join(kit.Env(cli.HOME), m.Option(AUTHKEY)), strings.Join(list, ice.NL)+ice.NL)
m.Cmdy(nfs.SAVE, kit.HomePath(m.Option(AUTHKEY)), strings.Join(list, ice.NL)+ice.NL)
}
}},
mdb.IMPORT: {Name: "import authkey=.ssh/authorized_keys", Help: "导入", Hand: func(m *ice.Message, arg ...string) {
p := path.Join(kit.Env(cli.HOME), m.Option(AUTHKEY))
for _, pub := range strings.Split(strings.TrimSpace(m.Cmdx(nfs.CAT, p)), ice.NL) {
m.Cmd(SERVICE, mdb.INSERT, mdb.TEXT, pub)
}
m.Echo(p)
}},
aaa.INVITE: {Name: "invite", Help: "邀请", Hand: func(m *ice.Message, arg ...string) {
u := kit.ParseURL(m.Option(ice.MSG_USERWEB))
m.Option(cli.HOSTNAME, strings.Split(u.Host, ":")[0])
m.ProcessInner()
m.Option(cli.HOSTNAME, web.OptionUserWeb(m).Hostname())
if buf, err := kit.Render(`ssh -p {{.Option "port"}} {{.Option "user.name"}}@{{.Option "hostname"}}`, m); err == nil {
m.EchoScript(string(buf))
}
}},
}, mdb.HashStatusAction()), Hand: func(m *ice.Message, arg ...string) {
if len(arg) == 0 { // 服务列表
mdb.HashSelect(m, arg...)
m.PushAction(mdb.IMPORT, mdb.INSERT, mdb.EXPORT, aaa.INVITE)
mdb.HashSelect(m, arg...).PushAction(aaa.INVITE, mdb.INSERT, nfs.LOAD, nfs.SAVE)
return
}

View File

@ -7,8 +7,9 @@ import (
"syscall"
"unsafe"
pty "shylinux.com/x/creackpty"
"golang.org/x/crypto/ssh"
pty "shylinux.com/x/creackpty"
ice "shylinux.com/x/icebergs"
"shylinux.com/x/icebergs/base/cli"
"shylinux.com/x/icebergs/base/mdb"
@ -33,7 +34,7 @@ func _ssh_handle(m *ice.Message, meta ice.Maps, c net.Conn, channel ssh.Channel,
m.Logs(CHANNEL, tcp.HOSTPORT, c.RemoteAddr(), "->", c.LocalAddr())
defer m.Logs("dischan", tcp.HOSTPORT, c.RemoteAddr(), "->", c.LocalAddr())
shell := kit.Select("bash", kit.Env("SHELL"))
shell := kit.Select("bash", kit.Env(cli.SHELL))
list := []string{cli.PATH + "=" + kit.Env(cli.PATH)}
pty, tty, err := pty.Open()

View File

@ -4,6 +4,7 @@ import (
"io"
"golang.org/x/crypto/ssh"
ice "shylinux.com/x/icebergs"
"shylinux.com/x/icebergs/base/ctx"
"shylinux.com/x/icebergs/base/mdb"
@ -12,35 +13,27 @@ import (
kit "shylinux.com/x/toolkits"
)
func _ssh_session(m *ice.Message, h string, client *ssh.Client) (*ssh.Session, error) {
session, e := client.NewSession()
func _ssh_session(m *ice.Message, client *ssh.Client) (*ssh.Session, error) {
s, e := client.NewSession()
m.Assert(e)
out, e := s.StdoutPipe()
m.Assert(e)
in, e := s.StdinPipe()
m.Assert(e)
out, e := session.StdoutPipe()
m.Assert(e)
in, e := session.StdinPipe()
m.Assert(e)
h := m.Cmdx(SESSION, mdb.CREATE, mdb.STATUS, tcp.OPEN, CONNECT, m.Option(mdb.NAME), kit.Dict(mdb.TARGET, in))
m.Go(func() {
buf := make([]byte, ice.MOD_BUFS)
for {
n, e := out.Read(buf)
if e != nil {
if n, e := out.Read(buf); e != nil {
break
} else {
m.Cmd(SESSION, mdb.INSERT, mdb.HASH, h, mdb.TYPE, RES, mdb.TEXT, string(buf[:n]))
}
mdb.Grow(m, SESSION, kit.Keys(mdb.HASH, h), kit.Dict(
mdb.TYPE, RES, mdb.TEXT, string(buf[:n]),
))
}
})
mdb.Richs(m, SESSION, "", h, func(key string, value ice.Map) {
kit.Value(value, kit.Keym(OUTPUT), out, kit.Keym(INPUT), in)
})
return session, nil
return s, nil
}
const (
@ -59,20 +52,17 @@ const SESSION = "session"
func init() {
psh.Index.MergeCommands(ice.Commands{
SESSION: {Name: "session name id auto", Help: "会话", Actions: ice.MergeActions(ice.Actions{
SESSION: {Name: "session hash id auto", Help: "会话", Actions: ice.MergeActions(ice.Actions{
mdb.REPEAT: {Name: "repeat", Help: "执行", Hand: func(m *ice.Message, arg ...string) {
m.Cmdy(SESSION, ctx.ACTION, ctx.COMMAND, CMD, m.Option(mdb.TEXT))
m.Cmdy("", ctx.COMMAND, CMD, m.Option(mdb.TEXT))
}},
ctx.COMMAND: {Name: "command cmd=pwd", Help: "命令", Hand: func(m *ice.Message, arg ...string) {
mdb.Richs(m, SESSION, "", m.Option(mdb.NAME), func(key string, value ice.Map) {
if w, ok := kit.Value(value, kit.Keym(INPUT)).(io.Writer); ok {
mdb.Grow(m, SESSION, kit.Keys(mdb.HASH, key), kit.Dict(mdb.TYPE, CMD, mdb.TEXT, m.Option(CMD)))
w.Write([]byte(m.Option(CMD) + ice.NL))
}
})
m.ProcessRefresh300ms()
m.Cmd("", mdb.INSERT, m.OptionSimple(mdb.HASH), mdb.TYPE, CMD, mdb.TEXT, m.Option(CMD))
w := mdb.HashTarget(m, m.Option(mdb.HASH), nil).(io.Writer)
w.Write([]byte(m.Option(CMD) + ice.NL))
m.Sleep300ms()
}},
}, mdb.ZoneAction(mdb.SHORT, "name", mdb.FIELD, "time,name,status,count,connect")), Hand: func(m *ice.Message, arg ...string) {
}, mdb.ZoneAction(mdb.FIELD, "time,hash,count,status,connect")), Hand: func(m *ice.Message, arg ...string) {
if len(arg) == 0 {
mdb.HashSelect(m, arg...).Tables(func(value ice.Maps) {
m.PushButton(kit.Select("", ctx.COMMAND, value[mdb.STATUS] == tcp.OPEN), mdb.REMOVE)

View File

@ -6,7 +6,7 @@ refer `
`
field "连接" ssh.connect
field "通道" ssh.channel
field "会话" ssh.session
field "服务" ssh.service
field "通道" ssh.channel

View File

@ -8,8 +8,8 @@ import (
)
type WebView struct {
Source string
WebView webview.WebView
webview.WebView
Source string
}
func (w WebView) Menu() bool {
@ -20,31 +20,29 @@ func (w WebView) Menu() bool {
w.WebView.Bind(ls[0], func() { w.navigate(ls[1]) })
}
})
if len(list) == 0 {
return false
}
w.WebView.SetTitle("contexts")
w.WebView.SetTitle(ice.CONTEXTS)
w.WebView.SetSize(200, 60*len(list), webview.HintNone)
w.WebView.Navigate(kit.Format(`data:text/html,
<!doctype html>
<html>
<head>
<!doctype html>
<html>
<head>
<style>button { font-size:24px; font-family:monospace; margin:10px; width:-webkit-fill-available; display:block; clear:both; }</style>
</head>
<body>%s</body>
<script>
document.body.onkeydown = function(event) {
if (event.metaKey) {
switch (event.key) {
case "q": window.terminate(); break
document.body.onkeydown = function(event) {
if (event.metaKey) {
switch (event.key) {
case "q": window.terminate(); break
}
}
}
}
</script>
</html>`, kit.Join(list, ice.NL)))
</head>
<body>%s</body>
</html>`, kit.Join(list, ice.NL)))
return true
}
func (w WebView) Title(text string) { w.WebView.SetTitle(text) }