From 7e0d1ef90b9f97d7e809319715437d914c39f3ab Mon Sep 17 00:00:00 2001 From: zervo Date: Mon, 23 Jun 2025 22:07:37 +0200 Subject: [PATCH] Initial commit --- cmd/bytesock/main.go | 13 ++++++ go.mod | 30 +++++++++++++ go.sum | 47 ++++++++++++++++++++ internal/conn/websocket.go | 19 ++++++++ internal/types/conn_type.go | 17 ++++++++ internal/types/input_mode.go | 17 ++++++++ internal/types/mode.go | 17 ++++++++ internal/ui/model.go | 33 ++++++++++++++ internal/ui/run.go | 11 +++++ internal/ui/style.go | 65 +++++++++++++++++++++++++++ internal/ui/update.go | 38 ++++++++++++++++ internal/ui/view.go | 85 ++++++++++++++++++++++++++++++++++++ 12 files changed, 392 insertions(+) create mode 100644 cmd/bytesock/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/conn/websocket.go create mode 100644 internal/types/conn_type.go create mode 100644 internal/types/input_mode.go create mode 100644 internal/types/mode.go create mode 100644 internal/ui/model.go create mode 100644 internal/ui/run.go create mode 100644 internal/ui/style.go create mode 100644 internal/ui/update.go create mode 100644 internal/ui/view.go diff --git a/cmd/bytesock/main.go b/cmd/bytesock/main.go new file mode 100644 index 0000000..400dfb1 --- /dev/null +++ b/cmd/bytesock/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "git.zervo.org/zervo/bytesock/internal/ui" +) + +func main() { + if err := ui.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..256d7b9 --- /dev/null +++ b/go.mod @@ -0,0 +1,30 @@ +module git.zervo.org/zervo/bytesock + +go 1.24.4 + +require ( + github.com/charmbracelet/bubbletea v1.3.5 + github.com/charmbracelet/lipgloss v1.1.0 + github.com/coder/websocket v1.8.13 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9b3559f --- /dev/null +++ b/go.sum @@ -0,0 +1,47 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= +github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= +github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/internal/conn/websocket.go b/internal/conn/websocket.go new file mode 100644 index 0000000..54dbe19 --- /dev/null +++ b/internal/conn/websocket.go @@ -0,0 +1,19 @@ +package conn + +import ( + "context" + + "github.com/coder/websocket" +) + +type WebSocketConn struct { + c *websocket.Conn +} + +func DialWebSocket(ctx context.Context, url string) (*WebSocketConn, error) { + c, _, err := websocket.Dial(ctx, url, nil) + if err != nil { + return nil, err + } + return &WebSocketConn{c: c}, nil +} diff --git a/internal/types/conn_type.go b/internal/types/conn_type.go new file mode 100644 index 0000000..1ae8395 --- /dev/null +++ b/internal/types/conn_type.go @@ -0,0 +1,17 @@ +package types + +type ConnType uint + +const ( + CONN_WS ConnType = iota + CONN_TCP + CONN_UNIX +) + +func (c ConnType) ToString() string { + return []string{ + "WebSocket", + "TCP", + "Unix", + }[c] +} diff --git a/internal/types/input_mode.go b/internal/types/input_mode.go new file mode 100644 index 0000000..295b440 --- /dev/null +++ b/internal/types/input_mode.go @@ -0,0 +1,17 @@ +package types + +type InputMode uint + +const ( + INPUT_MESSAGE InputMode = iota + INPUT_INMODE + INPUT_OUTMODE +) + +func (m InputMode) ToString() string { + return []string{ + "Message", + "InMode", + "OutMode", + }[m] +} diff --git a/internal/types/mode.go b/internal/types/mode.go new file mode 100644 index 0000000..c9b6795 --- /dev/null +++ b/internal/types/mode.go @@ -0,0 +1,17 @@ +package types + +type MessageMode uint + +const ( + MODE_HEX_BINARY MessageMode = iota + MODE_UTF8 + MODE_ASCII +) + +func (m MessageMode) ToString() string { + return []string{ + "HexBinary", + "UTF-8", + "ASCII", + }[m] +} diff --git a/internal/ui/model.go b/internal/ui/model.go new file mode 100644 index 0000000..6e56344 --- /dev/null +++ b/internal/ui/model.go @@ -0,0 +1,33 @@ +package ui + +import ( + "git.zervo.org/zervo/bytesock/internal/types" + tea "github.com/charmbracelet/bubbletea" +) + +type model struct { + input string + inputMode types.InputMode + logs []string + width int + height int + omode types.MessageMode + imode types.MessageMode + conn types.ConnType + address string +} + +func initialModel() model { + return model{ + input: "", + logs: []string{msgInfoStyle.Render("Welcome to ByteSock! Use Ctrl+H for help.")}, + omode: types.MODE_HEX_BINARY, + imode: types.MODE_HEX_BINARY, + conn: types.CONN_WS, + address: "ws://localhost:8080", + } +} + +func (m model) Init() tea.Cmd { + return nil +} diff --git a/internal/ui/run.go b/internal/ui/run.go new file mode 100644 index 0000000..e05eefe --- /dev/null +++ b/internal/ui/run.go @@ -0,0 +1,11 @@ +package ui + +import ( + tea "github.com/charmbracelet/bubbletea" +) + +func Run() error { + p := tea.NewProgram(initialModel(), tea.WithAltScreen()) + _, err := p.Run() + return err +} diff --git a/internal/ui/style.go b/internal/ui/style.go new file mode 100644 index 0000000..5d2a64c --- /dev/null +++ b/internal/ui/style.go @@ -0,0 +1,65 @@ +package ui + +import "github.com/charmbracelet/lipgloss" + +const ( + COLOR_BORDER = COLOR_DARK_GRAY //lipgloss.Color("#7D56F4") + COLOR_GREEN = lipgloss.Color("#00ff87") + COLOR_YELLOW = lipgloss.Color("#fff95b") + COLOR_CYAN = lipgloss.Color("#5bfff9") + COLOR_DARK_GRAY = lipgloss.Color("#555") + COLOR_GRAY = lipgloss.Color("#aaa") + COLOR_LIGHT_GRAY = lipgloss.Color("#cccccc") + COLOR_WHITE = lipgloss.Color("#fefefe") + COLOR_RED = lipgloss.Color("#ff0000") + COLOR_ORANGE = lipgloss.Color("#f58216") + COLOR_MAGENTA = lipgloss.Color("#c000c0") +) + +var ( + boxStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(COLOR_BORDER). + Padding(0, 1) + + headerBoxStyle = lipgloss.NewStyle(). + Foreground(COLOR_CYAN). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(COLOR_BORDER). + Padding(0, 1) + + titleStyle = lipgloss.NewStyle(). + Foreground(COLOR_GREEN). + Bold(true) + + modeStyle = lipgloss.NewStyle(). + Foreground(COLOR_YELLOW). + PaddingRight(1) + + rulesetStyle = lipgloss.NewStyle(). + Foreground(COLOR_ORANGE) + + addressStyle = lipgloss.NewStyle(). + Foreground(COLOR_MAGENTA). + Bold(true). + PaddingLeft(1) + + inputStyle = lipgloss.NewStyle(). + Foreground(COLOR_WHITE). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(COLOR_BORDER). + Padding(0, 1) + + logStyle = lipgloss.NewStyle(). + Foreground(COLOR_WHITE). + Padding(0, 1) + + msgInfoStyle = lipgloss.NewStyle(). + Foreground(COLOR_CYAN) + + msgErrorStyle = lipgloss.NewStyle(). + Foreground(COLOR_RED) + + msgSuccessStyle = lipgloss.NewStyle(). + Foreground(COLOR_GREEN) +) diff --git a/internal/ui/update.go b/internal/ui/update.go new file mode 100644 index 0000000..6efc518 --- /dev/null +++ b/internal/ui/update.go @@ -0,0 +1,38 @@ +package ui + +import ( + tea "github.com/charmbracelet/bubbletea" +) + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + + case tea.KeyMsg: + switch msg.Type { + case tea.KeyEnter: + m.logs = append(m.logs, "-> "+m.input) + m.input = "" + case tea.KeyBackspace: + if len(m.input) > 0 { + m.input = m.input[:len(m.input)-1] + } + case tea.KeyCtrlH: + msg := msgInfoStyle.Render( + "\nThe following actions are available:" + + "\n* Ctrl+H : Help" + + "\n* Ctrl+C : Exit" + + "\n* Ctrl+F : Command") + m.logs = append(m.logs, msg) + case tea.KeyCtrlC: + return m, tea.Quit + default: + m.input += msg.String() + } + + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + } + + return m, nil +} diff --git a/internal/ui/view.go b/internal/ui/view.go new file mode 100644 index 0000000..080586f --- /dev/null +++ b/internal/ui/view.go @@ -0,0 +1,85 @@ +package ui + +import ( + "strings" + + "github.com/charmbracelet/lipgloss" +) + +func (m model) View() string { + // Title + title := titleStyle.Render(" ByteSock v1.0.0 ") + + // Status header content + status := lipgloss.JoinHorizontal(lipgloss.Top, + modeStyle.Render(" [OutMode: "+m.omode.ToString()+"]"), + modeStyle.Render("[InMode: "+m.imode.ToString()+"]"), + modeStyle.Render("[Type: "+m.conn.ToString()+"]"), + rulesetStyle.Render(""), + addressStyle.Render(m.address), + ) + + // Padding + spacer := m.width - lipgloss.Width(title) - lipgloss.Width(status) - 4 + if spacer < 0 { + spacer = 0 + } + usableWidth := m.width - 2 + if usableWidth < 0 { + usableWidth = 0 + } + + // Status header layout + headerLine := lipgloss.JoinHorizontal(lipgloss.Top, + title, + strings.Repeat("─", spacer), + status, + ) + headerBox := headerBoxStyle.Width(usableWidth).Render(headerLine) + + // Input field box + inputBox := inputStyle.Width(usableWidth).Render("> " + m.input) + + // Calculate message log height + logHeight := m.height - lipgloss.Height(headerBox) - lipgloss.Height(inputBox) - 2 + if logHeight < 3 { + logHeight = 3 + } + renderedHeight := logHeight - 2 + if renderedHeight < 0 { + renderedHeight = 0 + } + + // Extract lines to render + var renderedLines []string + lineCount := 0 + + for i := len(m.logs) - 1; i >= 0; i-- { + localLines := strings.Split(m.logs[i], "\n") + + localLineCount := len(localLines) + if lineCount+localLineCount > renderedHeight { + startIndex := localLineCount - (renderedHeight - lineCount) + localLines = localLines[startIndex:] + renderedLines = append(localLines, renderedLines...) + break + } + + lineCount += localLineCount + renderedLines = append(localLines, renderedLines...) + } + + // Message log box + logContent := strings.Join(renderedLines, "\n") + logBox := boxStyle. + Height(logHeight). + Width(usableWidth). + Render(logStyle.Render(logContent)) + + // Final layout + return lipgloss.JoinVertical(lipgloss.Left, + headerBox, + logBox, + inputBox, + ) +}