diff options
Diffstat (limited to 'internal/client/ui')
| -rw-r--r-- | internal/client/ui/core/core.go | 187 | ||||
| -rw-r--r-- | internal/client/ui/tosscreen/tosscreen.go | 332 |
2 files changed, 475 insertions, 44 deletions
diff --git a/internal/client/ui/core/core.go b/internal/client/ui/core/core.go index 8ff7810..61810cd 100644 --- a/internal/client/ui/core/core.go +++ b/internal/client/ui/core/core.go @@ -33,6 +33,7 @@ import ( "github.com/kyren223/eko/internal/client/ui/core/state" "github.com/kyren223/eko/internal/client/ui/core/usersettings" "github.com/kyren223/eko/internal/client/ui/loadscreen" + "github.com/kyren223/eko/internal/client/ui/tosscreen" "github.com/kyren223/eko/internal/client/ui/viminput" "github.com/kyren223/eko/internal/data" "github.com/kyren223/eko/internal/packet" @@ -40,12 +41,12 @@ import ( "github.com/kyren223/eko/pkg/snowflake" ) -var ( - connectingToServer = "Connecting to server.." - connectionFailed = "Connection failed - retrying in %d sec..." - connectionTimeout = 5 * time.Second - initialTimeout = 3750 * time.Millisecond - timerInterval = 50 * time.Millisecond +const ( + ConnectingToServer = "Connecting to server.." + ConnectionFailed = "Connection failed - retrying in %d sec..." + ConnectionTimeout = 5 * time.Second + InitialTimeout = 3750 * time.Millisecond + TimerInterval = 50 * time.Millisecond ) const ( @@ -62,14 +63,25 @@ const ( FocusMax ) +type State int + +const ( + Disconnected State = iota + Connected + ConnectedReceivedTos + ConnectedAcceptedTos + Authenticated +) + type Model struct { name string privKey ed25519.PrivateKey - loading loadscreen.Model - timer timer.Model - timeout time.Duration - connected bool + loading loadscreen.Model + tos *tosscreen.Model + timer timer.Model + timeout time.Duration + state State helpPopup *HelpPopup userSettingsPopup *usersettings.Model @@ -94,10 +106,11 @@ func New(privKey ed25519.PrivateKey, name string) Model { m := Model{ name: name, privKey: privKey, - loading: loadscreen.New(connectingToServer), - timer: newTimer(initialTimeout), - timeout: initialTimeout, - connected: false, + loading: loadscreen.New(ConnectingToServer), + tos: nil, + timer: newTimer(InitialTimeout), + timeout: InitialTimeout, + state: Disconnected, helpPopup: nil, userSettingsPopup: nil, networkCreationPopup: nil, @@ -108,6 +121,7 @@ func New(privKey ed25519.PrivateKey, name string) Model { banReasonPopup: nil, banViewPopup: nil, signalAddPopup: nil, + profilePopup: nil, networkList: networklist.New(), signalList: signallist.New(), frequencyList: frequencylist.New(), @@ -121,12 +135,29 @@ func New(privKey ed25519.PrivateKey, name string) Model { } func (m Model) Init() tea.Cmd { - return tea.Batch(gateway.Connect(m.privKey, connectionTimeout), m.loading.Init()) + return tea.Batch(gateway.Connect(ConnectionTimeout), m.loading.Init()) } func (m Model) View() string { - if !m.connected { + switch m.state { + case Disconnected: return m.loading.View() + + case Connected: + return m.loading.View() + case ConnectedReceivedTos: + assert.NotNil(m.tos, "TOS is expected to not be nil if received TOS already") + return m.tos.View() + + case ConnectedAcceptedTos: + // TODO: auth + return m.loading.View() + + case Authenticated: + // Continue + + default: + panic(fmt.Sprintf("unexpected core.State: %#v", m.state)) } if m.HasPopup() { @@ -188,36 +219,52 @@ func (m Model) View() string { } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - if m.connected { + switch m.state { + + case Disconnected: + cmd := m.updateDisconnected(msg) + return m, cmd + + case Connected: + fallthrough + case ConnectedReceivedTos: + fallthrough + case ConnectedAcceptedTos: cmd := m.updateConnected(msg) return m, cmd - } else { - cmd := m.updateNotConnected(msg) + + case Authenticated: + cmd := m.updateAuthenticated(msg) return m, cmd + + default: + panic(fmt.Sprintf("unexpected core.State: %#v", m.state)) } } -func (m *Model) updateNotConnected(msg tea.Msg) tea.Cmd { +func (m *Model) updateDisconnected(msg tea.Msg) tea.Cmd { switch msg := msg.(type) { case gateway.ConnectionEstablished: - state.UserID = (*snowflake.ID)(&msg) - m.connected = true - m.timeout = initialTimeout - - var setName tea.Cmd - if m.name != "" { - setName = gateway.Send(&packet.SetUserData{ - Data: nil, - User: &data.User{ - Name: m.name, - Description: "", - IsPublicDM: true, - }, - }) - m.name = "" - } + m.state = Connected + m.timeout = InitialTimeout - return tea.Batch(m.timer.Stop(), setName) + // TODO: + // // state.UserID = (*snowflake.ID)(&msg) + // var setName tea.Cmd + // if m.name != "" { + // setName = gateway.Send(&packet.SetUserData{ + // Data: nil, + // User: &data.User{ + // Name: m.name, + // Description: "", + // IsPublicDM: true, + // }, + // }) + // m.name = "" + // } + // + // return tea.Batch(m.timer.Stop(), setName) + return m.timer.Stop() case gateway.ConnectionFailed: log.Println("failed to connect:", msg) @@ -227,8 +274,8 @@ func (m *Model) updateNotConnected(msg tea.Msg) tea.Cmd { case timer.TimeoutMsg: m.timeout = min(m.timeout*2, time.Minute) - m.loading.SetContent(connectingToServer) - return gateway.Connect(m.privKey, connectionTimeout) + m.loading.SetContent(ConnectingToServer) + return gateway.Connect(ConnectionTimeout) case timer.StartStopMsg: var cmd tea.Cmd @@ -252,6 +299,54 @@ func (m *Model) updateNotConnected(msg tea.Msg) tea.Cmd { } func (m *Model) updateConnected(message tea.Msg) tea.Cmd { + switch msg := message.(type) { + + case gateway.ConnectionLost: + m.state = Disconnected + m.timeout = InitialTimeout + return tea.Batch(gateway.Connect(ConnectionTimeout), m.loading.Init()) + + case *packet.TosInfo: + m.state = ConnectedReceivedTos + combined := msg.Tos + "\n" + msg.PrivacyPolicy + tos := tosscreen.New(combined) + m.tos = &tos + // var setName tea.Cmd + // if m.name != "" { + // setName = gateway.Send(&packet.SetUserData{ + // Data: nil, + // User: &data.User{ + // Name: m.name, + // Description: "", + // IsPublicDM: true, + // }, + // }) + // m.name = "" + // } + // + // return tea.Batch(m.timer.Stop(), setName) + + case tea.KeyMsg: + switch msg.String() { + case "enter": + if m.state == ConnectedReceivedTos { + // TODO: + m.state = ConnectedAcceptedTos + } + } + + } + + if m.state == ConnectedReceivedTos { + tos, cmd := m.tos.Update(message) + m.tos = &tos + return cmd + } + + return nil +} + +func (m *Model) updateAuthenticated(message tea.Msg) tea.Cmd { totalWidth := max(ui.Width, ui.MinWidth) totalWidth -= NetworkWidth sidebarWidth := int(math.Round(float64(totalWidth) * SidebarPercentage)) @@ -274,9 +369,9 @@ func (m *Model) updateConnected(message tea.Msg) tea.Cmd { case gateway.ConnectionLost: state.UserID = nil - m.connected = false - m.timeout = initialTimeout - return tea.Batch(gateway.Connect(m.privKey, connectionTimeout), m.loading.Init()) + m.state = Disconnected + m.timeout = InitialTimeout + return tea.Batch(gateway.Connect(ConnectionTimeout), m.loading.Init()) case *packet.Error: err := "new connection from another location, closing this one" @@ -619,11 +714,11 @@ func (m *Model) updateConnected(message tea.Msg) tea.Cmd { func (m *Model) updateLoadScreenContent() { seconds := m.timer.Timeout.Round(time.Second) / time.Second - m.loading.SetContent(fmt.Sprintf(connectionFailed, seconds)) + m.loading.SetContent(fmt.Sprintf(ConnectionFailed, seconds)) } func newTimer(timeout time.Duration) timer.Model { - return timer.NewWithInterval(timeout.Truncate(time.Second)+(time.Second/2), timerInterval) + return timer.NewWithInterval(timeout.Truncate(time.Second)+(time.Second/2), TimerInterval) } func (m *Model) move(direction int) { @@ -814,3 +909,7 @@ func getSignalNotification(signal snowflake.ID) int { return pings } + +func (m Model) ViewTos() string { + return "TODO TOS" +} diff --git a/internal/client/ui/tosscreen/tosscreen.go b/internal/client/ui/tosscreen/tosscreen.go new file mode 100644 index 0000000..317a724 --- /dev/null +++ b/internal/client/ui/tosscreen/tosscreen.go @@ -0,0 +1,332 @@ +package tosscreen + +import ( + "strings" + + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" + "github.com/charmbracelet/glamour/ansi" + "github.com/charmbracelet/lipgloss" + + // "github.com/charmbracelet/glamour" + "github.com/kyren223/eko/internal/client/ui" + "github.com/kyren223/eko/internal/client/ui/colors" + "github.com/kyren223/eko/pkg/assert" +) + +const MarginPercentage = 0.25 + +var style = func() lipgloss.Style { + return lipgloss.NewStyle(). + Border(lipgloss.ThickBorder()). + BorderBackground(colors.Background). + Background(colors.Background). + MarginBackground(colors.BackgroundDimmer). + Padding(0, 3). + Margin(1, int(float32(ui.Width)*MarginPercentage), 0) +} + +type Model struct { + content string + vp viewport.Model +} + +func New(content string) Model { + m := Model{ + content: content, + vp: viewport.New(ui.Width, ui.Height), + } + m.SetContent(content) + + return m +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) View() string { + footer := "-- Accept | ^C Decline | ↑↓ or J/K Scroll | ^D/^U PgDn/PgUp --" + + fWidth := lipgloss.Width(footer) + paddingLeft := (ui.Width - fWidth) / 2 + paddingRight := ui.Width - fWidth - paddingLeft + footerStyle := lipgloss.NewStyle(). + Padding(0, paddingLeft, 0, paddingRight). + Background(colors.BackgroundDimmer). + MarginBackground(colors.BackgroundDimmer) + footer = footerStyle.Render(footer) + + view := m.vp.View() + + return lipgloss.JoinVertical(lipgloss.Center, view, footer) +} + +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + var cmd tea.Cmd + + if m.vp.Width != ui.Width { + m.vp.Width = ui.Width + m.updateContent() + } + if m.vp.Height != ui.Height-1 { + m.vp.Height = ui.Height - 1 + m.updateContent() + } + + m.vp.Style = style() + m.vp, cmd = m.vp.Update(msg) + + return m, cmd +} + +func (m *Model) updateContent() { + style := style() + margin := style.GetMarginLeft() + style.GetMarginRight() + padding := style.GetPaddingLeft() + style.GetPaddingRight() + border := 2 + lineWidth := ui.Width - margin - padding - border + + r, _ := glamour.NewTermRenderer( + glamour.WithStyles(MdStyle()), + glamour.WithWordWrap(lineWidth), + ) + content := m.content + content = strings.ReplaceAll(content, "@", "") // HACK: workaround to remove mailto links + content, err := r.Render(content) + assert.NoError(err, "this should never error") + content = strings.ReplaceAll(content, "", "@") // HACK: workaround to remove mailto links + m.vp.SetContent(content) +} + +func (m *Model) SetContent(content string) { + m.content = content + m.updateContent() +} + +func MdStyle() ansi.StyleConfig { + mdStyle := ansi.StyleConfig{ + Document: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + BlockPrefix: "\n", + BlockSuffix: "\n", + Color: stringPtr(colors.White), + BackgroundColor: stringPtr(colors.Background), + }, + Margin: uintPtr(defaultMargin), + }, + BlockQuote: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{}, + Indent: uintPtr(1), + IndentToken: stringPtr("│ "), + }, + List: ansi.StyleList{ + LevelIndent: defaultListIndent, + }, + Heading: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + BlockSuffix: "\n", + Color: stringPtr(colors.LightBlue), + Bold: boolPtr(true), + }, + }, + H1: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: " ", + Suffix: " ", + Color: stringPtr(colors.Black), + BackgroundColor: stringPtr(colors.LightBlue), + Bold: boolPtr(true), + }, + }, + H2: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "## ", + }, + }, + H3: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "### ", + }, + }, + H4: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "#### ", + }, + }, + H5: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "##### ", + }, + }, + H6: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "###### ", + Bold: boolPtr(false), + }, + }, + Strikethrough: ansi.StylePrimitive{ + CrossedOut: boolPtr(true), + }, + Emph: ansi.StylePrimitive{ + Italic: boolPtr(true), + }, + Strong: ansi.StylePrimitive{ + Color: stringPtr(colors.Orange), + Bold: boolPtr(true), + }, + HorizontalRule: ansi.StylePrimitive{ + Color: stringPtr(colors.Gray), + Format: "\n--------\n", + }, + Item: ansi.StylePrimitive{ + BlockPrefix: " ", + }, + Enumeration: ansi.StylePrimitive{ + BlockPrefix: ". ", + }, + Task: ansi.StyleTask{ + StylePrimitive: ansi.StylePrimitive{}, + Ticked: "[✓] ", + Unticked: "[ ] ", + }, + Link: ansi.StylePrimitive{ + + // Format: "Bozo", + }, + LinkText: ansi.StylePrimitive{ + Color: stringPtr(colors.Gold), + Bold: boolPtr(true), + Conceal: boolPtr(true), + Faint: boolPtr(true), + }, + Image: ansi.StylePrimitive{ + Color: stringPtr(colors.Purple), + Underline: boolPtr(true), + }, + ImageText: ansi.StylePrimitive{ + Color: stringPtr(colors.LightGray), + Format: "Image: {{.text}} →", + }, + Code: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: " ", + Suffix: " ", + Color: stringPtr(colors.Purple), + BackgroundColor: stringPtr(colors.BackgroundHighlight), + }, + }, + CodeBlock: ansi.StyleCodeBlock{ + StyleBlock: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Color: stringPtr(colors.LightGray), + }, + Margin: uintPtr(defaultMargin), + }, + Chroma: &ansi.Chroma{ + Text: ansi.StylePrimitive{ + Color: stringPtr(colors.White), + }, + Error: ansi.StylePrimitive{ + Color: stringPtr(colors.White), + BackgroundColor: stringPtr(colors.Red), + }, + Comment: ansi.StylePrimitive{ + Color: stringPtr(colors.Gray), + }, + CommentPreproc: ansi.StylePrimitive{ + Color: stringPtr(colors.Orange), + }, + Keyword: ansi.StylePrimitive{ + Color: stringPtr(colors.Red), + }, + KeywordReserved: ansi.StylePrimitive{ + Color: stringPtr(colors.Red), + }, + KeywordNamespace: ansi.StylePrimitive{ + Color: stringPtr(colors.Red), + }, + KeywordType: ansi.StylePrimitive{ + Color: stringPtr(colors.Turquoise), + }, + Operator: ansi.StylePrimitive{ + Color: stringPtr(colors.Red), + }, + Punctuation: ansi.StylePrimitive{ + Color: stringPtr(colors.White), + }, + Name: ansi.StylePrimitive{ + Color: stringPtr(colors.Purple), + }, + NameBuiltin: ansi.StylePrimitive{ + Color: stringPtr(colors.Red), + }, + NameTag: ansi.StylePrimitive{ + Color: stringPtr(colors.Purple), + }, + NameAttribute: ansi.StylePrimitive{ + Color: stringPtr(colors.LightBlue), + }, + NameClass: ansi.StylePrimitive{ + Color: stringPtr(colors.White), + Underline: boolPtr(true), + Bold: boolPtr(true), + }, + NameDecorator: ansi.StylePrimitive{ + Color: stringPtr(colors.Gold), + }, + NameFunction: ansi.StylePrimitive{ + Color: stringPtr(colors.LightBlue), + }, + LiteralNumber: ansi.StylePrimitive{ + Color: stringPtr(colors.Purple), + }, + LiteralString: ansi.StylePrimitive{ + Color: stringPtr(colors.Orange), + }, + LiteralStringEscape: ansi.StylePrimitive{ + Color: stringPtr(colors.Purple), + }, + GenericDeleted: ansi.StylePrimitive{ + Color: stringPtr(colors.Red), + }, + GenericEmph: ansi.StylePrimitive{ + Italic: boolPtr(true), + }, + GenericInserted: ansi.StylePrimitive{ + Color: stringPtr(colors.Turquoise), + }, + GenericStrong: ansi.StylePrimitive{ + Bold: boolPtr(true), + }, + GenericSubheading: ansi.StylePrimitive{ + Color: stringPtr(colors.LightGray), + }, + Background: ansi.StylePrimitive{ + BackgroundColor: stringPtr(colors.BackgroundDim), + }, + }, + }, + Table: ansi.StyleTable{ + StyleBlock: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{}, + }, + }, + DefinitionDescription: ansi.StylePrimitive{ + BlockPrefix: "\n🠶 ", + }, + } + + return mdStyle +} + +const ( + defaultListIndent = 2 + defaultListLevelIndent = 4 + defaultMargin = 0 +) + +func stringPtr(c lipgloss.Color) *string { return (*string)(&c) } +func boolPtr(b bool) *bool { return &b } +func uintPtr(u uint) *uint { return &u } |
