diff --git a/tui/consts.go b/tui/consts.go new file mode 100644 index 0000000..c7f8c52 --- /dev/null +++ b/tui/consts.go @@ -0,0 +1,47 @@ +package tui + +const ( + MY_SIGNAL_EXIT = iota + MY_SIGNAL_MESSAGE + MY_SIGNAL_CONNECT +) + +const ( + cursorPosGeneralCenter cursorPosConfigValue = -1 + cursorPosGeneralLeft cursorPosConfigValue = -2 + cursorPosGeneralRight cursorPosConfigValue = -3 +) + +const ( + footerStart = "State: " +) + +func (c *cursorPosConfigValue) isGeneral() bool { + switch *c { + case cursorPosGeneralCenter: + return true + case cursorPosGeneralLeft: + return true + case cursorPosGeneralRight: + return true + } + return false +} + +const ( + widgetPosGeneralCenter widgetPosConfigValue = -1 + widgetPosGeneralLeftCenter widgetPosConfigValue = -2 + widgetPosGeneralRightCenter widgetPosConfigValue = -3 +) + +func (w *widgetPosConfigValue) isGeneral() bool { + switch *w { + case widgetPosGeneralCenter: + return true + case widgetPosGeneralLeftCenter: + return true + case widgetPosGeneralRightCenter: + return true + } + return false +} diff --git a/tui/funcs.go b/tui/funcs.go new file mode 100644 index 0000000..0d4a12f --- /dev/null +++ b/tui/funcs.go @@ -0,0 +1,101 @@ +package tui + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "log" + "os" + "strconv" + + "git.qowevisa.me/Qowevisa/gotell/env" + "git.qowevisa.me/Qowevisa/gotell/errors" +) + +func GetIntPercentFromData(a, b int) int { + return int(float64((a * 100)) / float64(b)) +} + +func SendMessageToConnectionEasy(msg *[]rune) (dataProcessHandler, dataT) { + return SendMessageToConnection, dataT{rawP: msg} +} + +func SendMessageToConnection(t *TUI, data dataT) error { + if t.tlsConnection == nil { + return errors.WrapErr("t.tlsConnection", errors.NOT_SET) + } + if data.rawP == nil { + return errors.WrapErr("data.rawP", errors.NOT_SET) + } + message := string(*data.rawP) + n, err := t.tlsConnection.Write([]byte(message)) + if err != nil { + return errors.WrapErr("t.tlsConnection.Write", err) + } + log.Printf("Successfully wrote %d bytes to connection; Message: %s", n, message) + return nil +} + +// takes data from storage +func FE_ConnectTLS(t *TUI, data dataT) error { + log.Printf("Start of FE_ConnectTLS") + host, exist := t.storage[STORAGE_HOST_CONST] + if !exist { + errors.WrapErr("t.storage:host", errors.NOT_SET) + } + portStr, exist := t.storage[STORAGE_PORT_CONST] + if !exist { + errors.WrapErr("t.storage:host", errors.NOT_SET) + } + port, err := strconv.ParseInt(portStr, 10, 32) + if err != nil { + errors.WrapErr("port.strconv.ParseInt", err) + } + loadingFileName := env.ServerFullchainFileName + cert, err := os.ReadFile(loadingFileName) + if err != nil { + errors.WrapErr("os.ReadFile", err) + } + log.Printf("Certificate %s loaded successfully!\n", loadingFileName) + // + roots := x509.NewCertPool() + if ok := roots.AppendCertsFromPEM(cert); !ok { + errors.WrapErr("client: failed to parse root certificate", nil) + } + + config := &tls.Config{ + RootCAs: roots, + } + conn, err := tls.Dial( + "tcp", + fmt.Sprintf("%s:%d", host, int(port)), + config, + ) + if err != nil { + return errors.WrapErr("tls.Dial", err) + } + // log.Printf("Set connection to %#v\n", conn) + t.tlsConnection = conn + if t.stateChannel == nil { + return errors.WrapErr("t.stateChannel", errors.NOT_INIT) + } + t.stateChannel <- "TLS Connected" + t.isConnected = true + return nil +} + +func AddToStorageEasy(key string, val *[]rune) (dataProcessHandler, dataT) { + // that's why I create wrapper around it. + // try to understand that, dear viewer! + return H_AddToStorage, dataT{rawP: val, op1: key} +} + +func H_AddToStorage(t *TUI, data dataT) error { + log.Printf("Debug: %#v", data) + log.Printf("Adding to storage: %s = %s", data.op1, string(*data.rawP)) + if t.storage == nil { + return errors.WrapErr("t.storage", errors.NOT_INIT) + } + t.storage[data.op1] = string(*data.rawP) + return nil +} diff --git a/tui/input.go b/tui/input.go new file mode 100644 index 0000000..89aa0ee --- /dev/null +++ b/tui/input.go @@ -0,0 +1 @@ +package tui diff --git a/tui/notification.go b/tui/notification.go new file mode 100644 index 0000000..b109740 --- /dev/null +++ b/tui/notification.go @@ -0,0 +1,111 @@ +package tui + +import ( + "fmt" + "strings" + + "git.qowevisa.me/Qowevisa/gotell/errors" +) + +func centerText(width int, text string) string { + emptyLen := width - len(text) - 2 + leftEmptyLen := emptyLen / 2 + return fmt.Sprintf( + "|%s%s%s|", + strings.Repeat(" ", leftEmptyLen), + text, + strings.Repeat(" ", emptyLen-leftEmptyLen), + ) +} + +func createNotification(message string) (notifier, error) { + title := "ERROR" + var buf string + width, height := UI.getSizes() + if width == 0 { + return notifier{}, errors.WrapErr("width", errors.NOT_INIT) + } + if height == 0 { + return notifier{}, 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, "OK") + + row++ + buf += getBufForMovingCursorTo(row, col) + buf += strings.Repeat("-", maxWidth) + return notifier{ + Row: startRow, + Col: startCol, + Width: maxWidth, + Height: row - startRow + 1, + Buf: buf, + }, nil +} + +func (n *notifier) Clear() string { + var buf string + for i := 0; i < n.Height; i++ { + buf += getBufForMovingCursorTo(n.Row, n.Col) + buf += strings.Repeat(" ", n.Width) + n.Row++ + } + return buf +} diff --git a/tui/storage_consts.go b/tui/storage_consts.go new file mode 100644 index 0000000..970d0e8 --- /dev/null +++ b/tui/storage_consts.go @@ -0,0 +1,6 @@ +package tui + +const ( + STORAGE_HOST_CONST = "host" + STORAGE_PORT_CONST = "port" +) diff --git a/tui/table.go b/tui/table.go new file mode 100644 index 0000000..3c8f0a5 --- /dev/null +++ b/tui/table.go @@ -0,0 +1,30 @@ +package tui + +const ( + CTRL_A = 1 << iota + CTRL_B + CTRL_C + CTRL_D + CTRL_E + CTRL_F + CTRL_G + CTRL_H + CTRL_I + CTRL_J + CTRL_K + CTRL_L + CTRL_M + CTRL_N + CTRL_O + CTRL_P + CTRL_Q + CTRL_R + CTRL_S + CTRL_T + CTRL_U + CTRL_V + CTRL_W + CTRL_X + CTRL_Y + CTRL_Z +) diff --git a/tui/types.go b/tui/types.go new file mode 100644 index 0000000..6707b62 --- /dev/null +++ b/tui/types.go @@ -0,0 +1,110 @@ +package tui + +import ( + "bufio" + "crypto/tls" + "os" + "sync" + + "golang.org/x/term" +) + +type mySignal struct { + Type int +} + +var UI TUI + +type dataT struct { + raw string + rawP *[]rune + op1 string + op2 string + ops []string +} + +type dataProcessHandler func(t *TUI, data dataT) error + +type tuiPointPair struct { + startCol int + startRow int + endCol int + endRow int +} + +type widget struct { + row int + col int + MinWidth int + MinHeight int + Width int + Height int + Title string + Input *[]rune + Handler dataProcessHandler + Data dataT + Next *widgetConfig + Finale dataProcessHandler + FinaleData dataT + percentPair tuiPointPair + snappedPair tuiPointPair + startupConfig widgetConfig +} + +type notifier struct { + Row int + Col int + Width int + Height int + Buf string +} + +type widgetDraw struct { + Buf string + Row int + Col int +} + +type cursorPosConfigValue int +type widgetPosConfigValue int + +type widgetConfig struct { + MinWidth int + MinHeight int + Input *[]rune + CursorPosConfig cursorPosConfigValue + Title string + WidgetPosConfig widgetPosConfigValue + HasBorder bool + DataHandler dataProcessHandler + Data dataT + Next *widgetConfig + Finale dataProcessHandler + FinaleData dataT +} + +type TUI struct { + width int + height int + cursorPosRow int + cursorPosCol int + writeMu sync.Mutex + sizeMutex sync.Mutex + oldState *term.State + input chan (rune) + printRunes chan (rune) + mySignals chan (mySignal) + osSignals chan (os.Signal) + errors chan (error) + readInputState chan (bool) + readEnterState chan (bool) + stateChannel chan (string) + widgets []*widget + widgetsMutext sync.Mutex + writer *bufio.Writer + isConnected bool + selectedWidget *widget + selectedNotifier *notifier + storage map[string]string + tlsConnection *tls.Conn +} diff --git a/tui/ui.go b/tui/ui.go new file mode 100644 index 0000000..3f38758 --- /dev/null +++ b/tui/ui.go @@ -0,0 +1,536 @@ +package tui + +import ( + "bufio" + "fmt" + "log" + "os" + "os/signal" + "sync" + "syscall" + "unicode" + + "git.qowevisa.me/Qowevisa/gotell/errors" + "golang.org/x/term" +) + +func (t *TUI) init() error { + var err error + t.input = make(chan rune, 32) + t.printRunes = make(chan rune, 32) + t.widgets = make([]*widget, 8) + t.errors = make(chan error, 4) + t.mySignals = make(chan mySignal, 1) + t.osSignals = make(chan os.Signal, 1) + t.writer = bufio.NewWriter(os.Stdout) + t.storage = make(map[string]string) + t.readInputState = make(chan bool, 1) + t.readEnterState = make(chan bool, 1) + t.stateChannel = make(chan string, 1) + signal.Notify(t.osSignals, syscall.SIGWINCH) + err = t.setSizes() + if err != nil { + return errors.WrapErr("t.getSizes", err) + } + err = t.setTermToRaw() + if err != nil { + return errors.WrapErr("t.setTermToRaw", err) + } + err = t.setRoutines() + if err != nil { + return errors.WrapErr("t.setRoutines", err) + } + err = t.readRoutines() + if err != nil { + return errors.WrapErr("t.readRoutines", err) + } + return nil +} + +func (t *TUI) exit() { + if t.oldState != nil { + term.Restore(int(os.Stdin.Fd()), t.oldState) + } +} + +func (t *TUI) Run() error { + defer t.exit() + var err error + err = t.init() + if err != nil { + return errors.WrapErr("t.init", err) + } + // + if t.mySignals == nil { + return errors.WrapErr("t.signals", errors.NOT_INIT) + } + err = t.Draw() + if err != nil { + return errors.WrapErr("t.Draw", err) + } + 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) + break + } + switch mySignal.Type { + case MY_SIGNAL_CONNECT: + var host []rune + var port []rune + hostHandler, hostData := AddToStorageEasy("host", &host) + portHandler, portData := AddToStorageEasy("port", &port) + err := t.addWidget(widgetConfig{ + Input: &host, + Title: "Host", + MinWidth: 16, + HasBorder: true, + WidgetPosConfig: widgetPosGeneralCenter, + CursorPosConfig: cursorPosGeneralCenter, + DataHandler: hostHandler, + Data: hostData, + Finale: nil, + Next: &widgetConfig{ + Input: &port, + Title: "Port", + MinWidth: 8, + HasBorder: true, + WidgetPosConfig: widgetPosGeneralCenter, + CursorPosConfig: cursorPosGeneralCenter, + DataHandler: portHandler, + Data: portData, + Next: nil, + Finale: FE_ConnectTLS, + FinaleData: dataT{}, + }, + }) + if err != nil { + t.errors <- errors.WrapErr("t.addWidget", err) + } + err = t.drawSelectedWidget() + if err != nil { + t.errors <- 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) + } + } + } + } + // + return nil +} + +func (t *TUI) createNotification(text string) { + notifier, err := createNotification(text) + t.selectedNotifier = ¬ifier + t.errors <- err + t.errors <- 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()) + } + } + }() + if t.input == nil { + return errors.WrapErr("t.input", errors.NOT_INIT) + } + if t.oldState == nil { + return errors.WrapErr("t.oldState", errors.NOT_INIT) + } + if t.readInputState == nil { + return errors.WrapErr("t.readInputState", errors.NOT_INIT) + } + if t.readEnterState == nil { + return errors.WrapErr("t.readEnterState", errors.NOT_INIT) + } + 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.moveCursor(t.height, len(footerStart)+1) + t.write(state) + t.moveCursor(oldRow, oldCol) + t.writeMu.Unlock() + } + }() + var readInputMu sync.Mutex + var readEnterdMu sync.Mutex + readInput := false + readCommand := false + readEnter := false + go func() { + for newState := range t.readInputState { + readInputMu.Lock() + readInput = newState + readInputMu.Unlock() + } + }() + go func() { + for newState := range t.readEnterState { + readEnterdMu.Lock() + readEnter = newState + readEnterdMu.Unlock() + } + }() + go func() { + reader := bufio.NewReader(os.Stdin) + for { + r, _, err := reader.ReadRune() + if err != nil { + panic(err) + } + log.Printf("Read %#v rune\n", r) + if readEnter { + if r == 13 { + readEnter = false + if t.selectedNotifier != nil { + t.errors <- t.write(t.selectedNotifier.Clear()) + } + continue + } + } + if readCommand { + switch r { + case 'q': + t.mySignals <- mySignal{ + Type: MY_SIGNAL_EXIT, + } + case 'c': + t.mySignals <- mySignal{ + Type: MY_SIGNAL_CONNECT, + } + readInputMu.Lock() + readInput = true + readInputMu.Unlock() + case 'm': + t.mySignals <- mySignal{ + Type: MY_SIGNAL_MESSAGE, + } + readInputMu.Lock() + readInput = true + readInputMu.Unlock() + case 't': + t.createNotification("some notify") + readEnter = true + } + + readCommand = false + continue + } + // + if unicode.IsControl(r) { + switch r { + case CTRL_A: + readCommand = true + } + } else { + if readInput { + log.Printf("Send %c | %d to t.input", r, r) + t.input <- r + } + } + readInputMu.Lock() + if readInput { + switch r { + case 13: + t.input <- r + case 127: + t.input <- 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 +} + +func (t *TUI) Draw() error { + err := t.clearScreen() + if err != nil { + return errors.WrapErr("t.clearScreen", err) + } + err = t.drawFooter() + if err != nil { + return errors.WrapErr("t.drawFooter", err) + } + return nil +} + +func (t *TUI) drawFooter() error { + t.writeMu.Lock() + defer t.writeMu.Unlock() + err := t.moveCursor(t.height, 0) + if err != nil { + return errors.WrapErr("t.moveCursor", err) + } + err = t.write(footerStart) + if err != nil { + return errors.WrapErr("t.write", err) + } + return nil +} + +func (t *TUI) write(s string) error { + _, err := t.writer.WriteString(s) + if err != nil { + return errors.WrapErr("t.writer.WriteString", err) + } + err = t.writer.Flush() + if err != nil { + return errors.WrapErr("t.writer.Flush", err) + } + t.cursorPosCol += len(s) + if t.cursorPosCol > t.width { + t.cursorPosCol %= t.width + t.cursorPosRow++ + } + return nil +} + +func (t *TUI) writeRune(r rune) error { + _, err := t.writer.WriteRune(r) + if err != nil { + return errors.WrapErr("t.writer.WriteRune", err) + } + err = t.writer.Flush() + if err != nil { + return errors.WrapErr("t.writer.Flush", err) + } + t.cursorPosCol++ + return nil +} + +func (t *TUI) getCursorPos() (int, int) { + return t.cursorPosRow, t.cursorPosCol +} + +func (t *TUI) moveCursor(row, col int) error { + t.sizeMutex.Lock() + defer t.sizeMutex.Unlock() + if row > t.height { + return errors.WrapErr(fmt.Sprintf("row: %d; height: %d", row, t.height), errors.OUT_OF_BOUND) + } + if col > t.width { + return errors.WrapErr(fmt.Sprintf("col: %d; width: %d", col, t.width), errors.OUT_OF_BOUND) + } + log.Printf("t.cursorPosRow: %d ; t.cursorPosCol: %d\n", t.cursorPosRow, t.cursorPosCol) + log.Printf("trying to move to row: %d ; col %d\n", row, col) + _, err := t.writer.WriteString(fmt.Sprintf("\033[%d;%dH", row, col)) + if err != nil { + return errors.WrapErr("t.writer.WriteString", err) + } + err = t.writer.Flush() + if err != nil { + return errors.WrapErr("t.writer.Flush", err) + } + t.cursorPosCol = col + log.Printf("t.cursorPosCol now is = %d\n", col) + t.cursorPosRow = row + log.Printf("t.cursorPosRow now is = %d\n", row) + return nil +} + +func (t *TUI) clearScreen() error { + _, err := t.writer.WriteString("\033[2J") + if err != nil { + return errors.WrapErr("t.writer.WriteString", err) + } + err = t.writer.Flush() + if err != nil { + return errors.WrapErr("t.writer.Flush", err) + } + return nil +} + +func (t *TUI) drawSelectedWidget() error { + wDraw, err := t.selectedWidget.Draw() + if err != nil { + return errors.WrapErr("t.selectedWidget.Draw", err) + } + t.writeMu.Lock() + err = t.write(wDraw.Buf) + t.writeMu.Unlock() + if err != nil { + return errors.WrapErr("t.write", err) + } + t.cursorPosRow = wDraw.Row + t.cursorPosCol = wDraw.Col + return nil +} + +// Creating and adding widget from config. +// Also sets created widget as selectedWidget +func (t *TUI) addWidget(config widgetConfig) error { + widget := &widget{} + err := widget.init(config) + if err != nil { + return errors.WrapErr("widget.init", err) + } + t.widgetsMutext.Lock() + defer t.widgetsMutext.Unlock() + t.widgets = append(t.widgets, widget) + t.selectedWidget = widget + return nil +} + +func (t *TUI) redraw() error { + var err error + err = t.setSizes() + if err != nil { + return errors.WrapErr("t.getSizes", err) + } + err = t.redrawWidgets() + if err != nil { + return errors.WrapErr("t.redrawWidgets", err) + } + return nil +} + +func (t *TUI) setSizes() error { + w, h, err := term.GetSize(int(os.Stdin.Fd())) + if err != nil { + return errors.WrapErr("term.GetSize", err) + } + t.sizeMutex.Lock() + t.width = w + t.height = h + t.sizeMutex.Unlock() + return nil +} + +func (t *TUI) getSizes() (int, int) { + t.sizeMutex.Lock() + defer t.sizeMutex.Unlock() + return t.width, t.height +} + +func (t *TUI) redrawWidgets() error { + if t.widgets == nil { + return errors.WrapErr("t.widgets", errors.NOT_INIT) + } + // TODO + return nil +} + +func (t *TUI) setTermToRaw() error { + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return errors.WrapErr("term.MakeRaw", err) + } + t.oldState = oldState + return nil +} diff --git a/tui/widget.go b/tui/widget.go new file mode 100644 index 0000000..bd1d8f5 --- /dev/null +++ b/tui/widget.go @@ -0,0 +1,175 @@ +package tui + +import ( + "fmt" + "strings" + + "git.qowevisa.me/Qowevisa/gotell/errors" +) + +// func (w *widget) init(startX, startY, endX, endY int) error { +// width, height := UI.getSizes() +// +// return nil +// } + +func (w *widget) init(config widgetConfig) error { + width, height := UI.getSizes() + if width == 0 { + return errors.WrapErr("width", errors.NOT_INIT) + } + if height == 0 { + return errors.WrapErr("height", errors.NOT_INIT) + } + var startRow, startCol int + // I guess I really has to do it that way bc go doesn't like my C style + // of writting `-2 * config.HasBorder` or `-2 * (config.HasBorder == true)` + var factorIfHasBorder int + if config.HasBorder { + factorIfHasBorder = 1 + } else { + factorIfHasBorder = 0 + } + if config.WidgetPosConfig.isGeneral() { + switch config.WidgetPosConfig { + case widgetPosGeneralCenter: + startCol = (width - len(config.Title)) / 2 + startRow = (height - 3 - 2*factorIfHasBorder) / 2 + case widgetPosGeneralLeftCenter: + startCol = 0 + startRow = (height - 3 - 2*factorIfHasBorder) / 2 + case widgetPosGeneralRightCenter: + startCol = (width - len(config.Title) - 2*factorIfHasBorder) + startRow = (height - 3 - 2*factorIfHasBorder) / 2 + default: + return errors.WrapErr(fmt.Sprintf("config.WidgetPosConfig: %d :", config.WidgetPosConfig), errors.NOT_HANDLED) + } + } + snappedPair := tuiPointPair{ + startCol: startCol, + startRow: startRow, + endCol: startCol + len(config.Title) + 2, + endRow: startRow + 5, + } + percentPair := tuiPointPair{ + startCol: GetIntPercentFromData(snappedPair.startCol, width), + startRow: GetIntPercentFromData(snappedPair.startRow, height), + endCol: GetIntPercentFromData(snappedPair.endCol, width), + endRow: GetIntPercentFromData(snappedPair.endRow, height), + } + w.percentPair = percentPair + w.snappedPair = snappedPair + w.startupConfig = config + w.Input = config.Input + w.Handler = config.DataHandler + w.Data = config.Data + w.Title = config.Title + w.MinWidth = config.MinWidth + w.MinHeight = config.MinHeight + if w.MinWidth > len(config.Title) { + w.Width = w.MinWidth + } else { + if w.startupConfig.HasBorder { + w.Width = len(config.Title) + 2 + } else { + w.Width = len(config.Title) + } + } + if config.HasBorder { + if config.MinHeight > 5 { + w.Height = config.MinHeight + } else { + w.Height = 5 + } + } + w.Next = config.Next + w.Finale = config.Finale + w.FinaleData = config.FinaleData + return nil +} + +func getBufForMovingCursorTo(row, col int) string { + return fmt.Sprintf("\033[%d;%dH", row, col) +} + +func (w *widget) moveToNextLine() string { + w.row++ + return getBufForMovingCursorTo(w.row, w.col) +} + +func (w *widget) Draw() (widgetDraw, error) { + w.row = w.snappedPair.startRow + w.col = w.snappedPair.startCol + var buf string + title := w.startupConfig.Title + buf += getBufForMovingCursorTo(w.row, w.col) + if w.startupConfig.HasBorder { + buf += strings.Repeat("-", w.Width) + buf += w.moveToNextLine() + emptyLen := w.Width - len(title) - 2 + firstHalf := (emptyLen) / 2 + buf += fmt.Sprintf("|%s%s%s|", + strings.Repeat(" ", firstHalf), + title, strings.Repeat(" ", + emptyLen-firstHalf)) + buf += w.moveToNextLine() + buf += strings.Repeat("-", w.Width) + buf += w.moveToNextLine() + buf += fmt.Sprintf("|%s%s|", string(*w.Input), strings.Repeat(" ", w.Width-2-len(*w.Input))) + buf += w.moveToNextLine() + buf += strings.Repeat("-", w.Width) + w.col++ + w.row-- + buf += getBufForMovingCursorTo(w.row, w.col) + } else { + buf += fmt.Sprintf("%s", title) + buf += w.moveToNextLine() + buf += strings.Repeat("-", w.Width) + buf += w.moveToNextLine() + buf += fmt.Sprintf("%s%s", string(*w.Input), strings.Repeat(" ", len(title)-len(*w.Input))) + buf += getBufForMovingCursorTo(w.row, w.col) + } + return widgetDraw{ + Buf: buf, + Row: w.row, + Col: w.col, + }, nil +} + +func (w *widget) Clear() string { + var buf string + w.row = w.snappedPair.startRow + w.col = w.snappedPair.startCol + title := w.startupConfig.Title + buf += getBufForMovingCursorTo(w.row, w.col) + if w.startupConfig.HasBorder { + buf += strings.Repeat(" ", w.Width) + buf += w.moveToNextLine() + buf += strings.Repeat(" ", w.Width) + buf += w.moveToNextLine() + buf += strings.Repeat(" ", w.Width) + buf += w.moveToNextLine() + maxClearInpLen := len(*w.Input) + 2 + if w.Width > maxClearInpLen { + maxClearInpLen = w.Width + } + buf += fmt.Sprintf("%s", strings.Repeat(" ", maxClearInpLen)) + buf += w.moveToNextLine() + buf += strings.Repeat(" ", w.Width) + w.row++ + w.col-- + buf += getBufForMovingCursorTo(0, 0) + } else { + buf += fmt.Sprintf("%s", strings.Repeat(" ", len(title))) + buf += w.moveToNextLine() + buf += strings.Repeat(" ", w.Width) + buf += w.moveToNextLine() + maxClearInpLen := len(*w.Input) + if w.Width > maxClearInpLen { + maxClearInpLen = w.Width + } + buf += fmt.Sprintf("%s", strings.Repeat(" ", maxClearInpLen)) + buf += getBufForMovingCursorTo(0, 0) + } + return buf +}