From 9e9eb70cd2edcf81f3d4c471f149d4e00754f0f0 Mon Sep 17 00:00:00 2001 From: qowevisa Date: Wed, 1 May 2024 17:21:10 +0300 Subject: [PATCH] Some changes in tui. --- tui/channel_all.go | 15 ++++ tui/channel_errors.go | 17 +++++ tui/channel_input.go | 71 ++++++++++++++++++ tui/channel_message.go | 37 ++++++++++ tui/channel_signals.go | 24 +++++++ tui/channel_state.go | 21 ++++++ tui/consts.go | 12 ++-- tui/reading.go | 6 +- tui/types.go | 16 ++++- tui/ui.go | 159 ++++++----------------------------------- tui/util.go | 27 +++++++ tui/widget_dialog.go | 125 ++++++++++++++++++++++++++++++++ 12 files changed, 383 insertions(+), 147 deletions(-) create mode 100644 tui/channel_all.go create mode 100644 tui/channel_errors.go create mode 100644 tui/channel_input.go create mode 100644 tui/channel_message.go create mode 100644 tui/channel_signals.go create mode 100644 tui/channel_state.go create mode 100644 tui/util.go create mode 100644 tui/widget_dialog.go diff --git a/tui/channel_all.go b/tui/channel_all.go new file mode 100644 index 0000000..7f1051d --- /dev/null +++ b/tui/channel_all.go @@ -0,0 +1,15 @@ +package tui + +func (t *TUI) launchAllChannels() error { + var err error + err = t.launchErrorsChannel() + if err != nil { + return err + } + t.errorsChannel <- t.launchInputChannel() + t.errorsChannel <- t.launchInputChannel() + t.errorsChannel <- t.launchMessageChannel() + t.errorsChannel <- t.launchSignalsChannel() + t.errorsChannel <- t.launchStateChannel() + return nil +} diff --git a/tui/channel_errors.go b/tui/channel_errors.go new file mode 100644 index 0000000..01159d0 --- /dev/null +++ b/tui/channel_errors.go @@ -0,0 +1,17 @@ +package tui + +import "git.qowevisa.me/Qowevisa/gotell/errors" + +func (t *TUI) launchErrorsChannel() error { + if t.errorsChannel == nil { + return errors.WrapErr("t.errors", errors.NOT_INIT) + } + go func() { + for err := range t.errorsChannel { + if err != nil { + t.createNotification(err.Error(), CONST_ERROR_N_TITLE) + } + } + }() + return nil +} diff --git a/tui/channel_input.go b/tui/channel_input.go new file mode 100644 index 0000000..2ce9211 --- /dev/null +++ b/tui/channel_input.go @@ -0,0 +1,71 @@ +package tui + +import ( + "log" + + "git.qowevisa.me/Qowevisa/gotell/errors" +) + +func (t *TUI) launchInputChannel() error { + if t.inputChannel == nil { + return errors.WrapErr("t.inputChannel", errors.NOT_INIT) + } + go func() { + for r := range t.inputChannel { + log.Printf("Read rune: %#v from t.input\n", r) + selWidget := t.selectedWidget + // Enter + if r == 13 { + log.Printf("Debug: selWidget: %#v ; selWidget.Input: %#v", selWidget, selWidget.Input) + if selWidget != nil && selWidget.Input != nil { + t.errorsChannel <- selWidget.Handler(t, selWidget.Data) + buf := selWidget.Clear() + t.writeMu.Lock() + err := t.write(buf) + t.writeMu.Unlock() + if err != nil { + t.errorsChannel <- errors.WrapErr("t.write", err) + } + if selWidget.Next != nil { + log.Printf("Seeing that widget.Next is not nil") + t.errorsChannel <- t.addWidget(*selWidget.Next) + t.errorsChannel <- t.drawSelectedWidget() + } else { + t.readInputState <- false + } + if selWidget.Finale != nil { + log.Printf("Seeing that widget.Finale is not nil") + t.errorsChannel <- selWidget.Finale(t, selWidget.FinaleData) + } + } + continue + } else if r == 127 { + log.Printf("seeing r = 127") + sliceLen := len(*selWidget.Input) + log.Printf("sliceLen = %d\n", sliceLen) + if sliceLen > 0 { + log.Printf("sliceLen > 0") + *selWidget.Input = (*selWidget.Input)[:sliceLen-1] + t.writeMu.Lock() + t.errorsChannel <- t.moveCursor(t.cursorPosRow, t.cursorPosCol-1) + t.errorsChannel <- t.writeRune(' ') + t.errorsChannel <- t.moveCursor(t.cursorPosRow, t.cursorPosCol-1) + t.writeMu.Unlock() + } + continue + } + if selWidget != nil && selWidget.Input != nil { + log.Printf("t.input: append %#v to widget input\n", r) + *selWidget.Input = append(*selWidget.Input, r) + log.Printf("t.input: trying to write %#v", r) + t.writeMu.Lock() + err := t.writeRune(r) + t.writeMu.Unlock() + if err != nil { + t.errorsChannel <- err + } + } + } + }() + return nil +} diff --git a/tui/channel_message.go b/tui/channel_message.go new file mode 100644 index 0000000..c91d7e9 --- /dev/null +++ b/tui/channel_message.go @@ -0,0 +1,37 @@ +package tui + +import ( + "git.qowevisa.me/Qowevisa/gotell/communication" + "git.qowevisa.me/Qowevisa/gotell/errors" +) + +// Basically every X_channel.go file launches some sort of channel + +func (t *TUI) launchMessageChannel() error { + if t.messageChannel == nil { + return errors.WrapErr("t.messageChannel", errors.NOT_INIT) + } + go func() { + for message := range t.messageChannel { + t.createNotification(string(message), "Message!") + msg, err := communication.Decode(message) + t.errorsChannel <- err + if err != nil { + continue + } + switch msg.Type { + case communication.SERVER_COMMAND: + t.handleServerCommands(msg.Data) + } + } + }() + return nil +} + +func (t *TUI) handleServerCommands(data []byte) { + if len(data) == 1 { + if data[0] == communication.NICKNAME { + t.SendMessageToServer("Nickname", 16) + } + } +} diff --git a/tui/channel_signals.go b/tui/channel_signals.go new file mode 100644 index 0000000..eb67cee --- /dev/null +++ b/tui/channel_signals.go @@ -0,0 +1,24 @@ +package tui + +import ( + "log" + "syscall" + + "git.qowevisa.me/Qowevisa/gotell/errors" +) + +func (t *TUI) launchSignalsChannel() error { + if t.osSignals == nil { + return errors.WrapErr("t.osSignals", errors.NOT_INIT) + } + go func() { + for sig := range t.osSignals { + log.Printf("Receive OS.signal: %#v\n", sig) + switch sig { + case syscall.SIGWINCH: + t.errorsChannel <- t.redraw() + } + } + }() + return nil +} diff --git a/tui/channel_state.go b/tui/channel_state.go new file mode 100644 index 0000000..a6c6570 --- /dev/null +++ b/tui/channel_state.go @@ -0,0 +1,21 @@ +package tui + +import "git.qowevisa.me/Qowevisa/gotell/errors" + +func (t *TUI) launchStateChannel() error { + if t.stateChannel == nil { + return errors.WrapErr("t.stateChannel", errors.NOT_INIT) + } + go func() { + for state := range t.stateChannel { + t.writeMu.Lock() + oldRow, oldCol := t.getCursorPos() + t.errorsChannel <- t.moveCursor(t.height, len(footerStart)+1) + t._clearLine() + t.errorsChannel <- t.write(state) + t.errorsChannel <- t.moveCursor(oldRow, oldCol) + t.writeMu.Unlock() + } + }() + return nil +} diff --git a/tui/consts.go b/tui/consts.go index 4ac60bb..b436522 100644 --- a/tui/consts.go +++ b/tui/consts.go @@ -5,6 +5,10 @@ const ( MY_SIGNAL_MESSAGE MY_SIGNAL_CONNECT MY_SIGNAL_CLOSE + MY_SIGNAL_MOVE_CURSOR_UP + MY_SIGNAL_MOVE_CURSOR_DOWN + MY_SIGNAL_MOVE_CURSOR_LEFT + MY_SIGNAL_MOVE_CURSOR_RIGHT ) const ( @@ -22,8 +26,8 @@ const ( footerStart = "State: " ) -func (c *cursorPosConfigValue) isGeneral() bool { - switch *c { +func (c cursorPosConfigValue) isGeneral() bool { + switch c { case cursorPosGeneralCenter: return true case cursorPosGeneralLeft: @@ -40,8 +44,8 @@ const ( widgetPosGeneralRightCenter widgetPosConfigValue = -3 ) -func (w *widgetPosConfigValue) isGeneral() bool { - switch *w { +func (w widgetPosConfigValue) isGeneral() bool { + switch w { case widgetPosGeneralCenter: return true case widgetPosGeneralLeftCenter: diff --git a/tui/reading.go b/tui/reading.go index 8b59fe7..890f319 100644 --- a/tui/reading.go +++ b/tui/reading.go @@ -15,7 +15,7 @@ func (t *TUI) launchReadingMessagesFromConnection(ctx context.Context, wg *sync. defer wg.Done() // Mark this goroutine as done when it exits if t.messageChannel == nil { - t.errors <- errors.WrapErr("t.messageChannel", errors.NOT_INIT) + t.errorsChannel <- errors.WrapErr("t.messageChannel", errors.NOT_INIT) return } buf := make([]byte, CONST_MESSAGE_LEN) @@ -27,7 +27,7 @@ func (t *TUI) launchReadingMessagesFromConnection(ctx context.Context, wg *sync. timeoutDuration := 5 * time.Second err := t.tlsConnection.SetReadDeadline(time.Now().Add(timeoutDuration)) if err != nil { - t.errors <- errors.WrapErr("SetReadDeadline", err) + t.errorsChannel <- errors.WrapErr("SetReadDeadline", err) return } n, err := t.tlsConnection.Read(buf) @@ -36,7 +36,7 @@ func (t *TUI) launchReadingMessagesFromConnection(ctx context.Context, wg *sync. if netErr, ok := err.(net.Error); ok && netErr.Timeout() { continue } - t.errors <- errors.WrapErr("t.tlsConnection.Read", err) + t.errorsChannel <- errors.WrapErr("t.tlsConnection.Read", err) } return } diff --git a/tui/types.go b/tui/types.go index 2442a15..de9e0e5 100644 --- a/tui/types.go +++ b/tui/types.go @@ -60,6 +60,15 @@ type notifier struct { Buf string } +type dialog struct { + Row int + Col int + Width int + Height int + Buf string + Catcher RuneCatcher +} + type widgetDraw struct { Buf string Row int @@ -89,6 +98,8 @@ type closeData struct { cancel context.CancelFunc } +type RuneCatcher func(runes chan (rune)) error + type TUI struct { width int height int @@ -97,11 +108,12 @@ type TUI struct { writeMu sync.Mutex sizeMutex sync.Mutex oldState *term.State - input chan (rune) + inputChannel chan (rune) + inputCatcher RuneCatcher printRunes chan (rune) mySignals chan (mySignal) osSignals chan (os.Signal) - errors chan (error) + errorsChannel chan (error) readInputState chan (bool) readEnterState chan (bool) stateChannel chan (string) diff --git a/tui/ui.go b/tui/ui.go index cbbb268..4255d83 100644 --- a/tui/ui.go +++ b/tui/ui.go @@ -17,10 +17,10 @@ import ( func (t *TUI) init() error { var err error - t.input = make(chan rune, 32) + t.inputChannel = make(chan rune, 32) t.printRunes = make(chan rune, 32) t.widgets = make([]*widget, 8) - t.errors = make(chan error, 4) + t.errorsChannel = make(chan error, 4) t.mySignals = make(chan mySignal, 1) t.osSignals = make(chan os.Signal, 1) t.writer = bufio.NewWriter(os.Stdout) @@ -46,6 +46,10 @@ func (t *TUI) init() error { if err != nil { return errors.WrapErr("t.readRoutines", err) } + err = t.launchAllChannels() + if err != nil { + return errors.WrapErr("t.launchAllChannels", err) + } return nil } @@ -64,7 +68,7 @@ func (t *TUI) Run() error { } // if t.mySignals == nil { - return errors.WrapErr("t.signals", errors.NOT_INIT) + return errors.WrapErr("t.mySignals", errors.NOT_INIT) } err = t.Draw() if err != nil { @@ -73,8 +77,8 @@ func (t *TUI) Run() error { for mySignal := range t.mySignals { log.Printf("Receive signal: %#v\n", mySignal) if mySignal.Type == MY_SIGNAL_EXIT { - t.errors <- t.clearScreen() - t.errors <- t.moveCursor(0, 0) + t.errorsChannel <- t.clearScreen() + t.errorsChannel <- t.moveCursor(0, 0) break } switch mySignal.Type { @@ -108,43 +112,23 @@ func (t *TUI) Run() error { }, }) if err != nil { - t.errors <- errors.WrapErr("t.addWidget", err) + t.errorsChannel <- errors.WrapErr("t.addWidget", err) } err = t.drawSelectedWidget() if err != nil { - t.errors <- errors.WrapErr("t.drawSelectedWidget", err) + t.errorsChannel <- errors.WrapErr("t.drawSelectedWidget", err) } case MY_SIGNAL_MESSAGE: if t.isConnected { - var msg []rune - h, d := SendMessageToConnectionEasy(&msg) - err := t.addWidget(widgetConfig{ - Input: &msg, - Title: "Message", - MinWidth: 20, - HasBorder: true, - WidgetPosConfig: widgetPosGeneralCenter, - CursorPosConfig: cursorPosGeneralCenter, - DataHandler: h, - Data: d, - Next: nil, - Finale: nil, - }) - if err != nil { - t.errors <- errors.WrapErr("t.addWidget", err) - } - err = t.drawSelectedWidget() - if err != nil { - t.errors <- errors.WrapErr("t.drawSelectedWidget", err) - } + t.SendMessageToServer("Message", 20) } case MY_SIGNAL_CLOSE: if t.isConnected { CloseConnection(t.tlsConnCloseData.wg, t.tlsConnCloseData.cancel) t.isConnected = false - t.errors <- t.tlsConnection.Close() + t.errorsChannel <- t.tlsConnection.Close() t.stateChannel <- "Disconnected" } default: @@ -157,23 +141,13 @@ func (t *TUI) Run() error { func (t *TUI) createNotification(text, title string) { notifier, err := createNotification(text, title) t.selectedNotifier = ¬ifier - t.errors <- err - t.errors <- t.write(notifier.Buf) + t.errorsChannel <- err + t.errorsChannel <- t.write(notifier.Buf) t.readEnterState <- true } func (t *TUI) setRoutines() error { - if t.errors == nil { - return errors.WrapErr("t.errors", errors.NOT_INIT) - } - go func() { - for err := range t.errors { - if err != nil { - t.createNotification(err.Error(), CONST_ERROR_N_TITLE) - } - } - }() - if t.input == nil { + if t.inputChannel == nil { return errors.WrapErr("t.input", errors.NOT_INIT) } if t.oldState == nil { @@ -191,22 +165,6 @@ func (t *TUI) setRoutines() error { if t.messageChannel == nil { return errors.WrapErr("t.messageChannel", errors.NOT_INIT) } - go func() { - for message := range t.messageChannel { - t.createNotification(string(message), "Message!") - } - }() - go func() { - for state := range t.stateChannel { - t.writeMu.Lock() - oldRow, oldCol := t.getCursorPos() - t.errors <- t.moveCursor(t.height, len(footerStart)+1) - t._clearLine() - t.errors <- t.write(state) - t.errors <- t.moveCursor(oldRow, oldCol) - t.writeMu.Unlock() - } - }() var readInputMu sync.Mutex var readEnterdMu sync.Mutex readInput := false @@ -239,7 +197,7 @@ func (t *TUI) setRoutines() error { if r == 13 { readEnter = false if t.selectedNotifier != nil { - t.errors <- t.write(t.selectedNotifier.Clear()) + t.errorsChannel <- t.write(t.selectedNotifier.Clear()) } continue } @@ -298,101 +256,26 @@ func (t *TUI) setRoutines() error { } else { if readInput { log.Printf("Send %c | %d to t.input", r, r) - t.input <- r + t.inputChannel <- r } } readInputMu.Lock() if readInput { switch r { case 13: - t.input <- r + t.inputChannel <- r case 127: - t.input <- r + t.inputChannel <- r } } readInputMu.Unlock() } }() // - if t.osSignals == nil { - return errors.WrapErr("t.osSignals", errors.NOT_INIT) - } - go func() { - for sig := range t.osSignals { - log.Printf("Receive OS.signal: %#v\n", sig) - switch sig { - case syscall.SIGWINCH: - t.errors <- t.redraw() - } - } - }() - if t.input == nil { - return errors.WrapErr("t.input", errors.NOT_INIT) - } - go func() { - for r := range t.input { - log.Printf("Read rune: %#v from t.input\n", r) - selWidget := t.selectedWidget - // Enter - if r == 13 { - log.Printf("Debug: selWidget: %#v ; selWidget.Input: %#v", selWidget, selWidget.Input) - if selWidget != nil && selWidget.Input != nil { - t.errors <- selWidget.Handler(t, selWidget.Data) - buf := selWidget.Clear() - t.writeMu.Lock() - err := t.write(buf) - t.writeMu.Unlock() - if err != nil { - t.errors <- errors.WrapErr("t.write", err) - } - if selWidget.Next != nil { - log.Printf("Seeing that widget.Next is not nil") - t.errors <- t.addWidget(*selWidget.Next) - t.errors <- t.drawSelectedWidget() - } else { - t.readInputState <- false - } - if selWidget.Finale != nil { - log.Printf("Seeing that widget.Finale is not nil") - t.errors <- selWidget.Finale(t, selWidget.FinaleData) - } - } - continue - } else if r == 127 { - log.Printf("seeing r = 127") - sliceLen := len(*selWidget.Input) - log.Printf("sliceLen = %d\n", sliceLen) - if sliceLen > 0 { - log.Printf("sliceLen > 0") - *selWidget.Input = (*selWidget.Input)[:sliceLen-1] - t.writeMu.Lock() - t.errors <- t.moveCursor(t.cursorPosRow, t.cursorPosCol-1) - t.errors <- t.writeRune(' ') - t.errors <- t.moveCursor(t.cursorPosRow, t.cursorPosCol-1) - t.writeMu.Unlock() - } - continue - } - if selWidget != nil && selWidget.Input != nil { - log.Printf("t.input: append %#v to widget input\n", r) - *selWidget.Input = append(*selWidget.Input, r) - log.Printf("t.input: trying to write %#v", r) - t.writeMu.Lock() - err := t.writeRune(r) - t.writeMu.Unlock() - if err != nil { - t.errors <- err - } - } - } - }() return nil } func (t *TUI) readRoutines() error { - if t.input == nil { - return errors.WrapErr("t.input", errors.NOT_INIT) - } return nil } @@ -457,7 +340,7 @@ func (t *TUI) getCursorPos() (int, int) { } func (t *TUI) _clearLine() { - t.errors <- t.write("\033[0K") + t.errorsChannel <- t.write("\033[0K") } func (t *TUI) moveCursor(row, col int) error { diff --git a/tui/util.go b/tui/util.go new file mode 100644 index 0000000..5653e36 --- /dev/null +++ b/tui/util.go @@ -0,0 +1,27 @@ +package tui + +import "git.qowevisa.me/Qowevisa/gotell/errors" + +func (t *TUI) SendMessageToServer(title string, minW int) { + var msg []rune + h, d := SendMessageToConnectionEasy(&msg) + err := t.addWidget(widgetConfig{ + Input: &msg, + Title: title, + MinWidth: minW, + HasBorder: true, + WidgetPosConfig: widgetPosGeneralCenter, + CursorPosConfig: cursorPosGeneralCenter, + DataHandler: h, + Data: d, + Next: nil, + Finale: nil, + }) + if err != nil { + t.errorsChannel <- errors.WrapErr("t.addWidget", err) + } + err = t.drawSelectedWidget() + if err != nil { + t.errorsChannel <- errors.WrapErr("t.drawSelectedWidget", err) + } +} diff --git a/tui/widget_dialog.go b/tui/widget_dialog.go new file mode 100644 index 0000000..307803a --- /dev/null +++ b/tui/widget_dialog.go @@ -0,0 +1,125 @@ +package tui + +import ( + "fmt" + "strings" + + "git.qowevisa.me/Qowevisa/gotell/errors" +) + +func createDialog(message, title string) (dialog, error) { + var buf string + width, height := UI.getSizes() + if width == 0 { + return dialog{}, errors.WrapErr("width", errors.NOT_INIT) + } + if height == 0 { + return dialog{}, errors.WrapErr("height", errors.NOT_INIT) + } + maxWidth := width / 3 + maxHeight := 5 + errMsgLen := len(message) + innerPart := maxWidth - 2 + if errMsgLen <= innerPart { + maxWidth = errMsgLen + 2 + } else { + for { + if errMsgLen <= innerPart { + break + } + maxHeight++ + errMsgLen -= innerPart + } + } + innerPart = maxWidth - 2 + col := (width - maxWidth) / 2 + row := (height - maxHeight) / 2 + startCol := col + startRow := row + + buf += getBufForMovingCursorTo(row, col) + buf += strings.Repeat("-", maxWidth) + + row++ + buf += getBufForMovingCursorTo(row, col) + buf += centerText(maxWidth, title) + + row++ + buf += getBufForMovingCursorTo(row, col) + buf += strings.Repeat("-", maxWidth) + + startI := 0 + endI := innerPart + for i := 3; i < maxHeight-1; i++ { + var tmp string + if endI > len(message) { + tmp = message[startI:] + } else { + tmp = message[startI:endI] + } + row++ + buf += getBufForMovingCursorTo(row, col) + var spaces string + if innerPart > len(tmp) { + spaces = strings.Repeat(" ", innerPart-len(tmp)) + } + buf += fmt.Sprintf("|%s%s|", tmp, spaces) + startI += innerPart + endI += innerPart + } + + row++ + buf += getBufForMovingCursorTo(row, col) + buf += strings.Repeat("-", maxWidth) + + row++ + buf += getBufForMovingCursorTo(row, col) + buf += centerText(maxWidth, "YES | NO") + + row++ + buf += getBufForMovingCursorTo(row, col) + buf += strings.Repeat("-", maxWidth) + return dialog{ + Row: startRow, + Col: startCol, + Width: maxWidth, + Height: row - startRow + 1, + Buf: buf, + }, nil +} + +const ( + _int_CatcherNone = iota + _int_Catcher1Arrow + _int_Catcher2Arrow + _int_CatcherArrow +) + +func dialogRuneCatcher(t *TUI, runes chan (rune)) error { + state := _int_CatcherNone + for r := range runes { + if r == 27 && state == _int_CatcherNone { + state = _int_Catcher1Arrow + } else { + continue + } + if r == 91 && state == _int_Catcher1Arrow { + state = _int_Catcher2Arrow + } else { + continue + } + if state == _int_Catcher2Arrow { + switch r { + case 65: + t.mySignals <- mySignal{Type: MY_SIGNAL_MOVE_CURSOR_UP} + case 66: + t.mySignals <- mySignal{Type: MY_SIGNAL_MOVE_CURSOR_DOWN} + case 67: + t.mySignals <- mySignal{Type: MY_SIGNAL_MOVE_CURSOR_RIGHT} + case 68: + t.mySignals <- mySignal{Type: MY_SIGNAL_MOVE_CURSOR_LEFT} + } + } + } + return nil +}