that's a lot of code

This commit is contained in:
2025-08-04 10:05:43 +01:00
parent dfa1fb54d4
commit c13ee7357c
9 changed files with 813 additions and 53 deletions

2
.gitignore vendored
View File

@@ -26,7 +26,7 @@ go.work.sum
# env file
.env
pi.json
# Editor/IDE
# .idea/
# .vscode/

View File

@@ -1,7 +1,57 @@
# pi
# π
Experimental and extremely weird XMPP client made with Go. No solicitors.
pi is currently pre-pre-pre-pre alpha software which you should not use right now.
pi uses [Fyne](https://fyne.io) for the frontend.
pi uses [Fyne](https://fyne.io) for the frontend and uses the [Oasis SDK](https://github.com/jjj333-p/oasis-sdk) for XMPP functionality.
pi is an extremely opinionated client. It aims to have as little extra windows as possible, instead using alt-menus to perform many of the actions you'd see in a typical client.
## διαμόρφωση
(configuration)
In order to use pi, you currently have to create a `pi.json` file in the working directory of the executable. Here is how one looks like:
```json
{
"Host":"example.com:5222",
"User":"user@example.com",
"Password":"123456",
"DisplayName":"user",
"NoTLS":false,
"StartTLS":true,
"Mucs":["room@muc.example.com"]}
```
Edit this file as necessary.
Currently joining and saving DM tabs is not supported, nor is getting avatars, reactions, encryption of media embed.
As of writing, pi supports basic message sending and receiving, replies and file upload.
## να χτίσω
(building)
To build pi, you will need the latest version of Go, at least 1.21. You can grab it [here](https://go.dev).
The build instructions are very simple. Simply clone the repo, fetch the repositories and build the program:
Here is a summary of the commands you would need to use to build and run the program:
```bash
git clone https://github.com/sunglocto/pi
cd pi
go mod tidy
go build .
vim pi.json
./pi
```
Static executable snapshots are also provided for GNU/Linux systems.
## χρήση
(usage)
TODO

4
go.mod
View File

@@ -4,6 +4,8 @@ go 1.24.5
require (
fyne.io/fyne/v2 v2.6.2
fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb
github.com/mbaklor/fyne-catppuccin v0.0.2
mellium.im/xmpp v0.22.0
pain.agency/oasis-sdk v0.0.0-20250803100711-2ed1355344d4
)
@@ -11,6 +13,7 @@ require (
require (
fyne.io/systray v1.11.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
@@ -28,7 +31,6 @@ require (
github.com/hack-pad/safejs v0.1.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect

7
go.sum
View File

@@ -2,9 +2,12 @@ fyne.io/fyne/v2 v2.6.2 h1:RPgwmXWn+EuP/TKwO7w5p73ILVC26qHD9j3CZUZNwgM=
fyne.io/fyne/v2 v2.6.2/go.mod h1:9IJ8uWgzfcMossFoUkLiOrUIEtaDvF4nML114WiCtXU=
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb h1:2BazNmb/kwgqRdvE9L+NgW8sfoWGn3iy1Ox8R4+CSmc=
fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb/go.mod h1:u3LF1EkElytjOT8OHxft16trctGndF9qpsoH6YIDOUU=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
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/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
@@ -47,6 +50,8 @@ github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe9
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mbaklor/fyne-catppuccin v0.0.2 h1:yMNnYkmFwjKJkFQvCd1uNKZDs07ZC85wTkedTIGcViE=
github.com/mbaklor/fyne-catppuccin v0.0.2/go.mod h1:ZBIy4dV1yMj+7oEaZYkXm5OfYESmXuPWwNcuUmD1Njo=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=

448
main.go
View File

@@ -1,58 +1,244 @@
package main
import (
"encoding/json"
"fmt"
"image/color"
"io"
"log"
_ "net/url"
"os"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
_ "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
_ "fyne.io/x/fyne/theme"
catppuccin "github.com/mbaklor/fyne-catppuccin"
"mellium.im/xmpp/jid"
"mellium.im/xmpp/muc"
"mellium.im/xmpp/stanza"
oasisSdk "pain.agency/oasis-sdk"
)
// by sunglocto
// license AGPL
func main() {
type Message struct {
Author string
Content string
ID string
ReplyID string
Raw oasisSdk.XMPPChatMessage
}
login := oasisSdk.LoginInfo{
Host: "sunglocto.net:5222",
User: "bot2@sunglocto.net",
Password: "iloverobots",
DisplayName: "bot2",
TLSoff: false,
StartTLS: true,
MucsToJoin: []string{"ringen@muc.isekai.rocks"},
type MucTab struct {
Jid jid.JID
Nick string
Messages []Message
Scroller *widget.List
isMuc bool
}
var chatTabs = make(map[string]*MucTab)
var tabs *container.AppTabs
var selectedId widget.ListItemID
var replying bool = false
var notifications bool = true
type myTheme struct{}
func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
return catppuccin.New().Color(name, variant)
}
func (m myTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(name)
}
func (m myTheme) Font(style fyne.TextStyle) fyne.Resource {
return theme.DefaultTheme().Font(style)
}
func (m myTheme) Size(name fyne.ThemeSizeName) float32 {
if name == theme.SizeNameHeadingText {
return 18
}
return theme.DefaultTheme().Size(name)
}
var scrollDownOnNewMessage bool = true
var w fyne.Window
var a fyne.App
func addChatTab(isMuc bool, chatJid jid.JID, nick string) {
mucJidStr := chatJid.String()
if _, ok := chatTabs[mucJidStr]; ok {
// Tab already exists
return
}
maina := container.New(layout.NewHBoxLayout(), widget.NewLabel("pi"))
scroller := container.NewVScroll(maina)
tabData := &MucTab{
Jid: chatJid,
Nick: nick,
Messages: []Message{},
isMuc: isMuc,
}
var scroller *widget.List
scroller = widget.NewList(
func() int {
return len(tabData.Messages)
},
func() fyne.CanvasObject {
author := widget.NewLabel("author")
author.TextStyle.Bold = true
content := widget.NewRichTextWithText("content")
content.Wrapping = fyne.TextWrapWord
return container.NewVBox(author, content)
},
func(i widget.ListItemID, co fyne.CanvasObject) {
vbox := co.(*fyne.Container)
author := vbox.Objects[0].(*widget.Label)
content := vbox.Objects[1].(*widget.RichText)
content.ParseMarkdown(tabData.Messages[i].Content)
if tabData.Messages[i].ReplyID != "PICLIENT:UNAVAILABLE" {
author.SetText(fmt.Sprintf("%s ↳ %s", tabData.Messages[i].Author, jid.MustParse(tabData.Messages[i].ReplyID).Resourcepart()))
} else {
author.SetText(tabData.Messages[i].Author)
}
scroller.SetItemHeight(i, vbox.MinSize().Height)
},
)
scroller.OnSelected = func(id widget.ListItemID) {
selectedId = id
}
tabData.Scroller = scroller
chatTabs[mucJidStr] = tabData
tabItem := container.NewTabItem(chatJid.Localpart(), scroller)
tabs.Append(tabItem)
}
func main() {
login := oasisSdk.LoginInfo{}
DMs := []string{}
bytes, err := os.ReadFile("./pi.json")
if err != nil {
a = app.New()
w = a.NewWindow("Error")
w.Resize(fyne.NewSize(500, 500))
dialog.ShowInformation("Error", fmt.Sprintf("Please make sure there is a file named pi.json in the same directory you are running this executable...\n%s", err.Error()), w)
w.ShowAndRun()
return
}
err = json.Unmarshal(bytes, &login)
if err != nil {
a = app.New()
w = a.NewWindow("Error")
w.Resize(fyne.NewSize(500, 500))
dialog.ShowError(err, w)
w.ShowAndRun()
return
}
client, err := oasisSdk.CreateClient(
&login,
func(client *oasisSdk.XmppClient, msg *oasisSdk.XMPPChatMessage) {
fyne.Do(func(){
card := widget.NewCard(msg.From.String(), *msg.CleanedBody, canvas.NewCircle(color.White))
maina.Add(card)
maina.Refresh()
scroller.ScrollToBottom()
})
fmt.Println(msg)
userJidStr := msg.From.Bare().String()
tab, ok := chatTabs[userJidStr]
fmt.Println(msg.From.String())
if ok {
str := *msg.CleanedBody
if notifications {
a.SendNotification(fyne.NewNotification(fmt.Sprintf("%s says", userJidStr), str))
}
/*
if strings.Contains(str, "https://") {
fmt.Println("Attempting to do URL thingy")
s := strings.Split(str, " ")
for i, v := range s {
_, err := url.Parse(v)
if err == nil {
s[i] = fmt.Sprintf("[%s](%s)", v, v)
}
}
str = strings.Join(s, " ")
}*/
var replyID string
if msg.Reply == nil {
replyID = "PICLIENT:UNAVAILABLE"
} else {
replyID = msg.Reply.ID
}
myMessage := Message{
Author: msg.From.Resourcepart(),
Content: str,
ID: msg.ID,
ReplyID: replyID,
Raw: *msg,
}
tab.Messages = append(tab.Messages, myMessage)
fyne.Do(func() {
tab.Scroller.Refresh()
if scrollDownOnNewMessage {
tab.Scroller.ScrollToBottom()
}
})
}
},
func(client *oasisSdk.XmppClient, _ *muc.Channel, msg *oasisSdk.XMPPChatMessage) {
fyne.Do(func(){
if msg.Reply != nil {
mucJidStr := msg.From.Bare().String()
if tab, ok := chatTabs[mucJidStr]; ok {
str := *msg.CleanedBody
if notifications {
if strings.Contains(str, login.DisplayName) || (msg.Reply != nil && strings.Contains(msg.Reply.To, login.User)) {
a.SendNotification(fyne.NewNotification(fmt.Sprintf("Mentioned in %s", mucJidStr), str))
}
}
card := widget.NewCard(msg.From.String(), *msg.CleanedBody, canvas.NewCircle(color.White))
maina.Add(card)
maina.Refresh()
scroller.ScrollToBottom()
})
/*
if strings.Contains(str, "https://") {
s := strings.Split(str, " ")
for i, v := range s {
_, err := url.Parse(v)
if err == nil {
s[i] = fmt.Sprintf("[%s](%s)", v, v)
}
}
str = strings.Join(s, " ")
}*/
fmt.Println(msg.ID)
var replyID string
if msg.Reply == nil {
replyID = "PICLIENT:UNAVAILABLE"
} else {
replyID = msg.Reply.To
}
myMessage := Message{
Author: msg.From.Resourcepart(),
Content: str,
ID: msg.ID,
ReplyID: replyID,
Raw: *msg,
}
tab.Messages = append(tab.Messages, myMessage)
fyne.Do(func() {
tab.Scroller.Refresh()
if scrollDownOnNewMessage {
tab.Scroller.ScrollToBottom()
}
})
}
},
func(_ *oasisSdk.XmppClient, from jid.JID, state oasisSdk.ChatState) {
//fromStr := from.String()
@@ -72,48 +258,214 @@ func main() {
fmt.Printf("%s has seen %s", from.String(), id)
},
)
if err != nil {
log.Fatalln("Could not create client - " + err.Error())
}
go func() {
err = client.Connect()
if err != nil {
log.Fatalln("Could not connect - " + err.Error())
}
err = client.Connect()
if err != nil {
log.Fatalln("Could not connect - " + err.Error())
}
}()
a := app.New()
w := a.NewWindow("pi")
w.Resize(fyne.NewSize(500,500))
a = app.New()
a.Settings().SetTheme(myTheme{})
w = a.NewWindow("pi")
w.Resize(fyne.NewSize(500, 500))
entry := widget.NewMultiLineEntry()
entry.SetPlaceHolder("Say something, you know you want to.")
sendbtn := widget.NewButton("Send", func() {
text := entry.Text
if tabs.Selected() == nil || tabs.Selected().Content == nil {
return
}
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeMucJid string
var isMuc bool
for jid, tabData := range chatTabs {
if tabData.Scroller == selectedScroller {
activeMucJid = jid
isMuc = tabData.isMuc
break
}
}
if activeMucJid == "" {
return
}
go func() {
//TODO: Fix message hack until jjj adds message sending
if replying {
m := chatTabs[activeMucJid].Messages[selectedId].Raw
client.ReplyToEvent(&m, text)
return
}
var typ stanza.MessageType
if isMuc {
typ = stanza.GroupChatMessage
} else {
typ = stanza.ChatMessage
}
msg := oasisSdk.XMPPChatMessage{
Message: stanza.Message{
To: jid.MustParse(activeMucJid),
Type: typ,
},
ChatMessageBody: oasisSdk.ChatMessageBody{
Body: &text,
},
}
err := client.Session.Encode(client.Ctx, msg)
if err != nil {
dialog.ShowError(err, w)
}
}()
if !isMuc {
chatTabs[activeMucJid].Messages = append(chatTabs[activeMucJid].Messages, Message{
Author: "You",
Content: text,
})
fyne.Do(func() {
if scrollDownOnNewMessage {
chatTabs[activeMucJid].Scroller.ScrollToBottom()
}
})
}
entry.SetText("")
})
mit := fyne.NewMenuItem("About pi", func() {
dialog.ShowInformation("About pi", "the XMPP client from hell\npi is an experimental XMPP client\nwritten by Sunglocto in Go.", w)
dialog.ShowInformation("About pi", "the XMPP client from hell\n\npi is an experimental XMPP client\nwritten by Sunglocto in Go.", w)
})
mia := fyne.NewMenuItem("Configure message view", func() {
ch := widget.NewCheck("", func(b bool) {})
ch2 := widget.NewCheck("", func(b bool) {})
ch.Checked = scrollDownOnNewMessage
ch2.Checked = notifications
scrollView := widget.NewFormItem("Scroll to bottom on new message", ch)
notiView := widget.NewFormItem("Send notifications when mentioned", ch2)
items := []*widget.FormItem{
scrollView,
notiView,
}
dialog.ShowForm("Configure message view", "Apply", "Cancel", items, func(b bool) {
if b {
scrollDownOnNewMessage = ch.Checked
notifications = ch2.Checked
}
}, w)
})
mis := fyne.NewMenuItem("Clear chat window", func() {
dialog.ShowConfirm("Clear chat window", "Are you sure you want to clear the chat window?", func(b bool) {
if b {
fmt.Println("clearing chat")
}
}, w)
})
mib := fyne.NewMenuItem("Join a room", func() {
nick := widget.NewEntry()
room := widget.NewEntry()
nickEntry := widget.NewEntry()
nickEntry.SetText(login.DisplayName)
roomEntry := widget.NewEntry()
items := []*widget.FormItem{
widget.NewFormItem("Nick", nick),
widget.NewFormItem("MUC address", room),
widget.NewFormItem("Nick", nickEntry),
widget.NewFormItem("MUC address", roomEntry),
}
dialog.ShowForm("Join a MUC", "Join", "Cancel", items, func(b bool) {
if b {
fmt.Println("attempting to join MUC")
fmt.Println(nick)
fmt.Println(room)
go func(){
client.MucClient.Join(client.Ctx, jid.MustParse(room.Text), client.Session, nil)
roomJid, err := jid.Parse(roomEntry.Text)
if err != nil {
dialog.ShowError(err, w)
return
}
nick := nickEntry.Text
go func() {
// We probably don't need to handle the error here, if it fails the user will know
_, err := client.MucClient.Join(client.Ctx, roomJid, client.Session, nil)
if err != nil {
panic(err)
}
}()
addChatTab(true, roomJid, nick)
}
}, w)
})
mic := fyne.NewMenuItem("Upload a file", func() {
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil {
dialog.ShowError(err, w)
}
bytes, err := io.ReadAll(reader)
link, err := client.UploadFileFromBytes(reader.URI().String(), bytes)
if err != nil {
dialog.ShowError(err, w)
return
}
a.Clipboard().SetContent(link)
dialog.ShowInformation("File successfully uploaded", link, w)
}, w)
})
menu_help := fyne.NewMenu("π", mit)
menu_changeroom := fyne.NewMenu("β", mib)
ma := fyne.NewMainMenu(menu_help, menu_changeroom)
menu_changeroom := fyne.NewMenu("β", mib, mic)
menu_configureview := fyne.NewMenu("γ", mia, mis)
bit := fyne.NewMenuItem("Mark message as read", func() {
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeMucJid string
for jid, tabData := range chatTabs {
if tabData.Scroller == selectedScroller {
activeMucJid = jid
break
}
}
m := chatTabs[activeMucJid].Messages[selectedId].Raw
client.MarkAsRead(&m)
})
bia := fyne.NewMenuItem("Toggle replying to message", func() {
replying = !replying
})
menu_messageoptions := fyne.NewMenu("Σ", bit, bia)
ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview, menu_messageoptions)
w.SetMainMenu(ma)
tabs := container.NewAppTabs(container.NewTabItem("pi", widget.NewLabel("pi\nthe XMPP client from hell")), container.NewTabItem("chat", scroller))
w.SetContent(tabs)
tabs = container.NewAppTabs(
container.NewTabItem("τίποτα", widget.NewRichTextFromMarkdown("# No chat selected.")),
)
for _, mucJidStr := range login.MucsToJoin {
mucJid, err := jid.Parse(mucJidStr)
if err == nil {
addChatTab(true, mucJid, login.DisplayName)
}
}
for _, userJidStr := range DMs {
fmt.Println(userJidStr)
DMjid, err := jid.Parse(userJidStr)
if err == nil {
addChatTab(false, DMjid, login.DisplayName)
}
}
w.SetContent(container.NewVSplit(tabs, container.NewHSplit(entry, sendbtn)))
w.ShowAndRun()
}

210
main.go.copy Normal file
View File

@@ -0,0 +1,210 @@
package main
import (
"fmt"
"image/color"
"log"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
_ "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
_ "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
_ "github.com/mbaklor/fyne-catppuccin"
_ "fyne.io/x/fyne/theme"
"mellium.im/xmpp/jid"
"mellium.im/xmpp/muc"
"mellium.im/xmpp/stanza"
oasisSdk "pain.agency/oasis-sdk"
)
type Message struct {
Author string
Content string
}
var Messages []Message
type myTheme struct{}
func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
return theme.DefaultTheme().Color(name, variant)
}
func (m myTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(name)
}
func (m myTheme) Font(style fyne.TextStyle) fyne.Resource {
return theme.DefaultTheme().Font(style)
}
func (m myTheme) Size(name fyne.ThemeSizeName) float32 {
if name == theme.SizeNameHeadingText {
return 18
}
return theme.DefaultTheme().Size(name)
}
var scrollDownOnNewMessage bool = true
func addMessageToView() fyne.CanvasObject {
author := widget.NewLabel("")
author.TextStyle.Bold = true
content := widget.NewLabel("")
content.Wrapping = fyne.TextWrapWord
return container.NewVBox(author, content)
}
func main() {
login := oasisSdk.LoginInfo{
Host: "sunglocto.net:5222",
User: "bot2@sunglocto.net",
Password: "iloverobots",
DisplayName: "bot2",
TLSoff: false,
StartTLS: true,
MucsToJoin: []string{"ringen@muc.isekai.rocks"},
}
scroller := widget.NewList(
func() int {
return len(Messages)
}, func() fyne.CanvasObject {
return addMessageToView()
}, func(i widget.ListItemID, co fyne.CanvasObject) {
co.(*fyne.Container).Objects[0].(*widget.Label).SetText(Messages[i].Author)
co.(*fyne.Container).Objects[1].(*widget.Label).SetText(Messages[i].Content)
})
client, err := oasisSdk.CreateClient(
&login,
func(client *oasisSdk.XmppClient, msg *oasisSdk.XMPPChatMessage) {
myMessage := Message{}
myMessage.Author = msg.From.Resourcepart()
myMessage.Content = *msg.CleanedBody
Messages = append(Messages, myMessage)
if scrollDownOnNewMessage {
fyne.Do(func() {
scroller.ScrollToBottom()
})
}
},
func(client *oasisSdk.XmppClient, _ *muc.Channel, msg *oasisSdk.XMPPChatMessage) {
myMessage := Message{}
myMessage.Author = msg.From.Resourcepart()
myMessage.Content = *msg.CleanedBody
Messages = append(Messages, myMessage)
if scrollDownOnNewMessage {
fyne.Do(func() {
scroller.ScrollToBottom()
})
}
},
func(_ *oasisSdk.XmppClient, from jid.JID, state oasisSdk.ChatState) {
//fromStr := from.String()
switch state {
case oasisSdk.ChatStateActive:
case oasisSdk.ChatStateComposing:
case oasisSdk.ChatStatePaused:
case oasisSdk.ChatStateInactive:
case oasisSdk.ChatStateGone:
default:
}
},
func(_ *oasisSdk.XmppClient, from jid.JID, id string) {
fmt.Printf("Delivered %s to %s", id, from.String())
},
func(_ *oasisSdk.XmppClient, from jid.JID, id string) {
fmt.Printf("%s has seen %s", from.String(), id)
},
)
if err != nil {
log.Fatalln("Could not create client - " + err.Error())
}
go func() {
err = client.Connect()
if err != nil {
log.Fatalln("Could not connect - " + err.Error())
}
}()
a := app.New()
a.Settings().SetTheme(myTheme{})
w := a.NewWindow("pi")
w.Resize(fyne.NewSize(500, 500))
entry := widget.NewMultiLineEntry()
entry.SetPlaceHolder("Say something, you know you want to.")
sendbtn := widget.NewButton("Send", func() {
text := entry.Text
go func() {
msg := oasisSdk.XMPPChatMessage{ // TODO: Remove hack when oasisSdk adds message sending (hopefully xd)
Message: stanza.Message{
To: jid.MustParse("ringen@muc.isekai.rocks"), //FIXME
Type: stanza.GroupChatMessage, //FIXME
},
ChatMessageBody: oasisSdk.ChatMessageBody{
Body: &text,
},
}
err := client.Session.Encode(client.Ctx, msg)
if err != nil {
dialog.ShowError(err, w)
}
}()
entry.SetText("")
})
mit := fyne.NewMenuItem("About pi", func() {
dialog.ShowInformation("About pi", "the XMPP client from hell\n\npi is an experimental XMPP client\nwritten by Sunglocto in Go.", w)
})
mia := fyne.NewMenuItem("Configure message view", func() {
ch := widget.NewCheck("", func(b bool) {})
ch.Checked = scrollDownOnNewMessage
scrollView := widget.NewFormItem("Scroll to bottom on new message", ch)
items := []*widget.FormItem{
scrollView,
}
dialog.ShowForm("Configure message view", "Apply", "Cancel", items, func(b bool) {
if b {
scrollDownOnNewMessage = ch.Checked
}
}, w)
})
mib := fyne.NewMenuItem("Join a room", func() {
nick := widget.NewEntry()
room := widget.NewEntry()
items := []*widget.FormItem{
widget.NewFormItem("Nick", nick),
widget.NewFormItem("MUC address", room),
}
dialog.ShowForm("Join a MUC", "Join", "Cancel", items, func(b bool) {
if b {
fmt.Println("attempting to join MUC")
fmt.Println(nick)
fmt.Println(room)
go func() {
client.MucClient.Join(client.Ctx, jid.MustParse(room.Text), client.Session, nil)
}()
}
}, w)
})
menu_help := fyne.NewMenu("π", mit)
menu_changeroom := fyne.NewMenu("β", mib)
menu_configureview := fyne.NewMenu("γ", mia)
ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview)
w.SetMainMenu(ma)
tabs := container.NewAppTabs(container.NewTabItem("pi", widget.NewLabel("pi\n\nthe XMPP client from hell")), container.NewTabItem("chat", scroller))
w.SetContent(container.NewVSplit(tabs, container.NewHSplit(entry, sendbtn))) // TODO: Add send message functionality
w.ShowAndRun()
}

BIN
pi Executable file

Binary file not shown.

BIN
pi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

141
pi.svg Normal file
View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="200mm"
height="200mm"
viewBox="0 0 200 200"
version="1.1"
id="svg1"
sodipodi:docname="pi.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.38395763"
inkscape:cx="65.111351"
inkscape:cy="388.06365"
inkscape:window-width="1876"
inkscape:window-height="1000"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g3" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g1"
transform="matrix(0.70699338,0,0,0.70699338,36.332006,51.956657)">
<g
id="g3">
<text
xml:space="preserve"
style="font-size:172.637px;font-family:'IBM Plex Sans';-inkscape-font-specification:'IBM Plex Sans';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#210063;fill-opacity:0.697983;stroke:#7400ae;stroke-width:14.3864;stroke-opacity:1"
x="25.983902"
y="131.06415"
id="text1-1"><tspan
sodipodi:role="line"
id="tspan1-5"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Serif;-inkscape-font-specification:Serif;fill:#210063;fill-opacity:0.697983;stroke:#7400ae;stroke-width:14.3864;stroke-opacity:1"
x="25.983902"
y="131.06415">π</tspan></text>
<text
xml:space="preserve"
style="font-size:172.637px;font-family:'IBM Plex Sans';-inkscape-font-specification:'IBM Plex Sans';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#b118ff;fill-opacity:1;stroke:#b118ff;stroke-width:14.3864;stroke-opacity:1"
x="35.94326"
y="121.42939"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Serif;-inkscape-font-specification:Serif;fill:#b118ff;fill-opacity:1;stroke:#b118ff;stroke-width:14.3864;stroke-opacity:1"
x="35.94326"
y="121.42939">π</tspan></text>
<g
id="g4-6"
transform="translate(-7.9610819,-28.846801)">
<ellipse
style="fill:#530075;fill-opacity:1;stroke:none;stroke-width:0.848664;stroke-miterlimit:26.7;stroke-dasharray:none;stroke-opacity:1"
id="path3-4"
cx="169.40752"
cy="45.499199"
rx="10.234183"
ry="10.32556" />
<rect
style="fill:#530075;fill-opacity:1;stroke:none;stroke-width:1.77871;stroke-miterlimit:26.7;stroke-dasharray:0.533614, 0.533614;stroke-dashoffset:0;stroke-opacity:1"
id="rect3-3"
width="2.9291241"
height="22.571486"
x="165.08534"
y="-90.200768"
transform="rotate(33.369596)" />
<rect
style="fill:#530075;fill-opacity:1;stroke:none;stroke-width:1.77871;stroke-miterlimit:26.7;stroke-dasharray:0.533614, 0.533614;stroke-dashoffset:0;stroke-opacity:1"
id="rect3-9-3"
width="2.9291241"
height="22.571486"
x="86.228531"
y="-188.09276"
transform="rotate(75.044324)" />
<rect
style="fill:#530075;fill-opacity:1;stroke:none;stroke-width:1.77871;stroke-miterlimit:26.7;stroke-dasharray:0.533614, 0.533614;stroke-dashoffset:0;stroke-opacity:1"
id="rect3-9-8-3"
width="2.9291241"
height="22.571486"
x="-28.242762"
y="-208.65135"
transform="rotate(113.84012)" />
</g>
<g
id="g4"
transform="translate(-6.8227879,-31.189888)">
<ellipse
style="fill:#9600d2;fill-opacity:1;stroke:none;stroke-width:0.848664;stroke-miterlimit:26.7;stroke-dasharray:none;stroke-opacity:1"
id="path3"
cx="169.40752"
cy="45.499199"
rx="10.234183"
ry="10.32556" />
<rect
style="fill:#9600d2;fill-opacity:1;stroke:none;stroke-width:1.77871;stroke-miterlimit:26.7;stroke-dasharray:0.533614, 0.533614;stroke-dashoffset:0;stroke-opacity:1"
id="rect3"
width="2.9291241"
height="22.571486"
x="165.08534"
y="-90.200768"
transform="rotate(33.369596)" />
<rect
style="fill:#9600d2;fill-opacity:1;stroke:none;stroke-width:1.77871;stroke-miterlimit:26.7;stroke-dasharray:0.533614, 0.533614;stroke-dashoffset:0;stroke-opacity:1"
id="rect3-9"
width="2.9291241"
height="22.571486"
x="86.228531"
y="-188.09276"
transform="rotate(75.044324)" />
<rect
style="fill:#9600d2;fill-opacity:1;stroke:none;stroke-width:1.77871;stroke-miterlimit:26.7;stroke-dasharray:0.533614, 0.533614;stroke-dashoffset:0;stroke-opacity:1"
id="rect3-9-8"
width="2.9291241"
height="22.571486"
x="-28.242762"
y="-208.65135"
transform="rotate(113.84012)" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB