Browse Source

Move widgets into subpackage, consistency improvements

master
Tyler Sommer 7 months ago
parent
commit
304b6e2301
Signed by: tyler-sommer GPG Key ID: C09C010500DBD008
10 changed files with 434 additions and 410 deletions
  1. +7
    -19
      Makefile
  2. +5
    -3
      handlers.go
  3. +24
    -23
      server.go
  4. +0
    -309
      widget.go
  5. +175
    -0
      widget/chatpane.go
  6. +89
    -0
      widget/irc_style.go
  7. +68
    -0
      widget/statusbar.go
  8. +49
    -36
      widget/text_input.go
  9. +11
    -11
      widget/userlist.go
  10. +6
    -9
      window_manager.go

+ 7
- 19
Makefile View File

@ -15,48 +15,36 @@ SQUIRSSI_TARGET := $(OUTPUT_BASE)/squirssi
RACE ?= -race
TEST_ARGS ?= -count 1
TESTDATA_NODEMODS_TARGET := testdata/node_modules
.PHONY: all build generate run squirssi plugins clean
all: build
all: build plugins
clean:
rm -rf plugins && cp -r $(SQUIRCY3_ROOT)/plugins .
rm -rf $(OUTPUT_BASE)
build: squirssi
generate: $(OUTPUT_BASE)/.generated
squirssi: $(SQUIRSSI_TARGET)
plugins: $(PLUGIN_TARGETS)
run: build
$(SQUIRSSI_TARGET) 2>> squirssi_errors.log
$(SQUIRSSI_TARGET) 2>> $(OUTPUT_BASE)/squirssi_errors.log
test: $(TESTDATA_NODEMODS_TARGET)
test:
go test -tags netgo $(RACE) $(TEST_ARGS) ./...
$(TESTDATA_NODEMODS_TARGET):
cd testdata && \
yarn install
$(OUTPUT_BASE)/plugins: $(OUTPUT_BASE)
cp -r $(SQUIRCY3_ROOT)/plugins $(OUTPUT_BASE)/plugins
.SECONDEXPANSION:
$(PLUGIN_TARGETS): $(OUTPUT_BASE)/%.so: $$(wildcard plugins/%/*) $(SOURCES)
go build -tags netgo $(RACE) -o $@ -buildmode=plugin plugins/$*/*.go
$(PLUGIN_TARGETS): $(OUTPUT_BASE)/%.so: $$(wildcard plugins/%/*) $(OUTPUT_BASE)/plugins $(SOURCES)
go build -tags netgo $(RACE) -o $@ -buildmode=plugin $(OUTPUT_BASE)/plugins/$*/*.go
$(SQUIRSSI_TARGET): $(SOURCES)
go build -tags netgo $(RACE) -o $@ cmd/squirssi/*.go
$(OUTPUT_BASE)/.generated: $(GENERATOR_SOURCES)
go generate
touch $@
$(OUTPUT_BASE):
mkdir -p $(OUTPUT_BASE)
$(SOURCES): $(OUTPUT_BASE)
$(GENERATOR_SOURCES): $(OUTPUT_BASE)

+ 5
- 3
handlers.go View File

@ -6,6 +6,8 @@ import (
"code.dopame.me/veonik/squircy3/event"
"github.com/sirupsen/logrus"
"code.dopame.me/veonik/squirssi/widget"
)
func bindUIHandlers(srv *Server, events *event.Dispatcher) {
@ -109,7 +111,7 @@ func onUIKeyPress(srv *Server, key string) {
} else {
tabbed = srv.tabber.Reset(srv.inputTextBox.Peek(), ch)
}
srv.inputTextBox.Set(ModedText{Kind: srv.inputTextBox.Mode(), Text: tabbed})
srv.inputTextBox.Set(widget.ModedText{Kind: srv.inputTextBox.Mode(), Text: tabbed})
}
srv.RenderOnly(InputTextBox)
case "<Enter>":
@ -126,7 +128,7 @@ func onUIKeyPress(srv *Server, key string) {
}
defer srv.HistoryManager.Append(channel, in)
switch in.Kind {
case ModeCommand:
case widget.ModeCommand:
args := strings.Split(in.Text, " ")
c := args[0]
if cmd, ok := builtIns[c]; ok {
@ -134,7 +136,7 @@ func onUIKeyPress(srv *Server, key string) {
} else {
logrus.Warnln("no command named:", c)
}
case ModeMessage:
case widget.ModeMessage:
srv.RenderOnly(InputTextBox)
if active == 0 {
// status window doesn't accept messages


+ 24
- 23
server.go View File

@ -13,6 +13,7 @@ import (
"github.com/sirupsen/logrus"
"code.dopame.me/veonik/squirssi/colors"
"code.dopame.me/veonik/squirssi/widget"
)
type logFormatter struct{}
@ -21,11 +22,11 @@ func (f *logFormatter) Format(entry *logrus.Entry) ([]byte, error) {
lvl := ""
switch entry.Level {
case logrus.InfoLevel:
lvl = "[INFO ](fg:blue)"
lvl = "[ INFO](fg:blue)"
case logrus.DebugLevel:
lvl = "[DEBUG](fg:white,bg:blue)"
case logrus.WarnLevel:
lvl = "[WARN ](fg:yellow)"
lvl = "[ WARN](fg:yellow)"
case logrus.ErrorLevel:
lvl = "[ERROR](fg:red)"
case logrus.FatalLevel:
@ -39,7 +40,7 @@ func (f *logFormatter) Format(entry *logrus.Entry) ([]byte, error) {
}
type HistoryManager struct {
histories map[Window][]ModedText
histories map[Window][]widget.ModedText
cursors map[Window]int
mu sync.Mutex
@ -47,12 +48,12 @@ type HistoryManager struct {
func NewHistoryManager() *HistoryManager {
return &HistoryManager{
histories: make(map[Window][]ModedText),
histories: make(map[Window][]widget.ModedText),
cursors: make(map[Window]int),
}
}
func (hm *HistoryManager) Append(win Window, input ModedText) {
func (hm *HistoryManager) Append(win Window, input widget.ModedText) {
hm.mu.Lock()
defer hm.mu.Unlock()
hm.cursors[win] = len(hm.histories[win])
@ -60,7 +61,7 @@ func (hm *HistoryManager) Append(win Window, input ModedText) {
hm.cursors[win] = len(hm.histories[win])
}
func (hm *HistoryManager) Insert(win Window, input ModedText) {
func (hm *HistoryManager) Insert(win Window, input widget.ModedText) {
hm.mu.Lock()
defer hm.mu.Unlock()
if hm.current(win) == input {
@ -69,28 +70,28 @@ func (hm *HistoryManager) Insert(win Window, input ModedText) {
hm.append(win, input)
}
func (hm *HistoryManager) append(win Window, input ModedText) {
hm.histories[win] = append(append(append([]ModedText{}, hm.histories[win][:hm.cursors[win]]...), input), hm.histories[win][hm.cursors[win]:]...)
func (hm *HistoryManager) append(win Window, input widget.ModedText) {
hm.histories[win] = append(append(append([]widget.ModedText{}, hm.histories[win][:hm.cursors[win]]...), input), hm.histories[win][hm.cursors[win]:]...)
}
func (hm *HistoryManager) current(win Window) ModedText {
func (hm *HistoryManager) current(win Window) widget.ModedText {
if hm.cursors[win] < 0 {
hm.cursors[win] = 0
}
if hm.cursors[win] >= len(hm.histories[win]) {
hm.cursors[win] = len(hm.histories[win])
return ModedText{}
return widget.ModedText{}
}
return hm.histories[win][hm.cursors[win]]
}
func (hm *HistoryManager) Current(win Window) ModedText {
func (hm *HistoryManager) Current(win Window) widget.ModedText {
hm.mu.Lock()
defer hm.mu.Unlock()
return hm.current(win)
}
func (hm *HistoryManager) Previous(win Window) ModedText {
func (hm *HistoryManager) Previous(win Window) widget.ModedText {
hm.mu.Lock()
defer hm.mu.Unlock()
hm.cursors[win] -= 1
@ -98,7 +99,7 @@ func (hm *HistoryManager) Previous(win Window) ModedText {
return res
}
func (hm *HistoryManager) Next(win Window) ModedText {
func (hm *HistoryManager) Next(win Window) widget.ModedText {
hm.mu.Lock()
defer hm.mu.Unlock()
hm.cursors[win] += 1
@ -179,11 +180,11 @@ type Server struct {
pageWidth, pageHeight int
mainWindow *ui.Grid
statusBar *ActivityTabPane
statusBar *widget.StatusBarPane
inputTextBox *ModedTextInput
chatPane *ChatPane
userListPane *UserList
inputTextBox *widget.ModedTextInput
chatPane *widget.ChatPane
userListPane *widget.UserList
tabber *Tabber
@ -216,7 +217,7 @@ func NewServer(ev *event.Dispatcher, irc *irc.Manager) (*Server, error) {
srv.initUI()
srv.HistoryManager = NewHistoryManager()
srv.WindowManager = NewWindowManager(ev)
srv.Logger.SetOutput(srv.WindowManager.status)
srv.Logger.SetOutput(srv.WindowManager.Index(0))
srv.Logger.SetFormatter(&logFormatter{})
return srv, nil
}
@ -247,7 +248,7 @@ func (srv *Server) initUI() {
ui.StyleParserColorMap["gray100"] = colors.Grey100
ui.StyleParserColorMap["grey100"] = colors.Grey100
srv.userListPane = NewUserList()
srv.userListPane = widget.NewUserList()
srv.userListPane.Rows = []string{}
srv.userListPane.Border = true
srv.userListPane.BorderRight = false
@ -259,7 +260,7 @@ func (srv *Server) initUI() {
srv.userListPane.PaddingRight = 0
srv.userListPane.TitleStyle.Fg = colors.Grey100
srv.chatPane = NewChatPane()
srv.chatPane = widget.NewChatPane()
srv.chatPane.Rows = []string{}
srv.chatPane.BorderStyle.Fg = colors.DodgerBlue1
srv.chatPane.Border = true
@ -270,7 +271,7 @@ func (srv *Server) initUI() {
srv.chatPane.SubTitleStyle.Fg = colors.White
srv.chatPane.ModeStyle.Fg = colors.Grey42
srv.statusBar = NewActivityTabPane()
srv.statusBar = widget.NewStatusBarPane()
srv.statusBar.ActiveTabStyle.Fg = colors.DodgerBlue1
srv.statusBar.NoticeStyle = ui.NewStyle(colors.White, colors.DodgerBlue1)
srv.statusBar.ActivityStyle = ui.NewStyle(ui.ColorBlack, ui.ColorWhite)
@ -281,7 +282,7 @@ func (srv *Server) initUI() {
srv.statusBar.BorderBottom = false
srv.statusBar.BorderStyle.Fg = colors.DodgerBlue1
srv.inputTextBox = NewModedTextInput(CursorFullBlock)
srv.inputTextBox = widget.NewModedTextInput()
srv.inputTextBox.Border = false
srv.mainWindow = ui.NewGrid()
@ -311,7 +312,7 @@ func (srv *Server) Update() {
return
}
win.Touch()
srv.statusBar.TabNames, srv.statusBar.TabsWithActivity = srv.WindowManager.tabNames()
srv.statusBar.TabNames, srv.statusBar.TabsWithActivity = srv.WindowManager.TabNames()
srv.chatPane.SelectedRow = win.CurrentLine()
srv.chatPane.Rows = win.Lines()
srv.chatPane.Title = win.Title()


+ 0
- 309
widget.go View File

@ -1,309 +0,0 @@
package squirssi
import (
"image"
"strconv"
"strings"
"unicode"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
rw "github.com/mattn/go-runewidth"
"github.com/mitchellh/go-wordwrap"
"code.dopame.me/veonik/squirssi/colors"
)
// A ChatPane contains the messages for the screen.
// This widget is based on the termui List widget.
// ChatPanes support both termui formatting as well as IRC formatting.
type ChatPane struct {
ui.Block
Rows []string
WrapText bool
TextStyle ui.Style
SelectedRow int
LeftPadding int
ModeText string
ModeStyle ui.Style
SubTitle string
SubTitleStyle ui.Style
}
func NewChatPane() *ChatPane {
return &ChatPane{
Block: *ui.NewBlock(),
TextStyle: ui.Theme.List.Text,
}
}
func ParseIRCStyles(c []ui.Cell) []ui.Cell {
var style ui.Style
var initial ui.Style
var cells []ui.Cell
changed := false
for i := 0; i < len(c); i++ {
cc := c[i]
if !changed {
initial = cc.Style
style = cc.Style
} else {
cc.Style = style
}
switch cc.Rune {
case 0x1E:
// strikethrough, not supported
case 0x1F:
changed = true
style.Modifier ^= ui.ModifierUnderline
case 0x1D:
// italics, not supported
case 0x02:
changed = true
style.Modifier ^= ui.ModifierBold
case 0x016:
changed = true
style.Modifier ^= ui.ModifierReverse
case 0x0F:
changed = false
style = initial
case 0x04:
// hex color, not supported
case 0x03:
// color
changed = true
fgdone := false
fg := ""
bg := ""
eat := 0
for j := i + 1; j-i < 5 && j < len(c); j++ {
cx := c[j]
if unicode.IsDigit(cx.Rune) {
eat++
if !fgdone {
fg = fg + string(cx.Rune)
} else {
bg = bg + string(cx.Rune)
}
} else if cx.Rune == ',' {
if fg == "" {
break
}
eat++
fgdone = true
} else {
break
}
}
i += eat
if fg == "" && bg == "" {
style.Fg = initial.Fg
style.Bg = initial.Bg
} else {
fgi, _ := strconv.Atoi(fg)
style.Fg = colors.IRCToUI(colors.IRC(fgi))
if bg != "" {
bgi, _ := strconv.Atoi(bg)
style.Bg = colors.IRCToUI(colors.IRC(bgi))
}
}
continue
default:
cells = append(cells, cc)
}
}
return cells
}
func WrapCellsPadded(cells []ui.Cell, width uint, leftPadding int) []ui.Cell {
str := ui.CellsToString(cells)
wrapped := wordwrap.WrapString(str, width)
wrappedCells := []ui.Cell{}
i := 0
twoLines := false
printPipe := false
loop:
for x, _rune := range wrapped {
if _rune == '│' {
printPipe = true
}
if _rune == '\n' {
wrappedCells = append(wrappedCells, ui.Cell{_rune, ui.StyleClear})
for j := 0; j < leftPadding; j++ {
wrappedCells = append(wrappedCells, ui.Cell{' ', ui.StyleClear})
}
if printPipe {
wrappedCells = append(wrappedCells, ui.Cell{ui.VERTICAL_LINE, ui.NewStyle(colors.Grey35)})
}
wrappedCells = append(wrappedCells, ui.Cell{' ', ui.StyleClear})
if !twoLines {
// the first time we wrap, we use the full available width, but the
// next lines are padded before they starts so that the text lines
// up on all lines with the nick and timestamp in a "gutter".
// so after wrapping the first time, recalculate the wrapping using the
// padded width. this only needs to happen once.
lPad := uint(leftPadding)
if printPipe {
lPad++
}
wrapped = wordwrap.WrapString(strings.ReplaceAll(wrapped[x+1:], "\n", " "), width-lPad)
twoLines = true
goto loop
}
} else {
wrappedCells = append(wrappedCells, ui.Cell{_rune, cells[i].Style})
}
i++
}
return wrappedCells
}
func (self *ChatPane) Draw(buf *ui.Buffer) {
self.Block.Draw(buf)
tcells := ui.ParseStyles(self.Title, self.TitleStyle)
if self.ModeText != "" {
tcells = append(tcells, ui.Cell{'(', self.TitleStyle})
tcells = append(tcells, ui.RunesToStyledCells([]rune(self.ModeText), self.ModeStyle)...)
tcells = append(tcells, ui.Cell{')', self.TitleStyle})
}
if self.SubTitle != "" {
tcells = append(tcells, ui.RunesToStyledCells([]rune{ui.HORIZONTAL_LINE, ui.HORIZONTAL_LINE}, ui.NewStyle(colors.Grey42))...)
tcells = append(tcells, ui.ParseStyles(self.SubTitle, self.SubTitleStyle)...)
}
pt := image.Pt(self.Min.X+2, self.Min.Y)
if self.Max.X > 0 && len(tcells) >= self.Max.X-5 {
tcells = append(tcells[:self.Max.X-5], ui.Cell{ui.ELLIPSES, self.SubTitleStyle})
}
for i := 0; i < len(tcells); i++ {
cc := tcells[i]
buf.SetCell(cc, pt)
pt.X++
}
point := self.Inner.Min
rows := make([]int, len(self.Rows))
actuals := [][]ui.Cell{}
actualLen := 0
for i, o := range self.Rows {
c := ui.ParseStyles(o, self.TextStyle)
c = ParseIRCStyles(c)
if self.WrapText {
c = WrapCellsPadded(c, uint(self.Inner.Dx()-1), self.LeftPadding)
}
p := ui.SplitCells(c, '\n')
l := len(p)
e := actualLen + l
actualLen = e
rows[i] = e - 1
for j := 0; j < l; j++ {
actuals = append(actuals, p[j])
}
}
// row that would actually be selected after text wrapping is done
actualSelected := rows[self.SelectedRow]
topRow := 0
// adjust starting row based on the bounding box and the actual selected row
if actualSelected >= self.Inner.Dy()+topRow {
topRow = actualSelected - self.Inner.Dy() + 1
} else if actualSelected < topRow {
topRow = actualSelected
}
// draw the already wrapped rows
for row := topRow; row < len(actuals) && point.Y < self.Inner.Max.Y; row++ {
cells := actuals[row]
for j := 0; j < len(cells) && point.Y < self.Inner.Max.Y; j++ {
style := cells[j].Style
if cells[j].Rune == '\n' {
point = image.Pt(self.Inner.Min.X, point.Y+1)
} else {
if point.X+1 == self.Inner.Max.X+1 && len(cells) > self.Inner.Dx() {
buf.SetCell(ui.NewCell(ui.ELLIPSES, style), point.Add(image.Pt(-1, 0)))
break
} else {
buf.SetCell(ui.NewCell(cells[j].Rune, style), point)
point = point.Add(image.Pt(rw.RuneWidth(cells[j].Rune), 0))
}
}
}
point = image.Pt(self.Inner.Min.X, point.Y+1)
}
// draw UP_ARROW if needed
if topRow > 0 {
buf.SetCell(
ui.NewCell(ui.UP_ARROW, ui.NewStyle(ui.ColorWhite)),
image.Pt(self.Inner.Max.X-1, self.Inner.Min.Y),
)
}
// draw DOWN_ARROW if needed
if len(self.Rows) > topRow+self.Inner.Dy() {
buf.SetCell(
ui.NewCell(ui.DOWN_ARROW, ui.NewStyle(ui.ColorWhite)),
image.Pt(self.Inner.Max.X-1, self.Inner.Max.Y-1),
)
}
}
// ActivityTabPane contains the tabs for available windows.
// This widget compounds a termui TabPane widget with highlighting of tabs
// in two additional ways: notice and activity. Notice is intended for when
// a tab wants extra attention (ie. user was mentioned) vs activity where
// there are just some new lines since last touched.
type ActivityTabPane struct {
*widgets.TabPane
TabsWithActivity map[int]activityType
NoticeStyle ui.Style
ActivityStyle ui.Style
}
func NewActivityTabPane() *ActivityTabPane {
return &ActivityTabPane{
TabPane: widgets.NewTabPane(" 0 "),
}
}
func (self *ActivityTabPane) Draw(buf *ui.Buffer) {
self.Block.Draw(buf)
xCoordinate := self.Inner.Min.X
for i, name := range self.TabNames {
ColorPair := self.InactiveTabStyle
if k, ok := self.TabsWithActivity[i]; ok {
switch k {
case TabHasActivity:
ColorPair = self.ActivityStyle
case TabHasNotice:
ColorPair = self.NoticeStyle
}
}
if i == self.ActiveTabIndex {
ColorPair = self.ActiveTabStyle
}
buf.SetString(
ui.TrimString(name, self.Inner.Max.X-xCoordinate),
ColorPair,
image.Pt(xCoordinate, self.Inner.Min.Y),
)
xCoordinate += 1 + len(name)
if i < len(self.TabNames)-1 && xCoordinate < self.Inner.Max.X {
buf.SetCell(
ui.NewCell(ui.VERTICAL_LINE, ui.NewStyle(ui.ColorWhite)),
image.Pt(xCoordinate, self.Inner.Min.Y),
)
}
xCoordinate += 2
}
}

+ 175
- 0
widget/chatpane.go View File

@ -0,0 +1,175 @@
package widget
import (
"image"
"strings"
ui "github.com/gizak/termui/v3"
rw "github.com/mattn/go-runewidth"
"github.com/mitchellh/go-wordwrap"
"code.dopame.me/veonik/squirssi/colors"
)
// A ChatPane contains the messages for the screen.
// This widget is based on the termui List widget.
// ChatPanes support both termui formatting as well as IRC formatting.
type ChatPane struct {
ui.Block
Rows []string
WrapText bool
TextStyle ui.Style
SelectedRow int
LeftPadding int
ModeText string
ModeStyle ui.Style
SubTitle string
SubTitleStyle ui.Style
}
func NewChatPane() *ChatPane {
return &ChatPane{
Block: *ui.NewBlock(),
TextStyle: ui.Theme.List.Text,
}
}
func WrapCellsPadded(cells []ui.Cell, width uint, leftPadding int) []ui.Cell {
str := ui.CellsToString(cells)
wrapped := wordwrap.WrapString(str, width)
wrappedCells := []ui.Cell{}
i := 0
twoLines := false
printPipe := false
loop:
for x, _rune := range wrapped {
if _rune == '│' {
printPipe = true
}
if _rune == '\n' {
wrappedCells = append(wrappedCells, ui.Cell{_rune, ui.StyleClear})
for j := 0; j < leftPadding; j++ {
wrappedCells = append(wrappedCells, ui.Cell{' ', ui.StyleClear})
}
if printPipe {
wrappedCells = append(wrappedCells, ui.Cell{ui.VERTICAL_LINE, ui.NewStyle(colors.Grey35)})
}
wrappedCells = append(wrappedCells, ui.Cell{' ', ui.StyleClear})
if !twoLines {
// the first time we wrap, we use the full available width, but the
// next lines are padded before they starts so that the text lines
// up on all lines with the nick and timestamp in a "gutter".
// so after wrapping the first time, recalculate the wrapping using the
// padded width. this only needs to happen once.
lPad := uint(leftPadding)
if printPipe {
lPad++
}
wrapped = wordwrap.WrapString(strings.ReplaceAll(wrapped[x+1:], "\n", " "), width-lPad)
twoLines = true
goto loop
}
} else {
wrappedCells = append(wrappedCells, ui.Cell{_rune, cells[i].Style})
}
i++
}
return wrappedCells
}
func (cp *ChatPane) Draw(buf *ui.Buffer) {
cp.Block.Draw(buf)
tcells := ui.ParseStyles(cp.Title, cp.TitleStyle)
if cp.ModeText != "" {
tcells = append(tcells, ui.Cell{'(', cp.TitleStyle})
tcells = append(tcells, ui.RunesToStyledCells([]rune(cp.ModeText), cp.ModeStyle)...)
tcells = append(tcells, ui.Cell{')', cp.TitleStyle})
}
if cp.SubTitle != "" {
tcells = append(tcells, ui.RunesToStyledCells([]rune{ui.HORIZONTAL_LINE, ui.HORIZONTAL_LINE}, ui.NewStyle(colors.Grey42))...)
tcells = append(tcells, ui.ParseStyles(cp.SubTitle, cp.SubTitleStyle)...)
}
pt := image.Pt(cp.Min.X+2, cp.Min.Y)
if cp.Max.X > 0 && len(tcells) >= cp.Max.X-5 {
tcells = append(tcells[:cp.Max.X-5], ui.Cell{ui.ELLIPSES, cp.SubTitleStyle})
}
for i := 0; i < len(tcells); i++ {
cc := tcells[i]
buf.SetCell(cc, pt)
pt.X++
}
point := cp.Inner.Min
rows := make([]int, len(cp.Rows))
actuals := [][]ui.Cell{}
actualLen := 0
for i, o := range cp.Rows {
c := ui.ParseStyles(o, cp.TextStyle)
c = ParseIRCStyles(c)
if cp.WrapText {
c = WrapCellsPadded(c, uint(cp.Inner.Dx()-1), cp.LeftPadding)
}
p := ui.SplitCells(c, '\n')
l := len(p)
e := actualLen + l
actualLen = e
rows[i] = e - 1
for j := 0; j < l; j++ {
actuals = append(actuals, p[j])
}
}
// row that would actually be selected after text wrapping is done
actualSelected := 0
if len(rows) > cp.SelectedRow {
actualSelected = rows[cp.SelectedRow]
}
topRow := 0
// adjust starting row based on the bounding box and the actual selected row
if actualSelected >= cp.Inner.Dy()+topRow {
topRow = actualSelected - cp.Inner.Dy() + 1
} else if actualSelected < topRow {
topRow = actualSelected
}
// draw the already wrapped rows
for row := topRow; row < len(actuals) && point.Y < cp.Inner.Max.Y; row++ {
cells := actuals[row]
for j := 0; j < len(cells) && point.Y < cp.Inner.Max.Y; j++ {
style := cells[j].Style
if cells[j].Rune == '\n' {
point = image.Pt(cp.Inner.Min.X, point.Y+1)
} else {
if point.X+1 == cp.Inner.Max.X+1 && len(cells) > cp.Inner.Dx() {
buf.SetCell(ui.NewCell(ui.ELLIPSES, style), point.Add(image.Pt(-1, 0)))
break
} else {
buf.SetCell(ui.NewCell(cells[j].Rune, style), point)
point = point.Add(image.Pt(rw.RuneWidth(cells[j].Rune), 0))
}
}
}
point = image.Pt(cp.Inner.Min.X, point.Y+1)
}
// draw UP_ARROW if needed
if topRow > 0 {
buf.SetCell(
ui.NewCell(ui.UP_ARROW, ui.NewStyle(ui.ColorWhite)),
image.Pt(cp.Inner.Max.X-1, cp.Inner.Min.Y),
)
}
// draw DOWN_ARROW if needed
if len(cp.Rows) > topRow+cp.Inner.Dy() {
buf.SetCell(
ui.NewCell(ui.DOWN_ARROW, ui.NewStyle(ui.ColorWhite)),
image.Pt(cp.Inner.Max.X-1, cp.Inner.Max.Y-1),
)
}
}

+ 89
- 0
widget/irc_style.go View File

@ -0,0 +1,89 @@
package widget
import (
"strconv"
"unicode"
ui "github.com/gizak/termui/v3"
"code.dopame.me/veonik/squirssi/colors"
)
func ParseIRCStyles(c []ui.Cell) []ui.Cell {
var style ui.Style
var initial ui.Style
var cells []ui.Cell
changed := false
for i := 0; i < len(c); i++ {
cc := c[i]
if !changed {
initial = cc.Style
style = cc.Style
} else {
cc.Style = style
}
switch cc.Rune {
case 0x1E:
// strikethrough, not supported
case 0x1F:
changed = true
style.Modifier ^= ui.ModifierUnderline
case 0x1D:
// italics, not supported
case 0x02:
changed = true
style.Modifier ^= ui.ModifierBold
case 0x016:
changed = true
style.Modifier ^= ui.ModifierReverse
case 0x0F:
changed = false
style = initial
case 0x04:
// hex color, not supported
case 0x03:
// color
changed = true
fgdone := false
fg := ""
bg := ""
eat := 0
for j := i + 1; j-i < 5 && j < len(c); j++ {
cx := c[j]
if unicode.IsDigit(cx.Rune) {
eat++
if !fgdone {
fg = fg + string(cx.Rune)
} else {
bg = bg + string(cx.Rune)
}
} else if cx.Rune == ',' {
if fg == "" {
break
}
eat++
fgdone = true
} else {
break
}
}
i += eat
if fg == "" && bg == "" {
style.Fg = initial.Fg
style.Bg = initial.Bg
} else {
fgi, _ := strconv.Atoi(fg)
style.Fg = colors.IRCToUI(colors.IRC(fgi))
if bg != "" {
bgi, _ := strconv.Atoi(bg)
style.Bg = colors.IRCToUI(colors.IRC(bgi))
}
}
continue
default:
cells = append(cells, cc)
}
}
return cells
}

+ 68
- 0
widget/statusbar.go View File

@ -0,0 +1,68 @@
package widget
import (
"image"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
type ActivityType int
const TabHasActivity ActivityType = 0
const TabHasNotice ActivityType = 1
// StatusBarPane contains the tabs for available windows.
// This widget compounds a termui TabPane widget with highlighting of tabs
// in two additional ways: notice and activity. Notice is intended for when
// a tab wants extra attention (ie. user was mentioned) vs activity where
// there are just some new lines since last touched.
type StatusBarPane struct {
*widgets.TabPane
TabsWithActivity map[int]ActivityType
NoticeStyle ui.Style
ActivityStyle ui.Style
}
func NewStatusBarPane() *StatusBarPane {
return &StatusBarPane{
TabPane: widgets.NewTabPane(" 0 "),
}
}
func (sb *StatusBarPane) Draw(buf *ui.Buffer) {
sb.Block.Draw(buf)
xCoordinate := sb.Inner.Min.X
for i, name := range sb.TabNames {
ColorPair := sb.InactiveTabStyle
if k, ok := sb.TabsWithActivity[i]; ok {
switch k {
case TabHasActivity:
ColorPair = sb.ActivityStyle
case TabHasNotice:
ColorPair = sb.NoticeStyle
}
}
if i == sb.ActiveTabIndex {
ColorPair = sb.ActiveTabStyle
}
buf.SetString(
ui.TrimString(name, sb.Inner.Max.X-xCoordinate),
ColorPair,
image.Pt(xCoordinate, sb.Inner.Min.Y),
)
xCoordinate += 1 + len(name)
if i < len(sb.TabNames)-1 && xCoordinate < sb.Inner.Max.X {
buf.SetCell(
ui.NewCell(ui.VERTICAL_LINE, ui.NewStyle(ui.ColorWhite)),
image.Pt(xCoordinate, sb.Inner.Min.Y),
)
}
xCoordinate += 2
}
}

text_input.go → widget/text_input.go View File


widget_nicklist.go → widget/userlist.go View File


+ 6
- 9
window_manager.go View File

@ -6,6 +6,8 @@ import (
"code.dopame.me/veonik/squircy3/event"
"github.com/sirupsen/logrus"
"code.dopame.me/veonik/squirssi/widget"
)
type WindowManager struct {
@ -26,22 +28,17 @@ func NewWindowManager(ev *event.Dispatcher) *WindowManager {
return wm
}
type activityType int
const TabHasActivity activityType = 0
const TabHasNotice activityType = 1
func (wm *WindowManager) tabNames() ([]string, map[int]activityType) {
func (wm *WindowManager) TabNames() ([]string, map[int]widget.ActivityType) {
wm.mu.RLock()
defer wm.mu.RUnlock()
res := make([]string, len(wm.windows))
activity := make(map[int]activityType)
activity := make(map[int]widget.ActivityType)
for i := 0; i < len(wm.windows); i++ {
win := wm.windows[i]
if win.HasNotice() {
activity[i] = TabHasNotice
activity[i] = widget.TabHasNotice
} else if win.HasActivity() {
activity[i] = TabHasActivity
activity[i] = widget.TabHasActivity
}
if wm.activeIndex == i {
res[i] = fmt.Sprintf(" %s ", win.Title())


Loading…
Cancel
Save