Files
lambda/main.go
2026-03-12 16:17:29 +00:00

911 lines
21 KiB
Go

package main
import (
"strings"
"os"
"sync"
"context"
"fmt"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
"github.com/diamondburned/gotk4/pkg/gio/v2"
"github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/gen2brain/beeep"
"github.com/go-analyze/charts"
"golang.org/x/net/html/charset"
"path/filepath"
"github.com/BurntSushi/toml"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
"mellium.im/xmpp/jid"
"time"
_ "embed"
"encoding/xml"
"github.com/kr/pretty"
"io"
"runtime"
)
var loadedConfig lambdaConfig
var empty_dialog *gtk.Image
var connectionStatus *gtk.Label
var connectionIcon *gtk.Image
var mStatus *gtk.Label
var mIcon *gtk.Image
var pingStatus *gtk.Label
// var msgs *gtk.ListBox
var content *gtk.Widgetter
// var tabs map[string]*chatTab = make(map[string]*chatTab)
var tabs sync.Map
var current string
var scroller *gtk.ScrolledWindow
var memberList *gtk.ScrolledWindow
var menu *gtk.Box
var message_en *gtk.Entry
//go:embed style.css
var styleCSS string
var client xmpp.Sender
var clientroot *xmpp.Client
var uiQueue = make(chan func(), 100)
var window *gtk.ApplicationWindow
// stores members of mucs
var mucmembers sync.Map
// stores devices of users
var userdevices sync.Map
var pingTimes = [][]float64{}
var clientAssets map[string]gdk.Paintabler = make(map[string]gdk.Paintabler)
func init() {
beeep.AppName = "Lambda"
go func() {
for fn := range uiQueue {
glib.IdleAdd(func() bool {
fn()
return false
})
time.Sleep(10 * time.Millisecond) // Small delay between updates
}
}()
}
func main() {
pingTimes = append(pingTimes, []float64{})
p, err := ensureConfig()
if err != nil {
panic(err)
}
b, err := os.ReadFile(filepath.Join(p, "lambda.toml"))
if err != nil {
dropToSignInPage(err)
return
// panic(err)
}
_, err = toml.Decode(string(b), &loadedConfig)
if err != nil {
panic(err)
}
if loadedConfig.Resource == "" {
fmt.Println("Config resource is empty! Generating a random one")
loadedConfig.Resource = randomClientResource()
}
config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{
Address: loadedConfig.Server,
CharsetReader: func(c string, input io.Reader) (io.Reader, error) {
return charset.NewReaderLabel(c, input)
},
},
Jid: loadedConfig.Username + "/" + loadedConfig.Resource,
Credential: xmpp.Password(loadedConfig.Password),
Insecure: loadedConfig.Insecure,
// StreamLogger: os.Stdout,
StreamManagementEnable: true,
}
router := xmpp.NewRouter()
router.NewRoute().IQNamespaces(stanza.NSDiscoInfo).HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
iq, ok := p.(*stanza.IQ)
if !ok || iq.Type != stanza.IQTypeGet {
return
}
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
if err != nil {
panic(err)
}
identity := stanza.Identity{
Name: "Lambda",
Category: "client", // TODO: Allow spoofing on user request
Type: "pc",
}
payload := stanza.DiscoInfo{
XMLName: xml.Name{
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: []stanza.Identity{identity},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
{Var: "jabber:iq:version"},
{Var: "urn:xmpp:delegation:1"},
{Var: "http://jabber.org/protocol/muc"},
{Var: "λ"},
{Var: "urn:xmpp:attention:0"},
},
}
iqResp.Payload = &payload
s.Send(iqResp)
})
router.NewRoute().IQNamespaces("jabber:iq:version").HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
iq, ok := p.(*stanza.IQ)
if !ok || iq.Type != stanza.IQTypeGet {
return
}
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
if err != nil {
panic(err)
}
v := &stanza.Version{}
v = v.SetInfo("Lambda", lambda_version, runtime.GOOS) // TODO: Allow spoofing on user request
iqResp.Payload = v
s.Send(iqResp)
})
router.HandleFunc("message", func(s xmpp.Sender, p stanza.Packet) {
m, ok := p.(stanza.Message)
if !ok {
return
}
pretty.Println(m)
e := stanza.PubSubEvent{}
ok = m.Get(&e)
if ok {
fmt.Println(e)
}
/*
if m.Body == "" {
return
}
*/
originator := JidMustParse(m.From).Bare()
mStatus.SetText(originator)
at := new(Attention)
ok = m.Get(at)
if ok {
beeep.Notify("Attention", fmt.Sprintf("%s: %s", JidMustParse(m.From).Resource, m.Body), commentBytes) // TODO: Use localpart if DM
}
sc := new(SentCarbon)
ok = m.Get(sc)
if ok {
fm, ok := sc.Forwarded.Stanza.(stanza.Message)
if ok {
if JidMustParse(fm.From).Bare() == JidMustParse(m.From).Bare() {
p = sc.Forwarded.Stanza
m = sc.Forwarded.Stanza.(stanza.Message)
} else {
panic(fmt.Sprintln("Impersonation: ", fm.From, m.From))
}
}
}
rc := new(SentCarbon)
ok = m.Get(rc)
if ok {
fm, ok := rc.Forwarded.Stanza.(stanza.Message)
if ok {
if JidMustParse(fm.From).Bare() == JidMustParse(m.From).Bare() {
p = rc.Forwarded.Stanza
m = rc.Forwarded.Stanza.(stanza.Message)
} else {
panic(fmt.Sprintln("Impersonation: ", fm.From, m.From))
}
}
}
glib.IdleAdd(func() {
//uiQueue <- func() {
b := gtk.NewBox(gtk.OrientationVertical, 0)
tab, ok := tabs.Load(originator)
if !ok {
return
}
typed_tab := tab.(*chatTab)
if ok {
typed_tab.msgs.Append(b)
scrollToBottomAfterUpdate(scroller)
} else {
fmt.Println("Got message when the tab does not exist!")
}
ba, ok := generateMessageWidget(p).(*gtk.Box)
if ok {
b.Append(ba)
}
//}
})
})
router.HandleFunc("presence", func(s xmpp.Sender, p stanza.Packet) {
presence, ok := p.(stanza.Presence)
if !ok {
return
}
if presence.Error.Reason != "" {
beeep.Notify(fmt.Sprintf("%s : %s", presence.From, presence.Error.Reason), presence.Error.Text, cancelBytes)
return
}
var mu MucUser
var ocu OccupantID
ok = presence.Get(&mu)
if ok { // This is a presence stanza from a user in a MUC
presence.Get(&ocu)
// id := ocu.ID
// if id == "" {
id := JidMustParse(presence.From).Resource
// }
from, _ := stanza.NewJid(presence.From)
muc := from.Bare()
_, ok = mucmembers.Load(muc)
if !ok {
mucmembers.Store(muc, mucUnit{})
}
unit, ok := mucmembers.Load(muc)
if !ok {
return
}
typed_unit := unit.(mucUnit)
if presence.Type != "unavailable" {
_, ok := typed_unit.Members.Load(id)
if !ok {
glib.IdleAdd(func() {
b := gtk.NewBox(gtk.OrientationVertical, 0)
ba, ok := generatePresenceWidget(p).(*gtk.Box)
if ok {
b = ba
}
tab, ok := tabs.Load(muc)
typed_tab := tab.(*chatTab)
if ok {
typed_tab.msgs.Append(b)
scrollToBottomAfterUpdate(scroller)
} else {
fmt.Println("Got message when the tab does not exist!")
}
})
}
typed_unit.Members.Store(id, presence)
} else {
typed_unit.Members.Delete(id)
glib.IdleAdd(func() {
b := gtk.NewBox(gtk.OrientationVertical, 0)
ba, ok := generatePresenceWidget(p).(*gtk.Box)
if ok {
b = ba
}
tab, ok := tabs.Load(muc)
typed_tab := tab.(*chatTab)
if ok {
typed_tab.msgs.Append(b)
scrollToBottomAfterUpdate(scroller)
} else {
fmt.Println("Got message when the tab does not exist!")
}
})
}
mucmembers.Store(muc, typed_unit)
} else { // This is a presence stanza from a regular user
// The code is basically the exact same as above, we just don't check for mucuser
user := jid.MustParse(presence.From).Bare().String()
_, ok := userdevices.Load(user)
_, mok := mucmembers.Load(user)
if !ok && !mok { // FIXME: The initial muc presence gets picked up from this check
ok := createTab(user, false)
if ok {
userdevices.Store(user, userUnit{})
b := gtk.NewLabel(user)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(user, &window.Window)
})
b.AddController(gesture1)
menu.Append(b)
}
}
unit, ok := userdevices.Load(user)
if !ok {
return
}
resource := jid.MustParse(presence.From).Resourcepart()
typed_unit := unit.(userUnit)
if presence.Type != "unavailable" {
typed_unit.Devices.Store(resource, presence)
} else {
typed_unit.Devices.Delete(resource)
}
userdevices.Store(user, typed_unit)
}
time.Sleep(1 * time.Second)
})
c, err := xmpp.NewClient(&config, router, func(err error) {
connectionStatus.SetText(fmt.Sprintf("Disconnected: %s", err.Error()))
connectionIcon.SetFromPaintable(clientAssets["disconnect"])
})
if err != nil {
showErrorDialog(err)
panic(err)
}
client = c
clientroot = c
cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) {
fmt.Println("XMPP client connected")
go func() {
for {
time.Sleep(5 * time.Second)
pingStatus.AddCSSClass("pending")
before := time.Now()
iq := new(stanza.IQ)
iq.From = clientroot.Session.BindJid
iq.To = iq.From
iq.Type = "get"
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
mychan, err := client.SendIQ(ctx, iq)
if err != nil {
continue
}
_ = <-mychan
pingStatus.RemoveCSSClass("pending")
delay := time.Since(before) / time.Millisecond
pingStatus.SetText(fmt.Sprintf("%d ms", delay))
pingTimes[0] = append(pingTimes[0], float64(delay))
}
}()
connectionStatus.SetText(fmt.Sprintf("Connected as %s", JidMustParse(clientroot.Session.BindJid).Bare()))
connectionStatus.SetTooltipText(fmt.Sprintf("Binded JID: %s\nUsing TLS: %t", clientroot.Session.BindJid, clientroot.Session.TlsEnabled))
connectionIcon.SetFromPaintable(clientAssets["connect"])
// Enable carbons
client.SendRaw(fmt.Sprintf(
`<iq xmlns='jabber:client'
from='%s'
id='enable1'
type='set'>
<enable xmlns='urn:xmpp:carbons:2'/>
</iq>
`, clientroot.Session.BindJid))
// Join rooms in bookmarks
if loadedConfig.JoinBookmarks {
books, err := stanza.NewItemsRequest("", "urn:xmpp:bookmarks:1", 0)
if err == nil {
mychan, err := c.SendIQ(context.TODO(), books)
result := <-mychan
if err == nil {
res, ok := result.Payload.(*stanza.PubSubGeneric)
if ok {
for _, item := range res.Items.List {
go func() {
jid := item.Id
node := item.Any
autojoin := false
nick := loadedConfig.Nick
for _, attr := range node.Attrs {
if attr.Name.Local == "autojoin" {
autojoin = attr.Value == "true"
}
}
for _, node := range node.Nodes {
if node.XMLName.Local == "nick" {
nick = node.Content
}
}
_, ok := tabs.Load(jid)
if !ok && autojoin {
err := joinMuc(client, clientroot.Session.BindJid, jid, nick)
if err != nil {
panic(err)
}
createTab(jid, true)
b := gtk.NewLabel(jid)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(jid, &window.Window)
})
b.AddController(gesture1)
menu.Append(b)
}
}()
}
}
}
}
}
})
go func() {
time.Sleep(3 * time.Second)
connectionStatus.SetText("Connecting...")
connectionIcon.SetFromPaintable(clientAssets["hourglass"])
err = cm.Run()
if err != nil {
fmt.Println(err.Error())
connectionStatus.SetText(fmt.Sprintf("Disconnected: %s", err.Error()))
connectionIcon.SetFromPaintable(clientAssets["disconnect"])
}
}()
app := gtk.NewApplication("net.sunglocto.lambda", gio.ApplicationFlagsNone)
app.ConnectActivate(func() { activate(app) })
if code := app.Run(os.Args); code > 0 {
os.Exit(code)
}
}
func activate(app *gtk.Application) {
// Load the CSS and apply it globally.
gtk.StyleContextAddProviderForDisplay(
gdk.DisplayGetDefault(), loadCSS(styleCSS),
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
window = gtk.NewApplicationWindow(app)
the_menu := gio.NewMenu()
fileMenu := gio.NewMenu()
fileMenu.Append("Join MUC", "app.join")
fileMenu.Append("Start DM", "app.dm")
fileMenu.Append("Destroy MUC", "app.destroymuc")
helpMenu := gio.NewMenu()
helpMenu.Append("About", "app.about")
aboutAction := gio.NewSimpleAction("about", nil)
aboutAction.ConnectActivate(func(p *glib.Variant) {
a := gtk.AboutDialog{}
a.SetVisible(true)
})
destroymucAction := gio.NewSimpleAction("destroymuc", nil)
destroymucAction.ConnectActivate(func(p *glib.Variant) {
cur, ok := tabs.Load(current)
if ok {
cur := cur.(*chatTab)
if cur.isMuc {
win := gtk.NewWindow()
win.SetTitle("Destroy MUC")
win.SetDefaultSize(400, 1)
win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(gtk.NewLabel("Are you sure? This MUC will be gone forever! (a very long time)"))
box.Append(gtk.NewLabel("If you wish to continue, type 'I understand'"))
cancel := gtk.NewButtonWithLabel("Cancel")
cancel.ConnectClicked(func() {
win.SetVisible(false)
})
en := gtk.NewEntry()
en.SetPlaceholderText("...")
submit := gtk.NewButtonWithLabel("Destroy")
submit.ConnectClicked(func() {
fmt.Println(en.Text())
if en.Text() == "I understand" {
cur, ok := tabs.Load(current)
if ok {
cur := cur.(*chatTab)
if cur.isMuc {
client.SendRaw(fmt.Sprintf(`
<iq from='%s'
id='begone'
to='%s'
type='set'>
<query xmlns='http://jabber.org/protocol/muc#owner'>
<destroy jid='%s'>
<reason>User requested</reason>
</destroy>
</query>
</iq>
`, clientroot.Session.BindJid, current, JidMustParse(clientroot.Session.BindJid).Bare()))
}
}
win.SetVisible(false)
}
})
box.Append(en)
box.Append(submit)
box.Append(cancel)
mu, ok := mucmembers.Load(current)
if ok {
typed_mu := mu.(mucUnit)
typed_mu.Members.Range(func(k, v any) bool {
user, ok := v.(stanza.Presence)
if ok {
mu := MucUser{}
ok := user.Get(&mu)
if ok {
if mu.MucUserItem.JID != "" {
if JidMustParse(mu.MucUserItem.JID).Bare() == JidMustParse(clientroot.Session.BindJid).Bare() {
if mu.MucUserItem.Affiliation != "owner" {
box.Append(gtk.NewLabel("You are not an owner of this MUC and thus will most likely not be able to delete it"))
}
// return false
}
}
} else {
panic("not ok")
}
} else {
panic("not ok")
}
return true
})
} else {
panic("not ok")
}
win.SetChild(box)
win.SetVisible(true)
}
}
})
joinAction := gio.NewSimpleAction("join", nil)
joinAction.ConnectActivate(func(p *glib.Variant) {
box := gtk.NewBox(gtk.OrientationVertical, 0)
jid_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
nick_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
jid_entry := gtk.NewEntry()
nick_entry := gtk.NewEntry()
jid_entry.SetHAlign(gtk.AlignEnd)
jid_entry.SetHExpand(true)
nick_entry.SetHAlign(gtk.AlignEnd)
nick_entry.SetHExpand(true)
nick_entry.SetText(loadedConfig.Nick)
jid_box.Append(gtk.NewLabel("MUC JID:"))
jid_box.Append(jid_entry)
nick_box.Append(gtk.NewLabel("Nick:"))
nick_box.Append(nick_entry)
box.Append(jid_box)
box.Append(nick_box)
btn := gtk.NewButtonWithLabel("Submit")
btn.SetVAlign(gtk.AlignBaseline)
box.Append(btn)
win := gtk.NewWindow()
win.SetTitle("Join MUC")
win.SetDefaultSize(400, 1)
win.SetResizable(false)
win.SetChild(box)
btn.ConnectClicked(func() {
t := jid_entry.Text()
_, ok := tabs.Load(t)
if !ok {
err := joinMuc(client, clientroot.Session.BindJid, t, nick_entry.Text())
if err != nil {
panic(err)
}
createTab(t, true)
b := gtk.NewLabel(t)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(t, &window.Window)
})
b.AddController(gesture1)
menu.Append(b)
}
win.SetVisible(false)
})
win.SetTransientFor(win)
win.Present()
})
app.AddAction(joinAction)
app.AddAction(aboutAction)
app.AddAction(destroymucAction)
the_menu.AppendSubmenu("File", fileMenu)
the_menu.AppendSubmenu("Help", helpMenu)
the_menuBar := gtk.NewPopoverMenuBarFromModel(the_menu)
app.SetMenubar(gio.NewMenu())
window.SetTitle("Lambda")
window.Window.AddCSSClass("ssd")
window.Window.SetDefaultSize(500, 500)
menu = gtk.NewBox(gtk.OrientationVertical, 0)
empty_dialog = gtk.NewImageFromPaintable(clientAssets["disabled_logo"])
empty_dialog.SetPixelSize(100)
empty_dialog.SetVExpand(true)
scroller = gtk.NewScrolledWindow()
memberList = gtk.NewScrolledWindow()
scroller.SetHExpand(true)
memberList.SetHExpand(true)
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(the_menuBar)
statBar := gtk.NewBox(gtk.OrientationHorizontal, 0)
cBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
connectionIcon = gtk.NewImageFromPaintable((clientAssets["disconnect"]))
connectionIcon.AddCSSClass("icon")
connectionStatus = gtk.NewLabel("Disconnected")
cBox.Append(connectionIcon)
cBox.Append(connectionStatus)
statBar.Append(cBox)
mBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
current = mStatus.Text()
switchToTab(current, &window.Window)
})
mIcon = gtk.NewImageFromPaintable((clientAssets["comment"]))
mIcon.AddCSSClass("icon")
mStatus = gtk.NewLabel("-")
mStatus.AddController(gesture1)
cBox.Append(mIcon)
cBox.Append(mStatus)
statBar.Append(mBox)
pBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
pBox.SetTooltipText("Ping between you and your XMPP server\nRight-click to see graph")
gesture := gtk.NewGestureClick()
gesture.SetButton(3)
gesture.Connect("pressed", func() {
opt := charts.NewLineChartOptionWithData(pingTimes)
opt.Title = charts.TitleOption{
Text: "Server latency",
}
/*
opt.XAxis.Labels = []string{
// The 7 labels here match to the 7 values above
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
}*/
opt.Legend = charts.LegendOption{
SeriesNames: []string{
"Ping (ms)",
},
}
opt.StrokeSmoothingTension = 0.9
p := charts.NewPainter(charts.PainterOptions{
Width: 600,
Height: 400,
})
err := p.LineChart(opt)
if err != nil {
panic(err)
}
buf, err := p.Bytes()
if err != nil {
panic(err)
}
loader := gdkpixbuf.NewPixbufLoader()
loader.Write(buf)
loader.Close()
i := gtk.NewPictureForPaintable(gdk.NewTextureForPixbuf(loader.Pixbuf()))
win := gtk.NewWindow()
win.SetDefaultSize(600, 400)
win.SetTitle("Server latency")
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(i)
win.SetChild(box)
win.SetVisible(true)
})
pBox.AddController(gesture)
i := (gtk.NewImageFromPaintable(clientAssets["chart_bar"]))
i.AddCSSClass("icon")
pBox.Append(i)
pingStatus = gtk.NewLabel("...")
pBox.Append(pingStatus)
statBar.Append(pBox)
scrollerStatBar := gtk.NewScrolledWindow()
scrollerStatBar.SetChild(statBar)
box.Append(scrollerStatBar)
// scroller.SetChild(empty_dialog)
scroller.SetChild(empty_dialog)
menu_scroll := gtk.NewScrolledWindow()
menu_scroll.SetHExpand(true)
menu_scroll.SetChild(menu)
// box.Append(menu_scroll)
chatbox := gtk.NewBox(gtk.OrientationHorizontal, 0)
// chatbox.Append(menu_scroll)
chat_pane := gtk.NewPaned(gtk.OrientationHorizontal)
chat_pane.SetStartChild(scroller)
chat_pane.SetEndChild(memberList)
chat_pane.SetPosition(225)
main_pane := gtk.NewPaned(gtk.OrientationHorizontal)
main_pane.SetStartChild(menu_scroll)
main_pane.SetEndChild(chat_pane)
main_pane.SetPosition(135)
chatbox.Append(main_pane)
box.Append(chatbox)
entry_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
oob_en := gtk.NewEntry()
oob_en.SetPlaceholderText("Embed URL")
message_en = gtk.NewEntry()
message_en.SetPlaceholderText("Say something, what else are you gonna do here?")
b := gtk.NewButtonWithLabel("Send")
sendtxt := func() {
t := message_en.Text()
if t == "" {
dialog := &gtk.AlertDialog{}
dialog.SetDetail("detail")
dialog.SetMessage("message")
dialog.SetButtons([]string{"yes, no"})
dialog.Choose(context.TODO(), &window.Window, nil)
}
message_type := stanza.MessageTypeChat
tab, ok := tabs.Load(current)
if !ok {
return
}
typed_tab := tab.(*chatTab)
if typed_tab.isMuc {
message_type = stanza.MessageTypeGroupchat
}
exts := []stanza.MsgExtension{}
if oob_en.Text() != "" {
new_oob := new(stanza.OOB)
new_oob.URL = oob_en.Text()
exts = append(exts, new_oob)
}
if strings.Contains(t, "@everyone") {
new_mention := new(Mention)
new_mention.Mentions = "urn:xmpp:mentions:0#channel"
exts = append(exts, new_mention)
} else if strings.Contains(t, "@here") {
new_attention := new(Attention)
exts = append(exts, new_attention)
}
err := sendMessage(client, current, message_type, t, "", "", exts)
if err != nil {
panic(err) // TODO: Show error message via GTK
}
message_en.SetText("")
scrollToBottomAfterUpdate(scroller)
}
message_en.Connect("activate", sendtxt)
b.ConnectClicked(sendtxt)
message_en.SetHExpand(true)
entry_box.Append(oob_en)
entry_box.Append(message_en)
entry_box.Append(b)
box.Append(entry_box)
window.SetChild(box)
window.SetVisible(true)
}
func loadCSS(content string) *gtk.CSSProvider {
prov := gtk.NewCSSProvider()
prov.LoadFromString(content)
return prov
}