diff --git a/constants.go b/constants.go index 0880297..7ba0acc 100644 --- a/constants.go +++ b/constants.go @@ -16,6 +16,20 @@ package main +const welcomeMsg = ` + + /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ + /$$__ $$ /$$__ $$ /$$__ $$ /$$_____/ +| $$ \ $$| $$ \ $$| $$ \__/| $$ +| $$ | $$| $$ | $$| $$ | $$ +| $$$$$$$| $$$$$$/| $$ | $$$$$$$ + \____ $$ \______/ |__/ \_______/ + /$$ \ $$ +| $$$$$$/ + \______/ + +` + /* --- IRC Ports --- */ const ( INSECURE_PORT = 6667 diff --git a/go.mod b/go.mod index 5ab1033..2104c84 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/illusionman1212/gorc go 1.16 + +require ( + github.com/charmbracelet/bubbles v0.9.0 + github.com/charmbracelet/bubbletea v0.17.0 // indirect + github.com/charmbracelet/lipgloss v0.4.0 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..685946a --- /dev/null +++ b/go.sum @@ -0,0 +1,52 @@ +github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= +github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/charmbracelet/bubbles v0.9.0 h1:lqJ8FXwoLceQF2J0A+dWo1Cuu1dNyjbW4Opgdi2vkhw= +github.com/charmbracelet/bubbles v0.9.0/go.mod h1:NWT/c+0rYEnYChz5qCyX4Lj6fDw9gGToh9EFJPajghU= +github.com/charmbracelet/bubbletea v0.14.1/go.mod h1:b5lOf5mLjMg1tRn1HVla54guZB+jvsyV0yYAQja95zE= +github.com/charmbracelet/bubbletea v0.17.0 h1:ABmd01l6K7Q8lSQxF7yG8yWayz5gXWSt3JfB/X1eYP4= +github.com/charmbracelet/bubbletea v0.17.0/go.mod h1:YTZSs2p3odhwYZdhqJheYHVUjU37c9OLgS85kw6NGQY= +github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v0.3.0/go.mod h1:VkhdBS2eNAmRkTwRKLJCFhCOVkjntMusBDxv7TXahuk= +github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g= +github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM= +github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +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.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= +github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.8.1 h1:9q230czSP3DHVpkaPDXGp0TOfAwyjyYwXlUCQxQSaBk= +github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0= +github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= +github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0= +golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/main.go b/main.go index 18f4333..123b538 100644 --- a/main.go +++ b/main.go @@ -17,24 +17,31 @@ package main import ( - "bufio" "log" + + tea "github.com/charmbracelet/bubbletea" ) func main() { - // create new client with the provided host and port - client := NewClient("irc.libera.chat", "6697") - defer client.conn.Close() - go client.Run() - - // main loop - r := bufio.NewReaderSize(client.conn, 512) - for { - msg, err := r.ReadString('\n') - if err != nil { - log.Print(err) - } - - client.receive <- msg + if err := tea.NewProgram(initialState(), tea.WithAltScreen()).Start(); err != nil { + log.Fatal(err) } + + /* + // create new client with the provided host and port + client := NewClient("irc.libera.chat", "6697") + defer client.conn.Close() + go client.Run() + + // main loop + r := bufio.NewReaderSize(client.conn, 512) + for { + msg, err := r.ReadString('\n') + if err != nil { + log.Print(err) + } + + client.receive <- msg + } + */ } diff --git a/state.go b/state.go new file mode 100644 index 0000000..ab130fe --- /dev/null +++ b/state.go @@ -0,0 +1,296 @@ +// gorc project +// Copyright (C) 2021 IllusionMan1212 and contributors +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, see https://www.gnu.org/licenses. + +package main + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "golang.org/x/term" +) + +var ( + border = lipgloss.Border{ + Top: "─", + Bottom: "─", + Left: "│", + Right: "│", + TopLeft: "╭", + TopRight: "╮", + BottomLeft: "╰", + BottomRight: "╯", + } + + mainStyle = lipgloss.NewStyle(). + Width(172). + Height(40) + + dialogStyle = lipgloss.NewStyle(). + Border(border, true). + Align(lipgloss.Center). + Width(50). + Padding(3, 6). + BorderForeground(lipgloss.Color("105")) + + welcomeMsgStyle = lipgloss.NewStyle(). + Padding(0, 5). + MarginBottom(5) + + cursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("105")) + focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("105")) + noStyle = lipgloss.NewStyle() + + focusedButton = lipgloss.NewStyle(). + Background(lipgloss.Color("105")). + Foreground(lipgloss.Color("255")). + MarginTop(1). + Padding(0, 2). + Align(lipgloss.Center). + Render("Connect") + blurredButton = lipgloss.NewStyle(). + Background(lipgloss.Color("#809070")). + Foreground(lipgloss.Color("#EEEEEE")). + MarginTop(1). + Padding(0, 2). + Align(lipgloss.Center). + Render("Connect") + + focusedCheckbox = lipgloss.NewStyle(). + Foreground(lipgloss.Color("105")). + MarginTop(1). + Render("[ ] Enable TLS") + + blurredCheckbox = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#EEEEEE")). + MarginTop(1). + Render("[ ] Enable TLS") + + focusedCheckedCheckbox = lipgloss.NewStyle(). + Foreground(lipgloss.Color("105")). + MarginTop(1). + Render("[x] Enable TLS") + + blurredCheckedCheckbox = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#EEEEEE")). + MarginTop(1). + Render("[x] Enable TLS") +) + +type loginState struct { + focusIndex int + inputs []textinput.Model + + tls bool +} + +// TODO: have a global state that holds a separate client state and separate tui state +type state struct { + host string + port string + + nick string + password string + + windowWidth int + windowHeight int + + login loginState +} + +func initialLoginState() loginState { + ls := loginState{ + inputs: make([]textinput.Model, 5), + } + + var t textinput.Model + for i := range ls.inputs { + t = textinput.NewModel() + t.CursorStyle = cursorStyle + + switch i { + case 0: + t.Placeholder = "Host" + t.Focus() + t.CharLimit = 40 + t.TextStyle = focusedStyle + case 1: + t.Placeholder = "Port" + t.CharLimit = 5 + case 2: + t.Placeholder = "Channel" + t.CharLimit = 32 + case 3: + t.Placeholder = "Nickname" + t.CharLimit = 32 + case 4: + t.Placeholder = "Password" + t.CharLimit = 32 + t.EchoMode = textinput.EchoPassword + t.EchoCharacter = '•' + } + + ls.inputs[i] = t + } + + return ls +} + +func initialState() state { + width, height, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil { + log.Fatal(err) + } + + s := state{ + login: initialLoginState(), + windowWidth: width, + windowHeight: height, + } + + return s +} + +func (s state) Init() tea.Cmd { + return textinput.Blink +} + +func (s state) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c": + return s, tea.Quit + case "tab", "shift+tab", "enter", "up", "down", "space": + str := msg.String() + + // run the newClient cmd when pressing "enter" while focused on the connect button + if str == "enter" && s.login.focusIndex == len(s.login.inputs)+1 { + return s, tea.Quit // TODO: change this to create a newClient and connect using a tea.Cmd + } + + // toggle tls state when pressing "enter" while focused on the checkbox + // TODO: figure out why space isn't working here. all other keys works just fine + if (str == "enter" || str == "space") && s.login.focusIndex == len(s.login.inputs) { + s.login.tls = !s.login.tls + return s, nil + } + + if str == "up" || str == "shift+tab" { + s.login.focusIndex-- + } else { + s.login.focusIndex++ + } + + if s.login.focusIndex > len(s.login.inputs)+1 { + s.login.focusIndex = 0 + } else if s.login.focusIndex < 0 { + s.login.focusIndex = len(s.login.inputs) + 1 + } + + cmds := make([]tea.Cmd, len(s.login.inputs)) + for i := 0; i <= len(s.login.inputs)-1; i++ { + if i == s.login.focusIndex { + cmds[i] = s.login.inputs[i].Focus() + s.login.inputs[i].PromptStyle = focusedStyle + s.login.inputs[i].TextStyle = focusedStyle + continue + } + + s.login.inputs[i].Blur() + s.login.inputs[i].PromptStyle = noStyle + s.login.inputs[i].TextStyle = noStyle + } + + return s, tea.Batch(cmds...) + } + case tea.WindowSizeMsg: + // NOTE: this kinda works. still need to fix it tho + width, height, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil { + log.Print(err) + return s, tea.Quit + } + + s.windowWidth = width + s.windowHeight = height + + mainStyle = mainStyle. + Width(width). + Height(height) + + return s, nil + } + + cmd := s.updateInputs(msg) + + return s, cmd +} + +func (s *state) updateInputs(msg tea.Msg) tea.Cmd { + var cmds = make([]tea.Cmd, len(s.login.inputs)) + + for i := range s.login.inputs { + s.login.inputs[i], cmds[i] = s.login.inputs[i].Update(msg) + } + + return tea.Batch(cmds...) +} + +func (s state) View() string { + var sb strings.Builder + + for i := range s.login.inputs { + sb.WriteString(s.login.inputs[i].View()) + if i < len(s.login.inputs)-1 { + sb.WriteRune('\n') + } + } + + checkbox := &blurredCheckbox + // if the checkbox is focused + if s.login.focusIndex == len(s.login.inputs) { + checkbox = &focusedCheckbox + // if tls is enabled + if s.login.tls { + checkbox = &focusedCheckedCheckbox + } + } + + // if the checkbox is not focused and tls is enabled + if s.login.focusIndex != len(s.login.inputs) && s.login.tls { + checkbox = &blurredCheckedCheckbox + } + + fmt.Fprintf(&sb, "\n%s\n", *checkbox) + + button := &blurredButton + if s.login.focusIndex == len(s.login.inputs)+1 { + button = &focusedButton + } + fmt.Fprintf(&sb, "\n%s\n", *button) + + ui := lipgloss.JoinVertical(0, welcomeMsgStyle.Render(welcomeMsg), dialogStyle.Render(sb.String())) + + final := lipgloss.Place(s.windowWidth, s.windowHeight, lipgloss.Center, lipgloss.Top, ui) + + return mainStyle.Render(final) +}