@ -4,87 +4,33 @@ package squirssi
import (
"fmt"
"os"
"strconv"
"strings"
"sync"
"code.dopame.me/veonik/squircy3/event"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
"github.com/sirupsen/logrus"
"code.dopame.me/veonik/squirssi/colors"
)
type Window interface {
Title ( ) string
Lines ( ) [ ] string
HasActivity ( ) bool
}
type WindowWithNicklist interface {
Window
Nicklist ( ) [ ] string
}
type Channel struct {
name string
topic string
modes string
users [ ] string
lines [ ] string
current int
hasUnseen bool
}
func ( c * Channel ) HasActivity ( ) bool {
return c . hasUnseen
}
func ( c * Channel ) Title ( ) string {
return c . name
}
func ( c * Channel ) Lines ( ) [ ] string {
return c . lines
}
func ( c * Channel ) Nicklist ( ) [ ] string {
return c . users
}
type DirectMessage struct {
user string
lines [ ] string
current int
hasUnseen bool
}
func ( c * DirectMessage ) Title ( ) string {
return c . user
}
func ( c * DirectMessage ) Lines ( ) [ ] string {
return c . lines
}
func ( c * DirectMessage ) HasActivity ( ) bool {
return c . hasUnseen
}
type Server struct {
ScreenWidth , ScreenHeight int
mainWindow * ui . Grid
statusBar * widgets. TabPane
statusBar * ActivityTabPane
inputTextBox * widgets. Paragraph
inputTextBox * ModedTextInput
chatPane * widgets . List
nicklistPane * widgets . Table
input * string
userListPane * widgets . Table
events * event . Dispatcher
windows [ ] Window
mu sync . Mutex
}
func NewServer ( ev * event . Dispatcher ) ( * Server , error ) {
@ -92,26 +38,68 @@ func NewServer(ev *event.Dispatcher) (*Server, error) {
return nil , err
}
w , h := ui . TerminalDimensions ( )
return & Server {
srv := & Server {
ScreenWidth : w ,
ScreenHeight : h ,
events : ev ,
} , nil
}
srv . initUI ( )
return srv , nil
}
func ( srv * Server ) initUI ( ) {
srv . userListPane = widgets . NewTable ( )
srv . userListPane . Rows = [ ] [ ] string { }
srv . userListPane . Border = false
srv . userListPane . BorderStyle . Fg = ui . ColorBlack
srv . userListPane . RowSeparator = false
srv . userListPane . Title = "Users"
srv . userListPane . TextAlignment = ui . AlignRight
srv . userListPane . PaddingRight = 1
srv . chatPane = widgets . NewList ( )
srv . chatPane . Rows = [ ] string { }
srv . chatPane . BorderStyle . Fg = colors . DodgerBlue1
srv . chatPane . Border = true
srv . chatPane . PaddingLeft = 1
srv . chatPane . PaddingRight = 1
srv . statusBar = & ActivityTabPane {
TabPane : widgets . NewTabPane ( " 0 " ) ,
ActivityStyle : ui . NewStyle ( ui . ColorBlack , ui . ColorWhite ) ,
}
srv . statusBar . SetRect ( 0 , srv . ScreenHeight - 3 , srv . ScreenWidth , srv . ScreenHeight )
srv . statusBar . ActiveTabStyle . Fg = colors . DodgerBlue1
srv . statusBar . Border = true
srv . statusBar . BorderTop = true
srv . statusBar . BorderLeft = false
srv . statusBar . BorderRight = false
srv . statusBar . BorderBottom = false
srv . statusBar . BorderStyle . Fg = colors . DodgerBlue1
srv . inputTextBox = NewModedTextInput ( )
srv . inputTextBox . Border = false
srv . mainWindow = ui . NewGrid ( )
status := Status { }
srv . windows = [ ] Window { & status }
}
func ( srv * Server ) Close ( ) {
ui . Close ( )
}
type ScreenItem int
type screenElement int
const (
InputTextBox ScreenItem = iota
InputTextBox screenElement = iota
StatusBar
MainWindow
)
func ( srv * Server ) RenderOnly ( items ... ScreenItem ) {
func ( srv * Server ) RenderOnly ( items ... screenElement ) {
var its [ ] ui . Drawable
for _ , it := range items {
switch it {
@ -126,20 +114,51 @@ func (srv *Server) RenderOnly(items ...ScreenItem) {
ui . Render ( its ... )
}
func ( srv * Server ) Render ( ) {
func tabNames ( windows [ ] Window ) ( [ ] string , map [ int ] struct { } ) {
res := make ( [ ] string , len ( windows ) )
activity := make ( map [ int ] struct { } )
for i := 0 ; i < len ( windows ) ; i ++ {
win := windows [ i ]
if win . HasActivity ( ) {
activity [ i ] = struct { } { }
}
res [ i ] = fmt . Sprintf ( " %d " , i )
}
return res , activity
}
func ( srv * Server ) Update ( ) {
srv . mu . Lock ( )
defer srv . mu . Unlock ( )
channel := srv . windows [ srv . statusBar . ActiveTabIndex ]
if channel == 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 ( )
srv . mainWindow . Items = nil
var rows [ ] [ ] string
if v , ok := channel . ( WindowWithNicklist ) ; ok {
for _ , nick := range v . Nicklist ( ) {
if v , ok := channel . ( WindowWith UserL ist) ; ok {
for _ , nick := range v . Users ( ) {
rows = append ( rows , [ ] string { nick } )
}
srv . mainWindow . Set (
ui . NewCol ( .9 , srv . chatPane ) ,
ui . NewCol ( .1 , srv . userListPane ) ,
)
} else {
srv . mainWindow . Set (
ui . NewCol ( 1 , srv . chatPane ) ,
)
}
srv . nicklistPane . Rows = rows
srv . chatPane . Title = channel . Title ( )
srv . userListPane . Rows = rows
}
func ( srv * Server ) Render ( ) {
srv . mu . Lock ( )
defer srv . mu . Unlock ( )
ui . Render ( srv . mainWindow , srv . statusBar , srv . inputTextBox )
}
@ -156,54 +175,73 @@ func (srv *Server) handleKey(e ui.Event) {
srv . chatPane . ScrollPageDown ( )
srv . Render ( )
case "<Space>" :
srv . appendInput ( " " )
srv . inputTextBox . Append ( " " )
srv . RenderOnly ( InputTextBox )
case "<Backspace>" :
srv . backspaceInput ( )
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 c , ok := channel . ( * Channel ) ; ok {
c . lines = append ( c . lines , "<veonik> " + srv . consumeInput ( ) )
defer srv . Render ( )
if len ( in . Text ) == 0 {
return
}
switch in . Kind {
case ModeCommand :
args := strings . Split ( in . Text , " " )
cmd := args [ 0 ]
switch cmd {
case "w" :
if len ( args ) < 2 {
return
}
ch , err := strconv . Atoi ( args [ 1 ] )
if err != nil {
panic ( err )
return
}
if ch < len ( srv . statusBar . TabNames ) {
srv . statusBar . ActiveTabIndex = ch
srv . Update ( )
srv . Render ( )
}
}
case ModeMessage :
if c , ok := channel . ( * Channel ) ; ok {
c . lines = append ( c . lines , "<veonik> " + in . Text )
} else if dm , ok := channel . ( * DirectMessage ) ; ok {
dm . lines = append ( dm . lines , "<veonik> " + in . Text )
}
srv . Update ( )
srv . Render ( )
}
default :
if len ( e . ID ) != 1 {
// a single key resulted in more than one character, probably not a regular char
return
}
srv . appendInput ( e . ID )
srv . RenderOnly ( InputTextBox )
}
}
func ( srv * Server ) consumeInput ( ) string {
defer srv . resetInput ( )
if len ( * srv . input ) < 2 {
return ""
}
return ( * srv . input ) [ 2 : ]
}
func ( srv * Server ) resetInput ( ) {
* srv . input = "> "
}
func ( srv * Server ) appendInput ( in string ) {
* srv . input = * srv . input + in
}
func ( srv * Server ) backspaceInput ( ) {
if len ( srv . inputTextBox . Text ) > 2 {
* srv . input = ( * srv . input ) [ 0 : len ( * srv . input ) - 1 ]
if e . ID == "/" && srv . inputTextBox . Len ( ) == 0 {
srv . inputTextBox . ToggleMode ( )
} else {
srv . inputTextBox . Append ( e . ID )
}
srv . RenderOnly ( InputTextBox )
}
}
@ -214,71 +252,50 @@ func (srv *Server) resize() {
srv . mainWindow . SetRect ( 0 , 0 , srv . ScreenWidth , srv . ScreenHeight - srv . statusBar . Dy ( ) - srv . inputTextBox . Dy ( ) )
}
func ( srv * Server ) Start ( ) {
nicklist := widgets . NewTable ( )
nicklist . Rows = [ ] [ ] string { }
nicklist . Border = false
nicklist . BorderStyle . Fg = ui . ColorBlack
nicklist . RowSeparator = false
nicklist . Title = "Users"
nicklist . TextAlignment = ui . AlignRight
nicklist . PaddingRight = 1
chat := widgets . NewList ( )
chat . Rows = [ ] string { }
chat . BorderStyle . Fg = colors . DodgerBlue1
chat . Border = true
chat . PaddingLeft = 1
chat . PaddingRight = 1
statusbar := widgets . NewTabPane ( " 0 " , " 1 " , " 2 " )
statusbar . SetRect ( 0 , srv . ScreenHeight - 3 , srv . ScreenWidth , srv . ScreenHeight )
statusbar . ActiveTabStyle . Fg = colors . DodgerBlue1
statusbar . Border = true
statusbar . BorderTop = true
statusbar . BorderLeft = false
statusbar . BorderRight = false
statusbar . BorderBottom = false
statusbar . BorderStyle . Fg = colors . DodgerBlue1
input := widgets . NewParagraph ( )
input . Border = false
input . Text = "> "
window := ui . NewGrid ( )
window . Set (
ui . NewCol ( .9 , chat ) ,
ui . NewCol ( .1 , nicklist ) ,
)
chan0 := Channel {
name : "##somechan" ,
lines : [ ] string { "<veonik> this is a test" , "<squishyj> this is only a test" } ,
users : [ ] string { "@veonik" , "+squishyj" } ,
}
chan1 := Channel {
name : "#uwot" ,
lines : [ ] string { "<trub0> u wot m8" , "<veonik> hi" , "<trub0> u wot m8" , "<veonik> hi" , "<trub0> u wot m8" , "<veonik> hi" , "<trub0> u wot m8" , "<veonik> hi" , "<trub0> u wot m8" , "<veonik> hi" , "<trub0> u wot m8" , "<veonik> hi" , "<trub0> u wot m8" , "<veonik> hi" , "<trub0> u wot m8" , "<veonik> hi" , "<trub0> u wot m8" , "<veonik> hi" , "<trub0> u wot m8" , "<veonik> hi" , "<trub0> u wot m8" , "<veonik> hi" , "<trub0> u wot m8" , "<veonik> hi" , "<trub0> u wot m8" , "<veonik> hi" , "<trub0> u wot m8" , "<veonik> hi" } ,
users : [ ] string { "trub0" , "veonik" } ,
}
chan2 := Channel {
name : "#squishyslab" ,
lines : [ ] string { "<angrywombat> i dont think thats right" , "<veonik> you are right" } ,
users : [ ] string { "angrywombat" , "veonik" } ,
}
srv . windows = [ ] Window { & chan0 , & chan1 , & chan2 }
srv . statusBar = statusbar
srv . mainWindow = window
srv . inputTextBox = input
srv . chatPane = chat
srv . nicklistPane = nicklist
srv . input = & srv . inputTextBox . Text
func ( srv * Server ) bind ( ) {
srv . events . Bind ( "irc.JOIN" , event . HandlerFunc ( func ( ev * event . Event ) {
var win Window
target := ev . Data [ "Target" ] . ( string )
for _ , w := range srv . windows {
if w . Title ( ) == target {
win = w
break
}
}
if win == nil {
ch := & Channel { name : target , users : [ ] string { "veonik" } }
srv . windows = append ( srv . windows , ch )
}
} ) )
srv . events . Bind ( "irc.PRIVMSG" , event . HandlerFunc ( func ( ev * event . Event ) {
var win Window
target := ev . Data [ "Target" ] . ( string )
for _ , w := range srv . windows {
if w . Title ( ) == target {
win = w
break
}
}
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 ++
}
v . lines = append ( v . lines , fmt . Sprintf ( "<%s> %s" , ev . Data [ "Nick" ] , ev . Data [ "Message" ] ) )
srv . Update ( )
srv . Render ( )
}
}
} ) )
}
func ( srv * Server ) Start ( ) {
srv . bind ( )
srv . inputTextBox . Reset ( )
srv . resize ( )
srv . Update ( )
srv . Render ( )
uiEvents := ui . PollEvents ( )
@ -298,8 +315,8 @@ func (srv *Server) Start() {
}
srv . events . Emit ( "ui.MOUSE" , map [ string ] interface { } {
"kind" : e . ID ,
"x" : mouse . X ,
"y" : mouse . Y ,
"x" : mouse . X ,
"y" : mouse . Y ,
"drag" : mouse . Drag ,
} )
case ui . ResizeEvent :
@ -308,10 +325,10 @@ func (srv *Server) Start() {
panic ( fmt . Sprintf ( "received termui Resize event but Payload was unexpected type %T" , e . Payload ) )
}
srv . ScreenHeight = resize . Height
srv . ScreenWidth = resize . Width
srv . ScreenWidth = resize . Width
srv . resize ( )
srv . events . Emit ( "ui.RESIZE" , map [ string ] interface { } {
"width" : resize . Width ,
"width" : resize . Width ,
"height" : resize . Height ,
} )
srv . Render ( )