summaryrefslogblamecommitdiff
path: root/ui/ui.go
blob: 859f32a878af2e98caa84af6cce4be7730e87b36 (plain) (tree)
1
2
3
4
5
6
7
8
9


          
                 
                     
              
 

                                      
                                     

 
                    




                             
                            


                                                                      




                                     
                          

 



                                   
                     
 

                         
                           
                     
 
                         
                         
                         


                        

 



                                             





                                                      









                                          


                                                 
                               
 
                                


                                    

                            
                                               

                                      





                                                   
                 
                                

           
                                                                 
                                                
                   















                                    
                                                     
                              

 

                            
                           

 

                                
                           

 










                                   


                                   

 



                                              







                                   
                          
                                          


                            








                                            

 







                                          










                                          



                                                                



                                   



                                  

























                                                         










                                         
                              
                              

 











                                 
                                                                           




                                                   

 

                                                 
                           

 




                                                  

                                                        
 
 

                                                                    

 
                                           
                                        
                                      
                                                                         


                                                   






                                   

                                           


                                           




                           












                                                                                             



                                                            







                                                                        
                                        


                          
                                              
                          

 


                                            

 
                                 
                       


                            
                    

 



                                
                           
                   

 



                               

                           



                          

 

                         



                           

 
                                          
                             

 
                                       
                                    

 



                                           

                                                       

 
                                             
                           

 

                                 

 



                                     



                                 
                        
                                
                                                                                       


                                                                                       
                               



                                                                             
                                 
                                                                
                
                                                                
         
                                     
                        

 



                                 



                        
                                          

                                
                                 

                                                                   
                                                                                   
         
 

                                                                                 
                                                                                                      
                
                                                                                                    
         

                                                                                                                       
         

                                                                        
                
                                                                                        
         
 
                                 




                                                                                  
                                                                                             

                                                                                  
                                                                                                
         
 

                        
 















                                                  
                                               
                                             




                            
                                 


                                                                  
                                            
                                                       
              

                 
                                                                  


                                                       
 
 
                                                                                                                         





                                                    


                                                   
 



                                                
 







                                                   
                                             
                                                        
                       
                           
                                   
                                                                                                        


                                                                                    
                                                          
                                                                                                        
                                                                                        
                        
                                    
                 



                                                                    
                                                                                                                
                        
                                                                                
                                                                                                      


                                                

         
package ui

import (
	"strings"
	"sync/atomic"
	"time"

	"git.sr.ht/~taiite/senpai/irc"

	"github.com/gdamore/tcell/v2"
)

type Config struct {
	NickColWidth     int
	ChanColWidth     int
	ChanColEnabled   bool
	MemberColWidth   int
	MemberColEnabled bool
	TextMaxWidth     int
	AutoComplete     func(cursorIdx int, text []rune) []Completion
	Mouse            bool
	MergeLine        func(former *Line, addition Line)
	Colors           ConfigColors
}

type ConfigColors struct {
	Unread tcell.Color
	Nicks  ColorScheme
}

type UI struct {
	screen tcell.Screen
	Events chan tcell.Event
	exit   atomic.Value // bool
	config Config

	bs     BufferList
	e      Editor
	prompt StyledString
	status string

	channelOffset int
	memberClicked int
	memberOffset  int

	channelWidth int
	memberWidth  int
}

func New(config Config) (ui *UI, err error) {
	ui = &UI{
		config: config,
	}
	if config.ChanColEnabled {
		ui.channelWidth = config.ChanColWidth
	}
	if config.MemberColEnabled {
		ui.memberWidth = config.MemberColWidth
	}

	ui.screen, err = tcell.NewScreen()
	if err != nil {
		return
	}

	err = ui.screen.Init()
	if err != nil {
		return
	}
	if ui.screen.HasMouse() && config.Mouse {
		ui.screen.EnableMouse()
	}
	ui.screen.EnablePaste()

	_, h := ui.screen.Size()
	ui.screen.Clear()
	ui.screen.ShowCursor(0, h-2)

	ui.exit.Store(false)

	ui.Events = make(chan tcell.Event, 128)
	go func() {
		for !ui.ShouldExit() {
			ev := ui.screen.PollEvent()
			if ev == nil {
				ui.Exit()
				break
			}
			ui.Events <- ev
		}
		close(ui.Events)
	}()

	ui.bs = NewBufferList(config.Colors, ui.config.MergeLine)
	ui.e = NewEditor(ui.config.AutoComplete)
	ui.Resize()

	return
}

func (ui *UI) ShouldExit() bool {
	return ui.exit.Load().(bool)
}

func (ui *UI) Exit() {
	ui.exit.Store(true)
}

func (ui *UI) Close() {
	ui.screen.Fini()
}

func (ui *UI) CurrentBuffer() (netID, title string) {
	return ui.bs.Current()
}

func (ui *UI) NextBuffer() {
	ui.bs.Next()
	ui.memberOffset = 0
}

func (ui *UI) PreviousBuffer() {
	ui.bs.Previous()
	ui.memberOffset = 0
}

func (ui *UI) ClickedBuffer() int {
	return ui.bs.clicked
}

func (ui *UI) ClickBuffer(i int) {
	if i < len(ui.bs.list) {
		ui.bs.clicked = i
	}
}

func (ui *UI) GoToBufferNo(i int) {
	if ui.bs.To(i) {
		ui.memberOffset = 0
	}
}

func (ui *UI) ShowBufferNumbers(enable bool) {
	ui.bs.ShowBufferNumbers(enable)
}

func (ui *UI) ClickedMember() int {
	return ui.memberClicked
}

func (ui *UI) ClickMember(i int) {
	ui.memberClicked = i
}

func (ui *UI) ScrollUp() {
	ui.bs.ScrollUp(ui.bs.tlHeight / 2)
}

func (ui *UI) ScrollDown() {
	ui.bs.ScrollDown(ui.bs.tlHeight / 2)
}

func (ui *UI) ScrollUpBy(n int) {
	ui.bs.ScrollUp(n)
}

func (ui *UI) ScrollDownBy(n int) {
	ui.bs.ScrollDown(n)
}

func (ui *UI) ScrollUpHighlight() bool {
	return ui.bs.ScrollUpHighlight()
}

func (ui *UI) ScrollDownHighlight() bool {
	return ui.bs.ScrollDownHighlight()
}

func (ui *UI) ScrollChannelUpBy(n int) {
	ui.channelOffset -= n
	if ui.channelOffset < 0 {
		ui.channelOffset = 0
	}
}

func (ui *UI) ScrollChannelDownBy(n int) {
	ui.channelOffset += n
}

func (ui *UI) HorizontalBufferOffset(x int) int {
	return ui.bs.HorizontalBufferOffset(x, ui.channelOffset)
}

func (ui *UI) ChannelOffset() int {
	return ui.channelOffset
}

func (ui *UI) MemberOffset() int {
	return ui.memberOffset
}

func (ui *UI) ChannelWidth() int {
	return ui.channelWidth
}

func (ui *UI) MemberWidth() int {
	return ui.memberWidth
}

func (ui *UI) ToggleChannelList() {
	if ui.channelWidth == 0 {
		ui.channelWidth = ui.config.ChanColWidth
	} else {
		ui.channelWidth = 0
	}
	ui.Resize()
}

func (ui *UI) ToggleMemberList() {
	if ui.memberWidth == 0 {
		ui.memberWidth = ui.config.MemberColWidth
	} else {
		ui.memberWidth = 0
	}
	ui.Resize()
}

func (ui *UI) ScrollMemberUpBy(n int) {
	ui.memberOffset -= n
	if ui.memberOffset < 0 {
		ui.memberOffset = 0
	}
}

func (ui *UI) ScrollMemberDownBy(n int) {
	ui.memberOffset += n
}

func (ui *UI) IsAtTop() bool {
	return ui.bs.IsAtTop()
}

func (ui *UI) OpenOverlay() {
	ui.bs.OpenOverlay()
}

func (ui *UI) CloseOverlay() {
	ui.bs.CloseOverlay()
}

func (ui *UI) HasOverlay() bool {
	return ui.bs.HasOverlay()
}

func (ui *UI) AddBuffer(netID, netName, title string) (i int, added bool) {
	i, added = ui.bs.Add(netID, netName, title)
	if added {
		ui.HorizontalBufferScrollTo()
	}
	return
}

func (ui *UI) RemoveBuffer(netID, title string) {
	_ = ui.bs.Remove(netID, title)
	ui.memberOffset = 0
}

func (ui *UI) RemoveNetworkBuffers(netID string) {
	ui.bs.RemoveNetwork(netID)
	ui.memberOffset = 0
}

func (ui *UI) AddLine(netID, buffer string, line Line) {
	ui.bs.AddLine(netID, buffer, line)
}

func (ui *UI) AddLines(netID, buffer string, before, after []Line) {
	ui.bs.AddLines(netID, buffer, before, after)
}

func (ui *UI) JumpBuffer(sub string) bool {
	subLower := strings.ToLower(sub)
	for i, b := range ui.bs.list {
		if strings.Contains(strings.ToLower(b.title), subLower) {
			if ui.bs.To(i) {
				ui.memberOffset = 0
			}
			return true
		}
	}

	return false
}

func (ui *UI) JumpBufferIndex(i int) bool {
	if i >= 0 && i < len(ui.bs.list) {
		if ui.bs.To(i) {
			ui.memberOffset = 0
		}
		return true
	}
	return false
}

func (ui *UI) JumpBufferNetwork(netID, sub string) bool {
	subLower := strings.ToLower(sub)
	for i, b := range ui.bs.list {
		if b.netID == netID && strings.Contains(strings.ToLower(b.title), subLower) {
			if ui.bs.To(i) {
				ui.memberOffset = 0
			}
			return true
		}
	}
	return false
}

func (ui *UI) SetTopic(netID, buffer string, topic string) {
	ui.bs.SetTopic(netID, buffer, topic)
}

func (ui *UI) SetRead(netID, buffer string, timestamp time.Time) {
	ui.bs.SetRead(netID, buffer, timestamp)
}

func (ui *UI) UpdateRead() (netID, buffer string, timestamp time.Time) {
	return ui.bs.UpdateRead()
}

func (ui *UI) SetStatus(status string) {
	ui.status = status
}

func (ui *UI) SetPrompt(prompt StyledString) {
	ui.prompt = prompt
}

// InputContent result must not be modified.
func (ui *UI) InputContent() []rune {
	return ui.e.Content()
}

func (ui *UI) InputRune(r rune) {
	ui.e.PutRune(r)
}

func (ui *UI) InputRight() {
	ui.e.Right()
}

func (ui *UI) InputRightWord() {
	ui.e.RightWord()
}

func (ui *UI) InputLeft() {
	ui.e.Left()
}

func (ui *UI) InputLeftWord() {
	ui.e.LeftWord()
}

func (ui *UI) InputHome() {
	ui.e.Home()
}

func (ui *UI) InputEnd() {
	ui.e.End()
}

func (ui *UI) InputUp() {
	ui.e.Up()
}

func (ui *UI) InputDown() {
	ui.e.Down()
}

func (ui *UI) InputBackspace() (ok bool) {
	return ui.e.RemRune()
}

func (ui *UI) InputDelete() (ok bool) {
	return ui.e.RemRuneForward()
}

func (ui *UI) InputDeleteWord() (ok bool) {
	return ui.e.RemWord()
}

func (ui *UI) InputAutoComplete(offset int) (ok bool) {
	return ui.e.AutoComplete(offset)
}

func (ui *UI) InputEnter() (content string) {
	return ui.e.Flush()
}

func (ui *UI) InputClear() bool {
	return ui.e.Clear()
}

func (ui *UI) InputSet(text string) {
	ui.e.Set(text)
}

func (ui *UI) InputBackSearch() {
	ui.e.BackSearch()
}

func (ui *UI) Resize() {
	w, h := ui.screen.Size()
	innerWidth := w - 9 - ui.channelWidth - ui.config.NickColWidth - ui.memberWidth
	if innerWidth <= 0 {
		innerWidth = 1 // will break display somewhat, but this is an edge case
	}
	ui.e.Resize(innerWidth)
	textWidth := innerWidth
	if ui.config.TextMaxWidth > 0 && ui.config.TextMaxWidth < textWidth {
		textWidth = ui.config.TextMaxWidth
	}
	if ui.channelWidth == 0 {
		ui.bs.ResizeTimeline(innerWidth, h-3, textWidth)
	} else {
		ui.bs.ResizeTimeline(innerWidth, h-2, textWidth)
	}
	ui.HorizontalBufferScrollTo()
	ui.screen.Sync()
}

func (ui *UI) Size() (int, int) {
	return ui.screen.Size()
}

func (ui *UI) Beep() {
	ui.screen.Beep()
}

func (ui *UI) Draw(members []irc.Member) {
	w, h := ui.screen.Size()

	if ui.channelWidth == 0 {
		ui.e.Draw(ui.screen, 9+ui.config.NickColWidth, h-2)
	} else {
		ui.e.Draw(ui.screen, 9+ui.channelWidth+ui.config.NickColWidth, h-1)
	}

	ui.bs.DrawTimeline(ui.screen, ui.channelWidth, 0, ui.config.NickColWidth)
	if ui.channelWidth == 0 {
		ui.bs.DrawHorizontalBufferList(ui.screen, 0, h-1, w-ui.memberWidth, &ui.channelOffset)
	} else {
		ui.bs.DrawVerticalBufferList(ui.screen, 0, 0, ui.channelWidth, h, &ui.channelOffset)
	}
	if ui.memberWidth != 0 {
		ui.drawVerticalMemberList(ui.screen, w-ui.memberWidth, 0, ui.memberWidth, h, members, &ui.memberOffset)
	}
	if ui.channelWidth == 0 {
		ui.drawStatusBar(ui.channelWidth, h-3, w-ui.memberWidth)
	} else {
		ui.drawStatusBar(ui.channelWidth, h-2, w-ui.channelWidth-ui.memberWidth)
	}

	if ui.channelWidth == 0 {
		for x := 0; x < 9+ui.config.NickColWidth; x++ {
			ui.screen.SetContent(x, h-2, ' ', nil, tcell.StyleDefault)
		}
		printIdent(ui.screen, 7, h-2, ui.config.NickColWidth, ui.prompt)
	} else {
		for x := ui.channelWidth; x < 9+ui.channelWidth+ui.config.NickColWidth; x++ {
			ui.screen.SetContent(x, h-1, ' ', nil, tcell.StyleDefault)
		}
		printIdent(ui.screen, ui.channelWidth+7, h-1, ui.config.NickColWidth, ui.prompt)
	}

	ui.screen.Show()
}

func (ui *UI) HorizontalBufferScrollTo() {
	w, _ := ui.screen.Size()
	screenWidth := w - ui.memberWidth
	if ui.bs.current < ui.channelOffset {
		ui.channelOffset = ui.bs.current
		return
	}

	leftMost := ui.bs.GetLeftMost(screenWidth)

	if ui.channelOffset >= leftMost {
		return
	}
	ui.channelOffset = leftMost
}

func (ui *UI) drawStatusBar(x0, y, width int) {
	clearArea(ui.screen, x0, y, width, 1)

	if ui.status == "" {
		return
	}

	var s StyledStringBuilder
	s.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
	s.WriteString("--")

	x := x0 + 5 + ui.config.NickColWidth
	printString(ui.screen, &x, y, s.StyledString())
	x += 2

	s.Reset()
	s.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
	s.WriteString(ui.status)

	printString(ui.screen, &x, y, s.StyledString())
}

func (ui *UI) drawVerticalMemberList(screen tcell.Screen, x0, y0, width, height int, members []irc.Member, offset *int) {
	if y0+len(members)-*offset < height {
		*offset = y0 + len(members) - height
		if *offset < 0 {
			*offset = 0
		}
	}
	if ui.memberClicked >= len(members) {
		ui.memberClicked = len(members) - 1
	}

	drawVerticalLine(screen, x0, y0, height)
	x0++
	width--
	clearArea(screen, x0, y0, width, height)

	padding := 1
	for _, m := range members {
		if m.Disconnected {
			padding = runeWidth(0x274C)
			break
		}
	}

	for i, m := range members[*offset:] {
		reverse := i+*offset == ui.memberClicked
		x := x0
		y := y0 + i
		if m.Disconnected {
			disconnectedSt := tcell.StyleDefault.Foreground(tcell.ColorRed).Reverse(reverse)
			printString(screen, &x, y, Styled("\u274C", disconnectedSt))
		} else if m.PowerLevel != "" {
			x += padding - 1
			powerLevelText := m.PowerLevel[:1]
			powerLevelSt := tcell.StyleDefault.Foreground(tcell.ColorGreen).Reverse(reverse)
			printString(screen, &x, y, Styled(powerLevelText, powerLevelSt))
		} else {
			x += padding
		}

		var name StyledString
		nameText := truncate(m.Name.Name, width-1, "\u2026")
		if m.Away {
			name = Styled(nameText, tcell.StyleDefault.Foreground(tcell.ColorGray).Reverse(reverse))
		} else {
			color := IdentColor(ui.config.Colors.Nicks, m.Name.Name)
			name = Styled(nameText, tcell.StyleDefault.Foreground(color).Reverse(reverse))
		}

		printString(screen, &x, y, name)
	}
}