1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-gtk.git synced 2024-12-05 04:14:56 +00:00

Working prototype

This commit is contained in:
diamondburned (Forefront) 2020-05-28 12:26:55 -07:00
parent 275ae709ae
commit ee657a8177
12 changed files with 195 additions and 83 deletions

1
go.mod
View file

@ -4,6 +4,7 @@ go 1.14
require (
github.com/Xuanwo/go-locale v0.2.0
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/diamondburned/cchat v0.0.9
github.com/diamondburned/cchat-mock v0.0.0-20200525222906-807afeffb7d4
github.com/goodsign/monday v1.0.0

14
go.sum
View file

@ -3,8 +3,8 @@ github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqC
github.com/Xuanwo/go-locale v0.2.0 h1:1N8SGG2VNpLl6VVa8ueZm3Nm+dxvk8ffY9aviKHl4IE=
github.com/Xuanwo/go-locale v0.2.0/go.mod h1:6qbT9M726OJgyiGZro2YwPmx63wQzlH+VvtjJWQoftw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diamondburned/cchat v0.0.8 h1:/PmI23SFHJcjYBWNBwQbp36n7fDvDu+NMnQuhM5FM2E=
github.com/diamondburned/cchat v0.0.8/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diamondburned/cchat v0.0.9 h1:+F96eDDuaOg4v4dz3GBDWbEW4dZ/k5uGrDp33/yeXR8=
github.com/diamondburned/cchat v0.0.9/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat-mock v0.0.0-20200525222906-807afeffb7d4 h1:k6vfDs6NR8yIi2V7YEpNp9Vujtl6mpDL5cWh7Pg09kk=
@ -13,18 +13,22 @@ github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/goodsign/monday v1.0.0 h1:Yyk/s/WgudMbAJN6UWSU5xAs8jtNewfqtVblAlw0yoc=
github.com/goodsign/monday v1.0.0/go.mod h1:r4T4breXpoFwspQNM+u2sLxJb2zyTaxVGqUfTBjWOu8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gotk3/gotk3 v0.4.0 h1:TIuhyQitGeRTxOQIV3AJlYtEWWJpC74JHwAIsxlH8MU=
github.com/gotk3/gotk3 v0.4.0/go.mod h1:Eew3QBwAOBTrfFFDmsDE5wZWbcagBL1NUslj1GhRveo=
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194 h1:bB6XWpxMt2isCWqzjXN8tfVazjxvD8nRJrNoKcL0xAc=
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -34,5 +38,7 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View file

@ -48,12 +48,13 @@ type WindowHeaderer interface {
func Main(wfn func() WindowHeaderer) {
App.Application.Connect("activate", func() {
App.Header, _ = gtk.HeaderBarNew()
App.Header.Show()
App.Header.SetShowCloseButton(true)
App.Header.Show()
App.Window, _ = gtk.ApplicationWindowNew(App.Application)
App.Window.Show()
App.Window.SetDefaultSize(750, 400)
App.Window.SetTitlebar(App.Header)
App.Window.Show()
// Execute the function later, because we need it to run after
// initialization.

View file

@ -8,23 +8,31 @@ import (
type Container struct {
*gtk.ScrolledWindow
main *gtk.Box
main *gtk.Grid
messages map[string]Message
}
func NewContainer() *Container {
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 3)
box.Show()
grid, _ := gtk.GridNew()
grid.SetColumnSpacing(8)
grid.SetRowSpacing(5)
grid.SetMarginStart(5)
grid.SetMarginEnd(5)
grid.Show()
sw, _ := gtk.ScrolledWindowNew(nil, nil)
sw.Add(grid)
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
sw.Show()
return &Container{sw, box, map[string]Message{}}
return &Container{sw, grid, map[string]Message{}}
}
func (c *Container) Reset() {
for _, msg := range c.messages {
c.main.Remove(msg)
// does this actually work?
var rows = len(c.messages)
for i := 0; i < rows; i++ {
c.main.RemoveRow(i)
}
c.messages = nil
@ -32,9 +40,11 @@ func (c *Container) Reset() {
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() {
var msgc = NewMessage(msg)
msgc := NewMessage(msg)
msgc.index = len(c.messages) // unsure
c.messages[msgc.ID] = msgc
c.main.Add(msgc)
msgc.Attach(c.main, msgc.index)
})
}
@ -58,7 +68,7 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
gts.ExecAsync(func() {
if m, ok := c.messages[msg.ID()]; ok {
delete(c.messages, msg.ID())
c.main.Remove(m)
c.main.RemoveRow(m.index)
}
})
}

View file

@ -11,38 +11,40 @@ import (
)
type Message struct {
index int
ID string
Nonce string
*gtk.Box
Timestamp *gtk.Label
Username *gtk.Label
Content *gtk.Label
}
func NewMessage(msg cchat.MessageCreate) Message {
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 3)
box.Show()
ts, _ := gtk.LabelNew("")
ts.Show()
ts.SetWidthChars(12)
ts.SetLineWrap(true)
ts.SetLineWrapMode(pango.WRAP_WORD)
ts.SetHAlign(gtk.ALIGN_END)
ts.SetVAlign(gtk.ALIGN_START)
ts.Show()
user, _ := gtk.LabelNew("")
user.Show()
user.SetLineWrap(true)
user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
user.SetHAlign(gtk.ALIGN_END)
user.SetVAlign(gtk.ALIGN_START)
user.Show()
content, _ := gtk.LabelNew("")
content.SetHExpand(true)
content.SetXAlign(0) // left-align with size filled
content.SetVAlign(gtk.ALIGN_START)
content.SetLineWrap(true)
content.SetLineWrapMode(pango.WRAP_WORD_CHAR)
content.Show()
box.PackStart(ts, false, false, 0)
box.PackStart(user, false, false, 0)
box.PackStart(content, true, true, 0)
m := Message{
ID: msg.ID(),
Box: box,
Timestamp: ts,
Username: user,
Content: content,
@ -58,6 +60,12 @@ func NewMessage(msg cchat.MessageCreate) Message {
return m
}
func (m *Message) Attach(grid *gtk.Grid, row int) {
grid.Attach(m.Timestamp, 0, row, 1, 1)
grid.Attach(m.Username, 1, row, 1, 1)
grid.Attach(m.Content, 2, row, 1, 1)
}
func (m *Message) UpdateTimestamp(t time.Time) {
m.Timestamp.SetLabel(humanize.TimeAgo(t))
}

View file

@ -21,9 +21,9 @@ func NewView() *View {
sendinput := input.NewField()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.Show()
box.PackStart(container, true, true, 0)
box.PackStart(sendinput, false, false, 0)
box.Show()
return &View{
Box: box,
@ -37,6 +37,9 @@ func (v *View) JoinServer(server cchat.ServerMessage) {
if err := v.current.LeaveServer(); err != nil {
log.Error(errors.Wrap(err, "Error leaving server"))
}
// Clean all messages.
v.Container.Reset()
}
v.current = server

View file

@ -0,0 +1,25 @@
package primitives
import "github.com/gotk3/gotk3/gtk"
type StyleContexter interface {
GetStyleContext() (*gtk.StyleContext, error)
}
func AddClass(styleCtx StyleContexter, classes ...string) {
var style, _ = styleCtx.GetStyleContext()
for _, class := range classes {
style.AddClass(class)
}
}
type Bin interface {
GetChild() (gtk.IWidget, error)
}
var _ Bin = (*gtk.Bin)(nil)
func BinLeftAlignLabel(bin Bin) {
widget, _ := bin.GetChild()
widget.(interface{ SetXAlign(float64) }).SetXAlign(0)
}

View file

@ -2,8 +2,10 @@ package service
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/service/auth"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/gotk3/gotk3/gtk"
)
@ -17,9 +19,12 @@ func NewView() *View {
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.Show()
primitives.AddClass(box, "services")
sw, _ := gtk.ScrolledWindowNew(nil, nil)
sw.Show()
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
sw.Add(box)
sw.Show()
return &View{
sw,
@ -28,8 +33,8 @@ func NewView() *View {
}
}
func (v *View) AddService(svc cchat.Service) {
s := NewContainer(svc)
func (v *View) AddService(svc cchat.Service, rowctrl server.RowController) {
s := NewContainer(svc, rowctrl)
v.Services = append(v.Services, s)
v.Box.Add(s)
}
@ -39,24 +44,36 @@ type Container struct {
header *header
revealer *gtk.Revealer
children *children
rowctrl server.RowController
}
func NewContainer(svc cchat.Service) *Container {
func NewContainer(svc cchat.Service, rowctrl server.RowController) *Container {
header := newHeader(svc)
children := newChildren()
chrev, _ := gtk.RevealerNew()
chrev.Show()
chrev.SetRevealChild(false)
chrev.Add(children)
chrev.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.Show()
box.PackStart(header, false, false, 0)
box.PackStart(chrev, false, false, 0)
var container = &Container{box, header, chrev, children}
primitives.AddClass(box, "service")
var container = &Container{box, header, chrev, children, rowctrl}
// On click, toggle reveal.
header.reveal.Connect("clicked", func() {
revealed := !chrev.GetRevealChild()
chrev.SetRevealChild(revealed)
header.reveal.SetActive(revealed)
})
// On click, show the auth dialog.
header.add.Connect("clicked", func() {
auth.NewDialog(svc.Name(), svc.Authenticate(), container.addSession)
})
@ -65,31 +82,34 @@ func NewContainer(svc cchat.Service) *Container {
}
func (c *Container) addSession(ses cchat.Session) {
srow := session.New(ses)
srow := session.New(ses, c.rowctrl)
c.children.addSessionRow(srow)
}
type header struct {
*gtk.Box
label *gtk.Label
add *gtk.Button
reveal *gtk.ToggleButton
add *gtk.Button
}
func newHeader(svc cchat.Service) *header {
label, _ := gtk.LabelNew(svc.Name())
label.Show()
label.SetXAlign(0)
reveal, _ := gtk.ToggleButtonNewWithLabel(svc.Name())
primitives.BinLeftAlignLabel(reveal) // do this first
reveal.SetRelief(gtk.RELIEF_NONE)
reveal.SetMode(true)
reveal.Show()
add, _ := gtk.ButtonNewFromIconName("list-add-symbolic", gtk.ICON_SIZE_BUTTON)
add.SetRelief(gtk.RELIEF_NONE)
add.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.Show()
box.PackStart(label, true, true, 5)
box.PackStart(reveal, true, true, 0)
box.PackStart(add, false, false, 0)
box.Show()
return &header{box, label, add}
return &header{box, reveal, add}
}
type children struct {

View file

@ -4,37 +4,49 @@ import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
const ChildrenMargin = 24
type RowController interface {
MessageRowSelected(*Row, cchat.ServerMessage)
}
type Row struct {
*gtk.Box
Button *gtk.Button
Server cchat.Server
ctrl RowController
// enum 1
clicked func(*Row)
message cchat.ServerMessage
// enum 2
children *Children
}
func New(server cchat.Server) *Row {
func New(server cchat.Server, ctrl RowController) *Row {
name, err := server.Name()
if err != nil {
log.Error(errors.Wrap(err, "Failed to get the server name"))
name = "no name"
}
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
button, _ := gtk.ButtonNewWithLabel(name)
primitives.BinLeftAlignLabel(button)
button.SetRelief(gtk.RELIEF_NONE)
button.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.PackStart(button, false, false, 0)
box.Show()
button, _ := gtk.ButtonNew()
button.Show()
button.SetRelief(gtk.RELIEF_NONE)
button.SetLabel(name)
primitives.AddClass(box, "server")
// TODO: images
@ -42,31 +54,30 @@ func New(server cchat.Server) *Row {
Box: box,
Button: button,
Server: server,
ctrl: ctrl,
}
button.Connect("clicked", row.onClick)
switch server := server.(type) {
case cchat.ServerList:
row.children = NewChildren(server)
row.children = NewChildren(server, ctrl)
box.PackStart(row.children, false, false, 0)
primitives.AddClass(box, "server-list")
case cchat.ServerMessage:
row.message = server
primitives.AddClass(box, "server-message")
}
return row
}
// SetOnClick sets the callback when the server is clicked. This only works if
// the passed in server implements ServerMessage.
func (row *Row) SetOnClick(clicked func(*Row)) {
if row.message != nil {
row.clicked = clicked
}
}
func (row *Row) onClick() {
switch {
case row.message != nil:
row.clicked(row)
row.ctrl.MessageRowSelected(row, row.message)
case row.children != nil:
row.children.SetRevealChild(!row.children.GetRevealChild())
}
@ -75,22 +86,27 @@ func (row *Row) onClick() {
type Children struct {
*gtk.Revealer
Main *gtk.Box
Rows []*Row
List cchat.ServerList
Rows []*Row
rowctrl RowController
}
func NewChildren(list cchat.ServerList) *Children {
rev, _ := gtk.RevealerNew()
rev.Show()
rev.SetRevealChild(false)
main, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
func NewChildren(list cchat.ServerList, ctrl RowController) *Children {
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
main.SetMarginStart(ChildrenMargin)
main.Show()
rev, _ := gtk.RevealerNew()
rev.SetRevealChild(false)
rev.Add(main)
rev.Show()
children := &Children{
Revealer: rev,
Main: main,
List: list,
rowctrl: ctrl,
}
if err := list.Servers(children); err != nil {
@ -109,7 +125,7 @@ func (c *Children) SetServers(servers []cchat.Server) {
c.Rows = make([]*Row, len(servers))
for i, server := range servers {
row := New(server)
row := New(server, c.rowctrl)
c.Rows[i] = row
c.Main.Add(row)
}

View file

@ -3,6 +3,7 @@ package session
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
@ -10,35 +11,46 @@ import (
type Row struct {
*gtk.Box
Button *gtk.Button
Button *gtk.ToggleButton
Session cchat.Session
Servers *server.Children
}
func New(ses cchat.Session) *Row {
func New(ses cchat.Session, rowctrl server.RowController) *Row {
n, err := ses.Name()
if err != nil {
log.Error(errors.Wrap(err, "Failed to get the username"))
n = "no name"
}
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.Show()
button, _ := gtk.ToggleButtonNewWithLabel(n)
primitives.BinLeftAlignLabel(button)
button, _ := gtk.ButtonNew()
button.Show()
button.SetRelief(gtk.RELIEF_NONE)
button.SetLabel(n)
button.Show()
rev, _ := gtk.RevealerNew()
rev.Show()
rev.SetRevealChild(false)
servers := server.NewChildren(ses, rowctrl)
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.Show()
box.SetMarginStart(server.ChildrenMargin)
box.PackStart(button, false, false, 0)
box.PackStart(servers, false, false, 0)
primitives.AddClass(box, "session")
// On click, toggle reveal.
button.Connect("clicked", func() {
revealed := !servers.GetRevealChild()
servers.SetRevealChild(revealed)
button.SetActive(revealed)
})
return &Row{
Box: box,
Button: button,
Session: ses,
Servers: server.NewChildren(ses),
Servers: servers,
}
}

View file

@ -3,6 +3,7 @@ package ui
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/gotk3/gotk3/gtk"
)
@ -14,8 +15,9 @@ type Application struct {
}
var (
_ gts.Windower = (*Application)(nil)
_ gts.Headerer = (*Application)(nil)
_ gts.Windower = (*Application)(nil)
_ gts.Headerer = (*Application)(nil)
_ server.RowController = (*Application)(nil)
)
func NewApplication() *Application {
@ -28,7 +30,11 @@ func NewApplication() *Application {
}
func (app *Application) AddService(svc cchat.Service) {
app.window.Services.AddService(svc)
app.window.Services.AddService(svc, app)
}
func (app *Application) MessageRowSelected(_ *server.Row, smsg cchat.ServerMessage) {
app.window.MessageView.JoinServer(smsg)
}
func (app *Application) Header() gtk.IWidget {

View file

@ -17,10 +17,14 @@ func newWindow() *window {
services.SetSizeRequest(LeftWidth, -1)
mesgview := message.NewView()
separator, _ := gtk.SeparatorNew(gtk.ORIENTATION_VERTICAL)
separator.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.Show()
box.PackStart(services, false, false, 0)
box.PackStart(separator, false, false, 0)
box.PackStart(mesgview, true, true, 0)
box.Show()
return &window{box, services, mesgview}
}