Continue work on basic functionality

master
Tyler Sommer 3 years ago
parent 6938bd6fae
commit 5b9b835cb1
Signed by: tyler-sommer
GPG Key ID: C09C010500DBD008

@ -2,30 +2,157 @@ package squirssi
import (
"strconv"
"strings"
"code.dopame.me/veonik/squircy3/event"
"code.dopame.me/veonik/squircy3/irc"
"github.com/sirupsen/logrus"
)
type Command func(*Server, []string)
var builtIns = map[string]Command{
"w": selectWindow,
"wc": closeWindow,
"w": selectWindow,
"wc": closeWindow,
"join": joinChannel,
"part": partChannel,
"whois": whoisNick,
"names": namesChannel,
"nick": changeNick,
}
func selectWindow(srv *Server, args []string) {
if len(args) < 2 {
logrus.Warnln("selectWindow: expected one argument")
return
}
var err error
ch, err := strconv.Atoi(args[1])
if err != nil {
logrus.Warnln("selectWindow: expected first argument to be an integer")
return
}
if ch < len(srv.statusBar.TabNames) {
srv.statusBar.ActiveTabIndex = ch
srv.Update()
srv.Render()
srv.mu.Lock()
if ch >= len(srv.windows) {
logrus.Warnf("selectWindow: no window #%d", ch)
srv.mu.Unlock()
return
}
srv.statusBar.ActiveTabIndex = ch
srv.mu.Unlock()
srv.Update()
srv.Render()
}
func closeWindow(srv *Server, args []string) {
var ch int
if len(args) < 2 {
srv.mu.Lock()
ch = srv.statusBar.ActiveTabIndex
srv.mu.Unlock()
} else {
var err error
ch, err = strconv.Atoi(args[1])
if err != nil {
logrus.Warnln("selectWindow: expected first argument to be an integer")
return
}
}
win := srv.windows[ch]
if strings.HasPrefix(win.Title(), "#") {
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.Part(win.Title())
return nil
}); err != nil {
logrus.Warnln("closeWindow: failed to part channel before closing window")
}
}
srv.CloseWindow(ch)
}
func joinChannel(srv *Server, args []string) {
if len(args) < 2 {
logrus.Warnln("joinChannel: expected one argument")
return
}
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.Join(args[1])
return nil
}); err != nil {
logrus.Warnln("joinChannel: error joining channel:", err)
}
}
func partChannel(srv *Server, args []string) {
if len(args) < 2 {
logrus.Warnln("partChannel: expected one argument")
return
}
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.Part(args[1])
return nil
}); err != nil {
logrus.Warnln("partChannel: error joining channel:", err)
}
}
func whoisNick(srv *Server, args []string) {
if len(args) < 2 {
logrus.Warnln("whoisNick: expected one argument")
return
}
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.SendRawf("WHOIS %s", args[1])
return nil
}); err != nil {
logrus.Warnln("whoisNick: error getting whois info:", err)
}
}
func namesChannel(srv *Server, args []string) {
if len(args) < 2 {
logrus.Warnln("namesChannel: expected one argument")
return
}
channel := args[1]
win := srv.WindowNamed(channel)
if win == nil {
logrus.Warnln("namesChannel: no window named", channel)
return
}
irc353Handler := event.HandlerFunc(func(ev *event.Event) {
args := ev.Data["Args"].([]string)
chanName := args[2]
nicks := args[3]
logrus.Infof("NAMES %s: %s", chanName, nicks)
})
var irc366Handler event.Handler
irc366Handler = event.HandlerFunc(func(ev *event.Event) {
args := ev.Data["Args"].([]string)
chanName := args[1]
logrus.Infof("END NAMES %s", chanName)
srv.events.Unbind("irc.353", irc353Handler)
srv.events.Unbind("irc.366", irc366Handler)
})
srv.events.Bind("irc.353", irc353Handler)
srv.events.Bind("irc.366", irc366Handler)
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.SendRawf("NAMES :%s", channel)
return nil
}); err != nil {
logrus.Warnln("namesChannel: error getting names:", err)
}
}
func changeNick(srv *Server, args []string) {
if len(args) < 2 {
logrus.Warnln("changeNick: expected one argument")
return
}
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.Nick(args[1])
return nil
}); err != nil {
logrus.Warnln("changeNick: error changing nick:", err)
}
}

@ -2,54 +2,418 @@ package squirssi
import (
"fmt"
"os"
"strings"
"sync"
"time"
"code.dopame.me/veonik/squircy3/event"
"code.dopame.me/veonik/squircy3/irc"
"github.com/sirupsen/logrus"
)
func (srv *Server) bind() {
srv.events.Bind("ui.DIRTY", event.HandlerFunc(srv.onUIDirty))
srv.events.Bind("irc.CONNECT", event.HandlerFunc(srv.onIRCConnect))
srv.events.Bind("irc.DISCONNECT", event.HandlerFunc(srv.onIRCDisconnect))
srv.events.Bind("irc.PRIVMSG", event.HandlerFunc(srv.onIRCPrivmsg))
srv.events.Bind("irc.JOIN", event.HandlerFunc(srv.onIRCJoin))
srv.events.Bind("irc.PART", event.HandlerFunc(srv.onIRCPart))
srv.events.Bind("irc.KICK", event.HandlerFunc(srv.onIRCKick))
srv.events.Bind("irc.JOIN", event.HandlerFunc(srv.onIRCNames))
srv.events.Bind("irc.PART", event.HandlerFunc(srv.onIRCNames))
srv.events.Bind("irc.KICK", event.HandlerFunc(srv.onIRCNames))
srv.events.Bind("irc.NICK", event.HandlerFunc(srv.onIRCNick))
srv.events.Bind("irc.353", event.HandlerFunc(srv.onIRC353))
srv.events.Bind("irc.366", event.HandlerFunc(srv.onIRC366))
errorCodes := []string{"irc.401", "irc.403", "irc.404", "irc.405", "irc.406", "irc.407", "irc.408", "irc.421"}
for _, code := range errorCodes {
srv.events.Bind(code, event.HandlerFunc(srv.onIRCError))
}
whoisCodes := []string{"irc.311", "irc.312", "irc.313", "irc.317", "irc.318", "irc.319", "irc.314", "irc.369"}
for _, code := range whoisCodes {
srv.events.Bind(code, event.HandlerFunc(srv.onIRCWhois))
}
}
func (srv *Server) onUIDirty(_ *event.Event) {
srv.Update()
srv.Render()
}
// onUIKeyPress handles keyboard input from termui.
// Not a regular event handler but instead called before the actual
// ui.KEYPRESS event is emitted. This is done to avoid extra lag between
// pressing a key and seeing the UI react.
func (srv *Server) onUIKeyPress(key string) {
switch key {
case "<C-c>":
srv.Close()
os.Exit(0)
return
case "<PageUp>":
srv.ScrollPageUp()
case "<PageDown>":
srv.ScrollPageDown()
case "<Home>":
srv.ScrollTop()
case "<End>":
srv.ScrollBottom()
case "<Space>":
srv.mu.Lock()
srv.inputTextBox.Append(" ")
srv.mu.Unlock()
srv.RenderOnly(InputTextBox)
case "<Backspace>":
srv.mu.Lock()
srv.inputTextBox.Backspace()
srv.mu.Unlock()
srv.RenderOnly(InputTextBox)
case "<C-5>":
srv.mu.Lock()
srv.statusBar.FocusRight()
srv.mu.Unlock()
srv.Update()
srv.Render()
case "<Escape>":
srv.mu.Lock()
srv.statusBar.FocusLeft()
srv.mu.Unlock()
srv.Update()
srv.Render()
case "<Tab>":
srv.mu.Lock()
channel := srv.windows[srv.statusBar.ActiveTabIndex]
srv.mu.Unlock()
if ch, ok := channel.(WindowWithUserList); ok {
msg := srv.inputTextBox.Peek()
parts := strings.Split(msg, " ")
match := parts[len(parts)-1]
extra := ""
if match == parts[0] {
extra = ": "
}
for _, u := range ch.Users() {
nick := strings.ReplaceAll(strings.ReplaceAll(u, "@", ""), "+", "")
if strings.HasPrefix(nick, match) {
srv.inputTextBox.Append(strings.Replace(nick, match, "", 1) + extra)
break
}
}
}
srv.RenderOnly(InputTextBox)
case "<Enter>":
srv.mu.Lock()
in := srv.inputTextBox.Consume()
if srv.inputTextBox.Mode() == ModeCommand {
srv.inputTextBox.ToggleMode()
}
active := srv.statusBar.ActiveTabIndex
channel := srv.windows[active]
myNick := srv.currentNick
srv.mu.Unlock()
if channel == nil {
return
}
if len(in.Text) == 0 {
// render anyway incase the textbox mode was changed
srv.RenderOnly(MainWindow)
return
}
switch in.Kind {
case ModeCommand:
args := strings.Split(in.Text, " ")
c := args[0]
if cmd, ok := builtIns[c]; ok {
cmd(srv, args)
}
case ModeMessage:
if active == 0 {
// status window doesn't accept messages
return
}
if err := srv.irc.Do(func(c *irc.Connection) error {
c.Privmsg(channel.Title(), in.Text)
return nil
}); err != nil {
logrus.Warnln("failed to send message:", err)
}
if _, err := channel.Write([]byte("<" + myNick + "> " + in.Text)); err != nil {
logrus.Warnln("failed to write message:", err)
}
}
default:
if len(key) != 1 {
// a single key resulted in more than one character, probably not a regular char
return
}
srv.mu.Lock()
if key == "/" && srv.inputTextBox.Len() == 0 {
srv.inputTextBox.ToggleMode()
} else {
srv.inputTextBox.Append(key)
}
srv.mu.Unlock()
srv.RenderOnly(InputTextBox)
}
}
func (srv *Server) onIRCConnect(_ *event.Event) {
err := srv.irc.Do(func(conn *irc.Connection) error {
srv.mu.Lock()
defer srv.mu.Unlock()
srv.currentNick = conn.GetNick()
return nil
})
if err != nil {
logrus.Warnln("failed to set current nick:", err)
}
}
func (srv *Server) onIRCDisconnect(_ *event.Event) {
logrus.Infoln("*** Disconnected")
srv.mu.Lock()
defer srv.mu.Unlock()
srv.currentNick = ""
}
func (srv *Server) onIRCNick(ev *event.Event) {
logrus.Infoln("irc.NICK:", ev.Data)
nick := ev.Data["Nick"].(string)
newNick := ev.Data["Message"].(string)
srv.mu.Lock()
if nick == srv.currentNick {
srv.currentNick = newNick
}
wins := make([]Window, len(srv.windows))
copy(wins, srv.windows)
srv.mu.Unlock()
for _, win := range wins {
if win.Title() == nick {
// direct message with nick, update title and print there
if dm, ok := win.(*DirectMessage); ok {
dm.mu.Lock()
dm.name = nick
dm.mu.Unlock()
}
if _, err := win.Write([]byte(fmt.Sprintf("*** %s is now known as %s", nick, newNick))); err != nil {
logrus.Warnln("failed to write nick change:", err)
}
} else if ch, ok := win.(*Channel); ok {
hasNick := false
ch.mu.Lock()
for i, u := range ch.users {
if strings.ReplaceAll(strings.ReplaceAll(u, "@", ""), "+", "") == nick {
ch.users[i] = newNick
hasNick = true
break
}
}
ch.mu.Unlock()
if hasNick {
if _, err := win.Write([]byte(fmt.Sprintf("*** %s is now known as %s", nick, newNick))); err != nil {
logrus.Warnln("failed to write nick change:", err)
}
}
}
}
}
func (srv *Server) onIRCKick(ev *event.Event) {
args := ev.Data["Args"].([]string)
channel := ev.Data["Target"].(string)
kicked := args[1]
srv.mu.Lock()
myNick := srv.currentNick
srv.mu.Unlock()
if kicked == myNick {
go func() {
<-time.After(2 * time.Second)
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.Join(channel)
return nil
}); err != nil {
logrus.Warnln("failed to rejoin after kick:", err)
}
}()
return
}
win := srv.WindowNamed(channel)
if win == nil {
logrus.Errorln("received kick with no Window:", channel, ev.Data["Message"], ev.Data["Nick"])
return
}
if _, err := win.Write([]byte(fmt.Sprintf("*** %s got kicked from %s (%s)", kicked, channel, ev.Data["Message"].(string)))); err != nil {
logrus.Warnln("failed to write message:", err)
}
}
var namesCache = &struct {
sync.Mutex
values map[string][]string
}{values: make(map[string][]string)}
func (srv *Server) onIRC353(ev *event.Event) {
// NAMES
args := ev.Data["Args"].([]string)
chanName := args[2]
nicks := strings.Split(args[3], " ")
win := srv.WindowNamed(chanName)
if win == nil {
logrus.Warnln("received NAMES for channel with no window:", chanName)
return
}
namesCache.Lock()
defer namesCache.Unlock()
namesCache.values[chanName] = append(namesCache.values[chanName], nicks...)
}
func (srv *Server) onIRC366(ev *event.Event) {
// END NAMES
args := ev.Data["Args"].([]string)
chanName := args[1]
win := srv.WindowNamed(chanName)
if win == nil {
logrus.Warnln("received END NAMES for channel with no window:", chanName)
return
}
ch, ok := win.(*Channel)
if !ok {
logrus.Warnln("received END NAMES for a non channel:", chanName)
return
}
namesCache.Lock()
defer namesCache.Unlock()
ch.mu.Lock()
ch.users = namesCache.values[chanName]
ch.mu.Unlock()
delete(namesCache.values, chanName)
}
func (srv *Server) onIRCError(ev *event.Event) {
args := ev.Data["Args"].([]string)
var kind string
if len(args) > 1 {
kind = args[1]
} else {
kind = ev.Data["Target"].(string)
}
logrus.Errorf("%s: %s", kind, ev.Data["Message"])
}
func (srv *Server) onIRCWhois(ev *event.Event) {
args := ev.Data["Args"].([]string)
kind := args[1]
data := args[2:]
if _, err := srv.status.Write([]byte(fmt.Sprintf("WHOIS %s => %s", kind, strings.Join(data, " ")))); err != nil {
logrus.Warnln("failed to write whois result to status:", err)
}
}
func (srv *Server) onIRCNames(ev *event.Event) {
if ev.Name == "irc.PART" || ev.Name == "irc.KICK" {
srv.mu.Lock()
myNick := srv.currentNick
srv.mu.Unlock()
nick := ev.Data["Nick"].(string)
if nick == myNick {
// dont bother trying to get names when we are the one leaving
return
}
}
target := ev.Data["Target"].(string)
if strings.HasPrefix(target, "#") {
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.SendRawf("NAMES :%s", target)
return nil
}); err != nil {
logrus.Warnln("failed to run NAMES on user change:", err)
}
}
}
func (srv *Server) onIRCJoin(ev *event.Event) {
target := ev.Data["Target"].(string)
win := srv.WindowNamed(target)
if win == nil {
srv.mu.Lock()
ch := &Channel{
bufferedWindow: newBufferedWindow(target, srv.events),
users: []string{srv.currentNick},
}
srv.windows = append(srv.windows, ch)
srv.mu.Unlock()
win = ch
}
if _, err := win.Write([]byte(fmt.Sprintf("*** %s joined %s", ev.Data["Nick"], win.Title()))); err != nil {
logrus.Warnln("%s: failed to write join message:", err)
}
}
func (srv *Server) onIRCPart(ev *event.Event) {
var win Window
var ch int
target := ev.Data["Target"].(string)
for _, w := range srv.windows {
nick := ev.Data["Nick"].(string)
srv.mu.Lock()
for i, w := range srv.windows {
if w.Title() == target {
win = w
ch = i
break
}
}
myNick := srv.currentNick
srv.mu.Unlock()
if win == nil {
ch := &Channel{
bufferedWindow: newBufferedWindow(target, srv.events),
users: []string{"veonik"},
if nick != myNick {
// dont bother logging if we are the ones leaving
logrus.Errorln("received message with no Window:", target, ev.Data["Message"], ev.Data["Nick"])
}
srv.windows = append(srv.windows, ch)
return
}
if nick == myNick {
// its me!
srv.CloseWindow(ch)
}
if _, err := win.Write([]byte(fmt.Sprintf("*** %s parted %s", nick, win.Title()))); err != nil {
logrus.Warnln("%s: failed to write part message:", err)
}
}
func (srv *Server) onIRCPrivmsg(ev *event.Event) {
var win Window
direct := false
target := ev.Data["Target"].(string)
nick := ev.Data["Nick"].(string)
srv.mu.Lock()
if target == srv.currentNick {
// its a direct message!
direct = true
target = nick
}
for _, w := range srv.windows {
if w.Title() == target {
win = w
break
}
}
srv.mu.Unlock()
if win == nil {
logrus.Warnln("received message with no Window:", target, ev.Data["Message"], ev.Data["Nick"])
} else {
if v, ok := win.(*Channel); ok {
if v.current == len(v.lines)-1 {
v.current++
}
if _, err := v.Write([]byte(fmt.Sprintf("<%s> %s", ev.Data["Nick"], ev.Data["Message"]))); err != nil {
logrus.Warnln("error writing to Channel:", err)
return
}
if !direct {
logrus.Warnln("received message with no Window:", target, ev.Data["Message"], nick)
return
} else {
srv.mu.Lock()
ch := &DirectMessage{bufferedWindow: newBufferedWindow(target, srv.events)}
srv.windows = append(srv.windows, ch)
win = ch
srv.mu.Unlock()
}
}
if _, err := win.Write([]byte(fmt.Sprintf("<%s> %s", nick, ev.Data["Message"]))); err != nil {
logrus.Warnln("error writing to Window:", err)
}
}

@ -16,7 +16,7 @@ func FromPlugins(m *plugin.Manager) (*Server, error) {
}
mplg, ok := plg.(*squirssiPlugin)
if !ok {
return nil, errors.Errorf("event: received unexpected plugin type")
return nil, errors.Errorf("event: received unexpected plugin type %T", plg)
}
return mplg.server, nil
}

@ -3,8 +3,6 @@ package squirssi
import (
"fmt"
"os"
"strings"
"sync"
"code.dopame.me/veonik/squircy3/event"
@ -16,6 +14,7 @@ import (
"code.dopame.me/veonik/squirssi/colors"
)
// A Server handles user interaction and displaying screen elements.
type Server struct {
*logrus.Logger
@ -31,12 +30,38 @@ type Server struct {
events *event.Dispatcher
irc *irc.Manager
currentNick string
windows []Window
status *Status
mu sync.Mutex
}
type logFormatter struct{}
func (f *logFormatter) Format(entry *logrus.Entry) ([]byte, error) {
lvl := ""
switch entry.Level {
case logrus.InfoLevel:
lvl = "[INFO ](fg:blue)"
case logrus.DebugLevel:
lvl = "[DEBUG](fg:white)"
case logrus.WarnLevel:
lvl = "[WARN ](fg:yellow)"
case logrus.ErrorLevel:
lvl = "[ERROR](fg:red)"
case logrus.FatalLevel:
lvl = "[FATAL](fg:white,bg:red)"
case logrus.TraceLevel:
lvl = "[TRACE](fg:white)"
case logrus.PanicLevel:
lvl = "[PANIC](fg:white,bg:red)"
}
return []byte(fmt.Sprintf("%s: %s", lvl, entry.Message)), nil
}
// NewServer creates a new server.
func NewServer(ev *event.Dispatcher, irc *irc.Manager) (*Server, error) {
if err := ui.Init(); err != nil {
return nil, err
@ -50,11 +75,106 @@ func NewServer(ev *event.Dispatcher, irc *irc.Manager) (*Server, error) {
events: ev,
irc: irc,
status: &Status{bufferedWindow: newBufferedWindow("status", ev)},
}
srv.windows = []Window{srv.status}
srv.Logger.SetOutput(srv.status)
srv.Logger.SetFormatter(&logFormatter{})
srv.initUI()
return srv, nil
}
// WindowNamed returns the window with the given name, if it exists.
func (srv *Server) WindowNamed(name string) Window {
var win Window
srv.mu.Lock()
defer srv.mu.Unlock()
for _, w := range srv.windows {
if w.Title() == name {
win = w
break
}
}
return win
}
// CloseWindow closes a window denoted by tab index.
func (srv *Server) CloseWindow(ch int) {
if ch == 0 {
logrus.Warnln("CloseWindow: cant close status window")
return
}
srv.mu.Lock()
if ch >= len(srv.windows) {
logrus.Warnf("CloseWindow: no window #%d", ch)
srv.mu.Unlock()
return
}
srv.windows = append(srv.windows[:ch], srv.windows[ch+1:]...)
if ch >= srv.statusBar.ActiveTabIndex {
srv.statusBar.ActiveTabIndex = ch - 1
}
srv.mu.Unlock()
srv.Update()
srv.Render()
}
// ScrollPageUp scrolls one opage up in the current window.
func (srv *Server) ScrollPageUp() {
srv.mu.Lock()
srv.chatPane.ScrollPageUp()
row := srv.chatPane.SelectedRow
channel := srv.windows[srv.statusBar.ActiveTabIndex]
srv.mu.Unlock()
if channel == nil {
return
}
channel.ScrollTo(row)
srv.Render()
}
// ScrollPageDown scrolls one page down in the current window.
func (srv *Server) ScrollPageDown() {
srv.mu.Lock()
srv.chatPane.ScrollPageDown()
row := srv.chatPane.SelectedRow
channel := srv.windows[srv.statusBar.ActiveTabIndex]
srv.mu.Unlock()
if channel == nil {
return
}
channel.ScrollTo(row)
srv.Render()
}
// ScrollTop scrolls to the top of the current window.
func (srv *Server) ScrollTop() {
srv.mu.Lock()
srv.chatPane.ScrollTop()
row := srv.chatPane.SelectedRow
channel := srv.windows[srv.statusBar.ActiveTabIndex]
srv.mu.Unlock()
if channel == nil {
return
}
channel.ScrollTo(row)
srv.Render()
}
// ScrollBottom scrolls to the end of the current window.
func (srv *Server) ScrollBottom() {
srv.mu.Lock()
srv.chatPane.ScrollBottom()
channel := srv.windows[srv.statusBar.ActiveTabIndex]
srv.mu.Unlock()
if channel == nil {
return
}
channel.ScrollTo(-1)
srv.Render()
}
func (srv *Server) initUI() {
srv.userListPane = widgets.NewTable()
srv.userListPane.Rows = [][]string{}
@ -71,6 +191,7 @@ func (srv *Server) initUI() {
srv.chatPane.Border = true
srv.chatPane.PaddingLeft = 1
srv.chatPane.PaddingRight = 1
srv.chatPane.WrapText = true
srv.statusBar = &ActivityTabPane{
TabPane: widgets.NewTabPane(" 0 "),
@ -89,39 +210,16 @@ func (srv *Server) initUI() {
srv.inputTextBox.Border = false
srv.mainWindow = ui.NewGrid()
srv.status = &Status{bufferedWindow: newBufferedWindow("status", srv.events)}
srv.windows = []Window{srv.status}
}
// Close ends the UI session, returning control of stdout.
func (srv *Server) Close() {
srv.mu.Lock()
defer srv.mu.Unlock()
ui.Close()
}
type screenElement int
const (
InputTextBox screenElement = iota
StatusBar
MainWindow
)
func (srv *Server) RenderOnly(items ...screenElement) {
var its []ui.Drawable
for _, it := range items {
switch it {
case InputTextBox:
its = append(its, srv.inputTextBox)
case StatusBar:
its = append(its, srv.statusBar)
case MainWindow:
its = append(its, srv.mainWindow)
}
}
ui.Render(its...)
}
func tabNames(windows []Window) ([]string, map[int]struct{}) {
func tabNames(windows []Window, active int) ([]string, map[int]struct{}) {
res := make([]string, len(windows))
activity := make(map[int]struct{})
for i := 0; i < len(windows); i++ {
@ -129,25 +227,31 @@ func tabNames(windows []Window) ([]string, map[int]struct{}) {
if win.HasActivity() {
activity[i] = struct{}{}
}
res[i] = fmt.Sprintf(" %d ", i)
if active == i {
res[i] = fmt.Sprintf(" %s ", win.Title())
} else {
res[i] = fmt.Sprintf(" %d ", i)
}
}
return res, activity
}
// Update refreshes the state of the UI but stops short of rendering.
func (srv *Server) Update() {
srv.mu.Lock()
defer srv.mu.Unlock()
channel := srv.windows[srv.statusBar.ActiveTabIndex]
if channel == nil {
win := srv.windows[srv.statusBar.ActiveTabIndex]
if win == nil {
return
}
srv.statusBar.TabNames, srv.statusBar.TabsWithActivity = tabNames(srv.windows)
srv.chatPane.Rows = channel.Lines()
srv.chatPane.Title = channel.Title()
srv.chatPane.SelectedRow = channel.CurrentLine()
win.Touch()
srv.statusBar.TabNames, srv.statusBar.TabsWithActivity = tabNames(srv.windows, srv.statusBar.ActiveTabIndex)
srv.chatPane.Rows = win.Lines()
srv.chatPane.Title = win.Title()
srv.chatPane.SelectedRow = win.CurrentLine()
srv.mainWindow.Items = nil
var rows [][]string
if v, ok := channel.(WindowWithUserList); ok {
if v, ok := win.(WindowWithUserList); ok {
for _, nick := range v.Users() {
rows = append(rows, []string{nick})
}
@ -163,101 +267,64 @@ func (srv *Server) Update() {
srv.userListPane.Rows = rows
}
func (srv *Server) Render() {
srv.mu.Lock()
defer srv.mu.Unlock()
ui.Render(srv.mainWindow, srv.statusBar, srv.inputTextBox)
}
type screenElement int
func (srv *Server) handleKey(e ui.Event) {
switch e.ID {
case "<C-c>":
srv.Close()
os.Exit(0)
return
case "<PageUp>":
srv.chatPane.ScrollPageUp()
srv.Render()
case "<PageDown>":
srv.chatPane.ScrollPageDown()
srv.Render()
case "<Space>":
srv.inputTextBox.Append(" ")
srv.RenderOnly(InputTextBox)
case "<Backspace>":
srv.inputTextBox.Backspace()
srv.RenderOnly(InputTextBox)
case "<C-5>":
srv.statusBar.FocusRight()
srv.Update()
srv.Render()
case "<Escape>":
srv.statusBar.FocusLeft()
srv.Update()
srv.Render()
case "<Tab>":
case "<Enter>":
in := srv.inputTextBox.Consume()
if srv.inputTextBox.Mode() == ModeCommand {
srv.inputTextBox.ToggleMode()
}
channel := srv.windows[srv.statusBar.ActiveTabIndex]
if channel == nil {
return
}
if len(in.Text) == 0 {
return
}
switch in.Kind {
case ModeCommand:
args := strings.Split(in.Text, " ")
c := args[0]
if cmd, ok := builtIns[c]; ok {
cmd(srv, args)
}
case ModeMessage:
if err := srv.irc.Do(func(c *irc.Connection) error {
c.Privmsg(channel.Title(), in.Text)
_, err := channel.Write([]byte("<veonik> " + in.Text))
return err
}); err != nil {
logrus.Warnln("failed to send message:", err)
}
}
const (
InputTextBox screenElement = iota
StatusBar
MainWindow
)
default:
if len(e.ID) != 1 {
// a single key resulted in more than one character, probably not a regular char
return
}
if e.ID == "/" && srv.inputTextBox.Len() == 0 {
srv.inputTextBox.ToggleMode()
} else {
srv.inputTextBox.Append(e.ID)
func (srv *Server) preRender() {
if len(srv.chatPane.Rows) == 0 {
srv.chatPane.SelectedRow = 0
} else if srv.chatPane.SelectedRow < 0 {
srv.chatPane.ScrollBottom()
}
}
// RenderOnly renders select screen elements rather than the whole screen.
func (srv *Server) RenderOnly(items ...screenElement) {
srv.mu.Lock()
defer srv.mu.Unlock()
var its []ui.Drawable
for _, it := range items {
switch it {
case InputTextBox:
its = append(its, srv.inputTextBox)
case StatusBar:
its = append(its, srv.statusBar)
case MainWindow:
srv.preRender()
its = append(its, srv.mainWindow)
}
srv.RenderOnly(InputTextBox)
}
ui.Render(its...)
}
// Render renders the current state to the screen.
func (srv *Server) Render() {
srv.mu.Lock()
defer srv.mu.Unlock()
srv.preRender()
ui.Render(srv.mainWindow, srv.statusBar, srv.inputTextBox)
}
func (srv *Server) resize() {
srv.mu.Lock()
defer srv.mu.Unlock()
srv.statusBar.SetRect(0, srv.ScreenHeight-3, srv.ScreenWidth, srv.ScreenHeight)
srv.inputTextBox.SetRect(0, srv.ScreenHeight-srv.statusBar.Dy()-1, srv.ScreenWidth, srv.ScreenHeight-srv.statusBar.Dy())
srv.mainWindow.SetRect(0, 0, srv.ScreenWidth, srv.ScreenHeight-srv.statusBar.Dy()-srv.inputTextBox.Dy())
}
func (srv *Server) bind() {
srv.events.Bind("ui.DIRTY", event.HandlerFunc(srv.onUIDirty))
srv.events.Bind("irc.JOIN", event.HandlerFunc(srv.onIRCJoin))
srv.events.Bind("irc.PRIVMSG", event.HandlerFunc(srv.onIRCPrivmsg))
}
// Start begins the UI event loop and does the initial render.
func (srv *Server) Start() {
srv.bind()
srv.inputTextBox.Reset()
srv.resize()
srv.Update()
srv.Render()
srv.Logger.SetOutput(srv.status)
uiEvents := ui.PollEvents()
@ -265,7 +332,10 @@ func (srv *Server) Start() {
e := <-uiEvents
switch e.Type {
case ui.KeyboardEvent:
srv.handleKey(e)
// handle keyboard input outside of the event emitter to avoid
// too long a delay between keypress and the UI reacting.
srv.onUIKeyPress(e.ID)
srv.RenderOnly(InputTextBox)
srv.events.Emit("ui.KEYPRESS", map[string]interface{}{
"key": e.ID,
})
@ -285,14 +355,17 @@ func (srv *Server) Start() {
if !ok {
panic(fmt.Sprintf("received termui Resize event but Payload was unexpected type %T", e.Payload))
}
srv.mu.Lock()
srv.ScreenHeight = resize.Height
srv.ScreenWidth = resize.Width
srv.mu.Unlock()
srv.resize()
srv.Update()
srv.Render()
srv.events.Emit("ui.RESIZE", map[string]interface{}{
"width": resize.Width,
"height": resize.Height,
})
srv.Render()
}
}
}

@ -35,22 +35,38 @@ func NewTextInput(cursor string) *TextInput {
}
}
// Peek returns the current input in the TextInput without clearing.
func (i *TextInput) Peek() string {
if i.Len() < 1 {
return ""
}
i.Lock()
defer i.Unlock()
return i.Text[i.prefixLen : len(i.Text)-i.cursorLen]
}
// Consume returns and clears the current input in the TextInput.
func (i *TextInput) Consume() string {
defer i.Reset()
if i.Len() < 1 {
return ""
}
i.Lock()
defer i.Unlock()
return i.Text[i.prefixLen : len(i.Text)-i.cursorLen]
}
// Len returns the length of the contents of the TextInput.
func (i *TextInput) Len() int {
i.Lock()
defer i.Unlock()
return len(i.Text) - i.cursorLen - i.prefixLen
}
// Reset the contents of the TextInput.
func (i *TextInput) Reset() {
i.Lock()
defer i.Unlock()
prefix := ""
if i.Prefix != nil {
prefix = i.Prefix()
@ -61,12 +77,16 @@ func (i *TextInput) Reset() {
// Append adds the given string to the end of the editable content.
func (i *TextInput) Append(in string) {
i.Lock()
defer i.Unlock()
i.Text = i.Text[0:len(i.Text)-i.cursorLen] + in + i.cursor
}
// Remove the last character from the end of the editable content.
func (i *TextInput) Backspace() {
if len(i.Paragraph.Text) > i.prefixLen+i.cursorLen {
i.Lock()
defer i.Unlock()
if len(i.Text) > i.prefixLen+i.cursorLen {
i.Text = (i.Text)[0:len(i.Text)-i.cursorLen-1] + i.cursor
}
}
@ -105,16 +125,20 @@ func NewModedTextInput(cursor string) *ModedTextInput {
// Mode returns the current editing mode.
func (i *ModedTextInput) Mode() InputMode {
i.Lock()
defer i.Unlock()
return i.mode
}
// ToggleMode switches between the editing modes.
func (i *ModedTextInput) ToggleMode() {
i.Lock()
if i.mode == ModeMessage {
i.mode = ModeCommand
} else {
i.mode = ModeMessage
}
i.Unlock()
i.Reset()
}
@ -126,8 +150,11 @@ type ModedText struct {
// Consume returns and clears the ModedText in the ModedTextInput.
func (i *ModedTextInput) Consume() ModedText {
txt := i.TextInput.Consume()
i.Lock()
defer i.Unlock()
return ModedText{
Kind: i.mode,
Text: i.TextInput.Consume(),
Text: txt,
}
}

@ -4,6 +4,7 @@ import (
"io"
"strings"
"sync"
"time"
"code.dopame.me/veonik/squircy3/event"
)
@ -15,9 +16,12 @@ type Window interface {
Title() string
// Contents of the Window, separated by line.
Lines() []string
// The bottom-most visible line number, or negative to indicate
// the window is pinned to the end of input.
CurrentLine() int
// Set the current line to pos. Set to negative to pin to the end of input.
ScrollTo(pos int)
// Clears the activity indicator for the window, it it's set.
Touch()
@ -43,8 +47,9 @@ type bufferedWindow struct {
func newBufferedWindow(name string, events *event.Dispatcher) bufferedWindow {
return bufferedWindow{
name: name,
events: events,
name: name,
events: events,
current: -1,
}
}
@ -58,7 +63,8 @@ func (c *bufferedWindow) Write(p []byte) (n int, err error) {
defer c.events.Emit("ui.DIRTY", map[string]interface{}{
"name": c.name,
})
c.lines = append(c.lines, strings.TrimRight(string(p), "\n"))
t := time.Now().Format("[15:04:05] ")
c.lines = append(c.lines, strings.TrimRight(t+string(p), "\n"))
c.hasUnseen = true
return len(p), nil
}
@ -87,6 +93,12 @@ func (c *bufferedWindow) CurrentLine() int {
return c.current
}
func (c *bufferedWindow) ScrollTo(pos int) {
c.mu.Lock()
defer c.mu.Unlock()
c.current = pos
}
type Status struct {
bufferedWindow
}
@ -104,6 +116,8 @@ type Channel struct {
}
func (c *Channel) Users() []string {
c.mu.Lock()
defer c.mu.Unlock()
return c.users
}

Loading…
Cancel
Save