mirror of https://github.com/veonik/squirssi
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
330 lines
6.3 KiB
330 lines
6.3 KiB
package squirssi
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"code.dopame.me/veonik/squircy3/event"
|
|
)
|
|
|
|
type Window interface {
|
|
io.Writer
|
|
io.StringWriter
|
|
|
|
// Title of the Window.
|
|
Title() string
|
|
// Lines returns the contents of the Window.
|
|
Lines() []string
|
|
|
|
// CurrentLine returns the bottom-most visible line number, or negative to indicate
|
|
// the window is pinned to the end of input.
|
|
CurrentLine() int
|
|
// ScrollTo sets the current line to pos. Set to negative to pin to the end of input.
|
|
ScrollTo(pos int)
|
|
// AutoScroll returns true if automatic scrolling is currently active.
|
|
AutoScroll() bool
|
|
|
|
// Touch clears the activity indicator for the window, it it's set.
|
|
Touch()
|
|
// HasActivity returns true if the Window has new lines since the last touch.
|
|
HasActivity() bool
|
|
// Notice set the notice indicator for this Window.
|
|
Notice()
|
|
// HasNotice returns true if the Window has new lines considered important since last touch.
|
|
HasNotice() bool
|
|
|
|
// padding returns how many characters wide the left gutter of the window is.
|
|
padding() int
|
|
}
|
|
|
|
type WindowWithUserList interface {
|
|
Window
|
|
// UserList returns the styled list of users.
|
|
UserList() []string
|
|
// Users returns just the usernames in the window.
|
|
Users() []string
|
|
// HasUser returns true if the window contains the given user.
|
|
HasUser(name string) bool
|
|
// UpdateUser updates the user to a new name in the current window.
|
|
UpdateUser(name, newNew string) bool
|
|
// DeleteUser removes the user from the current window.
|
|
DeleteUser(name string) bool
|
|
}
|
|
|
|
type bufferedWindow struct {
|
|
name string
|
|
lines []string
|
|
current int
|
|
|
|
hasUnseen bool
|
|
hasNotice bool
|
|
autoScroll bool
|
|
|
|
events *event.Dispatcher
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func newBufferedWindow(name string, events *event.Dispatcher) bufferedWindow {
|
|
return bufferedWindow{
|
|
name: name,
|
|
events: events,
|
|
|
|
current: -1,
|
|
autoScroll: true,
|
|
}
|
|
}
|
|
|
|
func (c *bufferedWindow) padding() int {
|
|
return 10
|
|
}
|
|
|
|
func (c *bufferedWindow) Title() string {
|
|
return c.name
|
|
}
|
|
|
|
func (c *bufferedWindow) Write(p []byte) (n int, err error) {
|
|
c.mu.Lock()
|
|
defer c.events.Emit("ui.DIRTY", map[string]interface{}{
|
|
"name": c.name,
|
|
})
|
|
defer c.mu.Unlock()
|
|
lines := bytes.Split(p, []byte("\n"))
|
|
t := time.Now().Format("[15:04](fg:gray) ")
|
|
const padding = " "
|
|
firstWritten := false
|
|
for _, l := range lines {
|
|
if len(l) == 0 {
|
|
continue
|
|
}
|
|
if !firstWritten {
|
|
c.lines = append(c.lines, strings.TrimRight(t+string(l), "\n"))
|
|
firstWritten = true
|
|
} else {
|
|
c.lines = append(c.lines, strings.TrimRight(padding+string(l), "\n"))
|
|
}
|
|
}
|
|
c.hasUnseen = true
|
|
return len(p), nil
|
|
}
|
|
|
|
func (c *bufferedWindow) WriteString(p string) (n int, err error) {
|
|
return c.Write([]byte(p))
|
|
}
|
|
|
|
func (c *bufferedWindow) Touch() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.hasUnseen = false
|
|
c.hasNotice = false
|
|
}
|
|
|
|
func (c *bufferedWindow) Notice() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.hasNotice = true
|
|
}
|
|
|
|
func (c *bufferedWindow) HasNotice() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.hasNotice
|
|
}
|
|
|
|
func (c *bufferedWindow) HasActivity() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.hasUnseen && !c.hasNotice
|
|
}
|
|
|
|
func (c *bufferedWindow) AutoScroll() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.autoScroll
|
|
}
|
|
|
|
func (c *bufferedWindow) Lines() []string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.lines
|
|
}
|
|
|
|
func (c *bufferedWindow) CurrentLine() int {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
if c.autoScroll {
|
|
return len(c.lines) - 1
|
|
}
|
|
return c.current
|
|
}
|
|
|
|
func (c *bufferedWindow) ScrollTo(pos int) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.current = pos
|
|
if pos < 0 {
|
|
c.autoScroll = true
|
|
} else {
|
|
c.autoScroll = false
|
|
}
|
|
}
|
|
|
|
type StatusWindow struct {
|
|
bufferedWindow
|
|
}
|
|
|
|
func (c *StatusWindow) padding() int {
|
|
return 6
|
|
}
|
|
|
|
func (c *StatusWindow) Title() string {
|
|
return "status"
|
|
}
|
|
|
|
type User struct {
|
|
string
|
|
modes string
|
|
}
|
|
|
|
func SomeUser(c string) User {
|
|
u := User{}
|
|
u.string = strings.ReplaceAll(strings.ReplaceAll(c, "@", ""), "+", "")
|
|
r := regexp.MustCompile("[^@+%]")
|
|
u.modes = r.ReplaceAllString(c, "")
|
|
return u
|
|
}
|
|
|
|
func (u User) String() string {
|
|
m := ""
|
|
if u.modes == "@" {
|
|
m = "[@](fg:cyan)"
|
|
} else if u.modes == "+" {
|
|
m = "[+](fg:yellow)"
|
|
}
|
|
return fmt.Sprintf("%s%s", m, u.string)
|
|
}
|
|
|
|
type Channel struct {
|
|
bufferedWindow
|
|
|
|
topic string
|
|
modes string
|
|
users []User
|
|
}
|
|
|
|
func (c *Channel) Topic() string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.topic
|
|
}
|
|
|
|
func (c *Channel) Modes() string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.modes
|
|
}
|
|
|
|
func (c *Channel) SetUsers(users []string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
r := make([]User, len(users))
|
|
for i, u := range users {
|
|
r[i] = SomeUser(u)
|
|
}
|
|
c.users = r
|
|
}
|
|
|
|
func (c *Channel) Users() []string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
t := make([]string, len(c.users))
|
|
for i, u := range c.users {
|
|
t[i] = u.string
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (c *Channel) UserList() []string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
t := make([]User, len(c.users))
|
|
copy(t, c.users)
|
|
sort.SliceStable(t, func(i, j int) bool {
|
|
ui := t[i]
|
|
uj := t[j]
|
|
if ui.modes == "@" && uj.modes != "@" {
|
|
return true
|
|
} else if uj.modes == "@" && ui.modes != "@" {
|
|
return false
|
|
}
|
|
if ui.modes == "+" && uj.modes != "+" {
|
|
return true
|
|
} else if uj.modes == "+" && ui.modes != "+" {
|
|
return false
|
|
}
|
|
return strings.Compare(ui.string, uj.string) < 0
|
|
})
|
|
res := make([]string, len(c.users))
|
|
for i, u := range t {
|
|
res[i] = u.String()
|
|
}
|
|
return res
|
|
}
|
|
|
|
func (c *Channel) userIndex(name string) int {
|
|
for i := 0; i < len(c.users); i++ {
|
|
if c.users[i].string == name {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func (c *Channel) AddUser(user User) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if idx := c.userIndex(user.string); idx >= 0 {
|
|
c.users[idx].modes = user.modes
|
|
return
|
|
}
|
|
c.users = append(c.users, user)
|
|
}
|
|
|
|
func (c *Channel) UpdateUser(name, newName string) bool {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if idx := c.userIndex(name); idx >= 0 {
|
|
c.users[idx].string = newName
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Channel) DeleteUser(name string) bool {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if idx := c.userIndex(name); idx >= 0 {
|
|
c.users = append(c.users[:idx], c.users[idx+1:]...)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Channel) HasUser(name string) bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
if idx := c.userIndex(name); idx >= 0 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
type DirectMessage struct {
|
|
bufferedWindow
|
|
}
|