947fcf0432
* PubSub protocol support Added support for : - XEP-0050 (Command)) - XEP-0060 (PubSub) - XEP-0004 (Forms) Fixed the NewClient function by adding parsing of the domain from the JID if no domain is provided in transport config. Updated xmpp_jukebox example * Delete useless pubsub errors * README.md update Fixed import in echo example * Typo * Fixed raw send on client example * Fixed jukebox example and added a README.md
372 lines
11 KiB
Go
372 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"github.com/awesome-gocui/gocui"
|
|
"log"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// Windows
|
|
chatLogWindow = "clw" // Where (received and sent) messages are logged
|
|
chatInputWindow = "iw" // Where messages are written
|
|
rawInputWindow = "rw" // Where raw stanzas are written
|
|
contactsListWindow = "cl" // Where the contacts list is shown, and contacts are selectable
|
|
menuWindow = "mw" // Where the menu is shown
|
|
disconnectMsg = "msg"
|
|
|
|
// Windows titles
|
|
chatLogWindowTitle = "Chat log"
|
|
menuWindowTitle = "Menu"
|
|
chatInputWindowTitle = "Write a message :"
|
|
rawInputWindowTitle = "Write or paste a raw stanza. Press \"Ctrl+E\" to send :"
|
|
contactsListWindowTitle = "Contacts"
|
|
|
|
// Menu options
|
|
disconnect = "Disconnect"
|
|
askServerForRoster = "Ask server for roster"
|
|
rawMode = "Switch to Send Raw Mode"
|
|
messageMode = "Switch to Send Message Mode"
|
|
contactList = "Contacts list"
|
|
backFromContacts = "<- Go back"
|
|
)
|
|
|
|
// To store names of views on top
|
|
type viewsState struct {
|
|
input string // Which input view is on top
|
|
side string // Which side view is on top
|
|
contacts []string // Contacts list
|
|
currentContact string // Contact we are currently messaging
|
|
}
|
|
|
|
var (
|
|
// Which window is on top currently on top of the other.
|
|
// This is the init setup
|
|
viewState = viewsState{
|
|
input: chatInputWindow,
|
|
side: menuWindow,
|
|
}
|
|
menuOptions = []string{contactList, rawMode, askServerForRoster, disconnect}
|
|
// Errors
|
|
servConnFail = errors.New("failed to connect to server. Check your configuration ? Exiting")
|
|
)
|
|
|
|
func setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) {
|
|
if _, err := g.SetCurrentView(name); err != nil {
|
|
return nil, err
|
|
}
|
|
return g.SetViewOnTop(name)
|
|
}
|
|
|
|
func layout(g *gocui.Gui) error {
|
|
maxX, maxY := g.Size()
|
|
|
|
if v, err := g.SetView(chatLogWindow, maxX/5, 0, maxX-1, 5*maxY/6-1, 0); err != nil {
|
|
if !gocui.IsUnknownView(err) {
|
|
return err
|
|
}
|
|
v.Title = chatLogWindowTitle
|
|
v.Wrap = true
|
|
v.Autoscroll = true
|
|
}
|
|
|
|
if v, err := g.SetView(contactsListWindow, 0, 0, maxX/5-1, 5*maxY/6-2, 0); err != nil {
|
|
if !gocui.IsUnknownView(err) {
|
|
return err
|
|
}
|
|
v.Title = contactsListWindowTitle
|
|
v.Wrap = true
|
|
// If we set this to true, the contacts list will "fit" in the window but if the number
|
|
// of contacts exceeds the maximum height, some contacts will be hidden...
|
|
// If set to false, we can scroll up and down the contact list... infinitely. Meaning lower lines
|
|
// will be unlimited and empty... Didn't find a way to quickfix yet.
|
|
v.Autoscroll = false
|
|
}
|
|
|
|
if v, err := g.SetView(menuWindow, 0, 0, maxX/5-1, 5*maxY/6-2, 0); err != nil {
|
|
if !gocui.IsUnknownView(err) {
|
|
return err
|
|
}
|
|
v.Title = menuWindowTitle
|
|
v.Wrap = true
|
|
v.Autoscroll = true
|
|
fmt.Fprint(v, strings.Join(menuOptions, "\n"))
|
|
if _, err = setCurrentViewOnTop(g, menuWindow); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if v, err := g.SetView(rawInputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil {
|
|
if !gocui.IsUnknownView(err) {
|
|
return err
|
|
}
|
|
v.Title = rawInputWindowTitle
|
|
v.Editable = true
|
|
v.Wrap = true
|
|
}
|
|
|
|
if v, err := g.SetView(chatInputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil {
|
|
if !gocui.IsUnknownView(err) {
|
|
return err
|
|
}
|
|
v.Title = chatInputWindowTitle
|
|
v.Editable = true
|
|
v.Wrap = true
|
|
|
|
if _, err = setCurrentViewOnTop(g, chatInputWindow); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func quit(g *gocui.Gui, v *gocui.View) error {
|
|
return gocui.ErrQuit
|
|
}
|
|
|
|
// Sends an input text from the user to the backend while also printing it in the chatlog window.
|
|
// KeyEnter is viewed as "\n" by gocui, so messages should only be one line, whereas raw sending has a different key
|
|
// binding and therefor should work with this too (for multiple lines stanzas)
|
|
func writeInput(g *gocui.Gui, v *gocui.View) error {
|
|
chatLogWindow, _ := g.View(chatLogWindow)
|
|
|
|
input := strings.Join(v.ViewBufferLines(), "\n")
|
|
|
|
fmt.Fprintln(chatLogWindow, "Me : ", input)
|
|
if viewState.input == rawInputWindow {
|
|
rawTextChan <- input
|
|
} else {
|
|
textChan <- input
|
|
}
|
|
|
|
v.Clear()
|
|
v.EditDeleteToStartOfLine()
|
|
return nil
|
|
}
|
|
|
|
func setKeyBindings(g *gocui.Gui) {
|
|
// ==========================
|
|
// All views
|
|
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
// ==========================
|
|
// Chat input
|
|
if err := g.SetKeybinding(chatInputWindow, gocui.KeyEnter, gocui.ModNone, writeInput); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding(chatInputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
// ==========================
|
|
// Raw input
|
|
if err := g.SetKeybinding(rawInputWindow, gocui.KeyCtrlE, gocui.ModNone, writeInput); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding(rawInputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
// ==========================
|
|
// Menu
|
|
if err := g.SetKeybinding(menuWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
if err := g.SetKeybinding(menuWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
if err := g.SetKeybinding(menuWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
if err := g.SetKeybinding(menuWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
// ==========================
|
|
// Contacts list
|
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
// ==========================
|
|
// Disconnect message
|
|
if err := g.SetKeybinding(disconnectMsg, gocui.KeyEnter, gocui.ModNone, delMsg); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
}
|
|
|
|
// General
|
|
// Used to handle menu selections and navigations
|
|
func getLine(g *gocui.Gui, v *gocui.View) error {
|
|
var l string
|
|
var err error
|
|
|
|
_, cy := v.Cursor()
|
|
if l, err = v.Line(cy); err != nil {
|
|
l = ""
|
|
}
|
|
if viewState.side == menuWindow {
|
|
if l == contactList {
|
|
cv, _ := g.View(contactsListWindow)
|
|
viewState.side = contactsListWindow
|
|
g.SetViewOnTop(contactsListWindow)
|
|
g.SetCurrentView(contactsListWindow)
|
|
if len(cv.ViewBufferLines()) == 0 {
|
|
printContactsToWindow(g, viewState.contacts)
|
|
}
|
|
} else if l == disconnect {
|
|
maxX, maxY := g.Size()
|
|
msg := "You disconnected from the server. Press enter to quit."
|
|
if v, err := g.SetView(disconnectMsg, maxX/2-30, maxY/2, maxX/2-29+len(msg), maxY/2+2, 0); err != nil {
|
|
if !gocui.IsUnknownView(err) {
|
|
return err
|
|
}
|
|
fmt.Fprintln(v, msg)
|
|
if _, err := g.SetCurrentView(disconnectMsg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
killChan <- disconnectErr
|
|
} else if l == askServerForRoster {
|
|
chlw, _ := g.View(chatLogWindow)
|
|
fmt.Fprintln(chlw, infoFormat+"Asking server for contacts list...")
|
|
rosterChan <- struct{}{}
|
|
} else if l == rawMode {
|
|
mw, _ := g.View(menuWindow)
|
|
viewState.input = rawInputWindow
|
|
g.SetViewOnTop(rawInputWindow)
|
|
g.SetCurrentView(rawInputWindow)
|
|
menuOptions[1] = messageMode
|
|
v.Clear()
|
|
v.EditDeleteToStartOfLine()
|
|
fmt.Fprintln(mw, strings.Join(menuOptions, "\n"))
|
|
message := "Now sending in raw stanza mode"
|
|
clv, _ := g.View(chatLogWindow)
|
|
fmt.Fprintln(clv, infoFormat+message)
|
|
} else if l == messageMode {
|
|
mw, _ := g.View(menuWindow)
|
|
viewState.input = chatInputWindow
|
|
g.SetViewOnTop(chatInputWindow)
|
|
g.SetCurrentView(chatInputWindow)
|
|
menuOptions[1] = rawMode
|
|
v.Clear()
|
|
v.EditDeleteToStartOfLine()
|
|
fmt.Fprintln(mw, strings.Join(menuOptions, "\n"))
|
|
message := "Now sending in messages mode"
|
|
clv, _ := g.View(chatLogWindow)
|
|
fmt.Fprintln(clv, infoFormat+message)
|
|
}
|
|
} else if viewState.side == contactsListWindow {
|
|
if l == backFromContacts {
|
|
viewState.side = menuWindow
|
|
g.SetViewOnTop(menuWindow)
|
|
g.SetCurrentView(menuWindow)
|
|
} else if l == "" {
|
|
return nil
|
|
} else {
|
|
// Updating the current correspondent, back-end side.
|
|
CorrespChan <- l
|
|
viewState.currentContact = l
|
|
// Showing the selected contact in contacts list
|
|
cl, _ := g.View(contactsListWindow)
|
|
cts := cl.ViewBufferLines()
|
|
cl.Clear()
|
|
printContactsToWindow(g, cts)
|
|
// Showing a message to the user, and switching back to input after the new contact is selected.
|
|
message := "Now sending messages to : " + l + " in a private conversation"
|
|
clv, _ := g.View(chatLogWindow)
|
|
fmt.Fprintln(clv, infoFormat+message)
|
|
g.SetCurrentView(chatInputWindow)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func printContactsToWindow(g *gocui.Gui, contactsList []string) {
|
|
cl, _ := g.View(contactsListWindow)
|
|
for _, c := range contactsList {
|
|
c = strings.ReplaceAll(c, " *", "")
|
|
if c == viewState.currentContact {
|
|
fmt.Fprintf(cl, c+" *\n")
|
|
} else {
|
|
fmt.Fprintf(cl, c+"\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Changing view between input and "menu/contacts" when pressing the specific key.
|
|
func nextView(g *gocui.Gui, v *gocui.View) error {
|
|
if v == nil || v.Name() == chatInputWindow || v.Name() == rawInputWindow {
|
|
_, err := g.SetCurrentView(viewState.side)
|
|
return err
|
|
} else if v.Name() == menuWindow || v.Name() == contactsListWindow {
|
|
_, err := g.SetCurrentView(viewState.input)
|
|
return err
|
|
}
|
|
|
|
// Should not be reached right now
|
|
_, err := g.SetCurrentView(chatInputWindow)
|
|
return err
|
|
}
|
|
|
|
func cursorDown(g *gocui.Gui, v *gocui.View) error {
|
|
if v != nil {
|
|
cx, cy := v.Cursor()
|
|
// Avoid going below the list of contacts. Although lines are stored in the view as a slice
|
|
// in the used lib. Therefor, if the number of lines is too big, the cursor will go past the last line since
|
|
// increasing slice capacity is done by doubling it. Last lines will be "nil" and reachable by the cursor
|
|
// in a dynamic context (such as contacts list)
|
|
cv := g.CurrentView()
|
|
h := cv.LinesHeight()
|
|
if cy+1 >= h {
|
|
return nil
|
|
}
|
|
// Lower cursor
|
|
if err := v.SetCursor(cx, cy+1); err != nil {
|
|
ox, oy := v.Origin()
|
|
if err := v.SetOrigin(ox, oy+1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cursorUp(g *gocui.Gui, v *gocui.View) error {
|
|
if v != nil {
|
|
ox, oy := v.Origin()
|
|
cx, cy := v.Cursor()
|
|
if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
|
|
if err := v.SetOrigin(ox, oy-1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func delMsg(g *gocui.Gui, v *gocui.View) error {
|
|
if err := g.DeleteView(disconnectMsg); err != nil {
|
|
return err
|
|
}
|
|
errChan <- gocui.ErrQuit // Quit the program
|
|
return nil
|
|
}
|