build(deps): upgrade tview to latest

This commit is contained in:
ayn2op 2026-02-24 20:10:17 -05:00
parent 06ee7fdad1
commit 63fef0b13e
15 changed files with 406 additions and 260 deletions

View file

@ -5,9 +5,9 @@ import (
"fmt"
"log/slog"
"github.com/ayn2op/discordo/internal/app"
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/discordo/internal/logger"
"github.com/ayn2op/discordo/internal/ui/root"
"github.com/diamondburned/arikawa/v3/utils/ws"
)
@ -45,5 +45,5 @@ func Run() error {
return fmt.Errorf("failed to load config: %w", err)
}
return app.New(cfg).Run()
return root.NewView(cfg).Run()
}

2
go.mod
View file

@ -8,7 +8,7 @@ require (
github.com/BurntSushi/toml v1.6.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/andybalholm/brotli v1.2.0
github.com/ayn2op/tview v0.0.0-20260223211434-6859308d597f
github.com/ayn2op/tview v0.0.0-20260225005318-47ca92c515c0
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260221051847-b81b70d1a5cb
github.com/diamondburned/ningen/v3 v3.0.1-0.20250920191746-98fbd92e134d

4
go.sum
View file

@ -14,8 +14,8 @@ github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/ayn2op/tview v0.0.0-20260223211434-6859308d597f h1:ZxDODg1E/RPoTWlXhnhZOKITe1ebBM0Cpy+GiGaa+xk=
github.com/ayn2op/tview v0.0.0-20260223211434-6859308d597f/go.mod h1:lZ8RdOegQWBQafTOasGE7Ps1/Ymy4jmXoPt5vz2QsS0=
github.com/ayn2op/tview v0.0.0-20260225005318-47ca92c515c0 h1:aWW3YTB4CKSrHlzg542uT7gIo812+VvNYIOogu5GAAs=
github.com/ayn2op/tview v0.0.0-20260225005318-47ca92c515c0/go.mod h1:lZ8RdOegQWBQafTOasGE7Ps1/Ymy4jmXoPt5vz2QsS0=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View file

@ -1,117 +0,0 @@
package app
import (
"fmt"
"log/slog"
"os"
"github.com/ayn2op/discordo/internal/clipboard"
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/discordo/internal/consts"
"github.com/ayn2op/discordo/internal/keyring"
"github.com/ayn2op/discordo/internal/ui/chat"
"github.com/ayn2op/discordo/internal/ui/login"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/keybind"
"github.com/gdamore/tcell/v3"
)
type App struct {
inner *tview.Application
chatView *chat.View
cfg *config.Config
}
func New(cfg *config.Config) *App {
tview.Styles = tview.Theme{}
app := &App{
inner: tview.NewApplication(),
cfg: cfg,
}
if err := clipboard.Init(); err != nil {
slog.Error("failed to init clipboard", "err", err)
}
app.inner.SetInputCapture(app.onInputCapture)
return app
}
func (a *App) Run() error {
token := os.Getenv("DISCORDO_TOKEN")
if token == "" {
t, err := keyring.GetToken()
if err != nil {
slog.Info("failed to retrieve token from keyring", "err", err)
}
token = t
}
screen, err := tcell.NewScreen()
if err != nil {
return fmt.Errorf("failed to create screen: %w", err)
}
if err := screen.Init(); err != nil {
return fmt.Errorf("failed to init screen: %w", err)
}
if a.cfg.Mouse {
screen.EnableMouse()
}
screen.SetTitle(consts.Name)
screen.EnablePaste()
screen.EnableFocus()
a.inner.SetScreen(screen)
if token == "" {
loginForm := login.NewForm(a.inner, a.cfg, func(token string) {
if err := a.showChatView(token); err != nil {
slog.Error("failed to show chat view", "err", err)
}
})
a.inner.SetRoot(loginForm)
} else {
if err := a.showChatView(token); err != nil {
return err
}
}
return a.inner.Run()
}
func (a *App) showChatView(token string) error {
a.chatView = chat.NewView(a.inner, a.cfg, a.quit)
if err := a.chatView.OpenState(token); err != nil {
return err
}
a.inner.SetRoot(a.chatView)
return nil
}
func (a *App) quit() {
if a.chatView != nil {
if err := a.chatView.CloseState(); err != nil {
slog.Error("failed to close the session", "err", err)
}
}
a.inner.Stop()
}
func (a *App) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
switch {
case keybind.Matches(event, a.cfg.Keybinds.Suspend.Keybind):
a.suspend()
return nil
case keybind.Matches(event, a.cfg.Keybinds.Quit.Keybind):
a.quit()
return nil
case keybind.Matches(event, keybind.NewKeybind(keybind.WithKeys("ctrl+c"))):
// https://github.com/ayn2op/tview/blob/a64fc48d7654432f71922c8b908280cdb525805c/application.go#L153
return tcell.NewEventKey(tcell.KeyCtrlC, "", tcell.ModNone)
}
return event
}

View file

@ -1,5 +0,0 @@
//go:build !unix
package app
func (a *App) suspend() {}

View file

@ -0,0 +1,101 @@
package chat
import (
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/discordo/internal/ui"
"github.com/ayn2op/discordo/pkg/picker"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/help"
"github.com/ayn2op/tview/keybind"
)
type attachmentItem struct {
label string
open func()
}
type attachmentsPicker struct {
*picker.Picker
cfg *config.Config
chatView *View
items []attachmentItem
}
var _ help.KeyMap = (*attachmentsPicker)(nil)
func newAttachmentsPicker(cfg *config.Config, chatView *View) *attachmentsPicker {
ap := &attachmentsPicker{
Picker: picker.New(),
cfg: cfg,
chatView: chatView,
}
ap.Box = ui.ConfigureBox(tview.NewBox(), &cfg.Theme)
ap.
SetBlurFunc(nil).
SetFocusFunc(nil).
SetBorderSet(cfg.Theme.Border.ActiveSet.BorderSet).
SetBorderStyle(cfg.Theme.Border.ActiveStyle.Style).
SetTitleStyle(cfg.Theme.Title.ActiveStyle.Style).
SetFooterStyle(cfg.Theme.Footer.ActiveStyle.Style)
ap.SetTitle("Attachments")
ap.SetSelectedFunc(ap.onSelected)
ap.SetCancelFunc(ap.close)
ap.SetKeyMap(&picker.KeyMap{
Cancel: cfg.Keybinds.Picker.Cancel.Keybind,
Up: cfg.Keybinds.Picker.Up.Keybind,
Down: cfg.Keybinds.Picker.Down.Keybind,
Top: cfg.Keybinds.Picker.Top.Keybind,
Bottom: cfg.Keybinds.Picker.Bottom.Keybind,
Select: cfg.Keybinds.Picker.Select.Keybind,
})
ap.SetScrollBarVisibility(cfg.Theme.ScrollBar.Visibility.ScrollBarVisibility)
ap.SetScrollBar(tview.NewScrollBar().
SetTrackStyle(cfg.Theme.ScrollBar.TrackStyle.Style).
SetThumbStyle(cfg.Theme.ScrollBar.ThumbStyle.Style).
SetGlyphSet(cfg.Theme.ScrollBar.GlyphSet.GlyphSet))
return ap
}
func (ap *attachmentsPicker) SetItems(items []attachmentItem) {
ap.items = items
ap.ClearItems()
for i, item := range items {
ap.AddItem(picker.Item{
Text: item.label,
FilterText: item.label,
Reference: i,
})
}
ap.Update()
}
func (ap *attachmentsPicker) onSelected(item picker.Item) {
index, ok := item.Reference.(int)
if !ok {
return
}
if index < 0 || index >= len(ap.items) {
return
}
ap.items[index].open()
ap.close()
}
func (ap *attachmentsPicker) close() {
ap.chatView.RemoveLayer(attachmentsListLayerName)
ap.chatView.app.SetFocus(ap.chatView.messagesList)
}
func (ap *attachmentsPicker) ShortHelp() []keybind.Keybind {
cfg := ap.cfg.Keybinds.Picker
return []keybind.Keybind{cfg.Up.Keybind, cfg.Down.Keybind, cfg.Select.Keybind, cfg.Cancel.Keybind}
}
func (ap *attachmentsPicker) FullHelp() [][]keybind.Keybind {
cfg := ap.cfg.Keybinds.Picker
return [][]keybind.Keybind{
{cfg.Up.Keybind, cfg.Down.Keybind, cfg.Top.Keybind, cfg.Bottom.Keybind},
{cfg.Select.Keybind, cfg.Cancel.Keybind},
}
}

View file

@ -55,8 +55,7 @@ func newGuildsTree(cfg *config.Config, chatView *View) *guildsTree {
SetGraphics(cfg.Theme.GuildsTree.Graphics).
SetGraphicsColor(tcell.GetColor(cfg.Theme.GuildsTree.GraphicsColor)).
SetSelectedFunc(gt.onSelected).
SetTitle("Guilds").
SetInputCapture(gt.onInputCapture)
SetTitle("Guilds")
return gt
}
@ -375,7 +374,7 @@ func (gt *guildsTree) collapseParentNode(node *tview.TreeNode) {
})
}
func (gt *guildsTree) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
func (gt *guildsTree) handleInput(event *tcell.EventKey) *tcell.EventKey {
switch {
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.CollapseParentNode.Keybind):
gt.collapseParentNode(gt.GetCurrentNode())
@ -404,6 +403,14 @@ func (gt *guildsTree) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
return nil
}
func (gt *guildsTree) InputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
event = gt.handleInput(event)
if event == nil {
return
}
gt.TreeView.InputHandler(event, setFocus)
}
func (gt *guildsTree) yankID() {
node := gt.GetCurrentNode()
if node == nil {

View file

@ -66,7 +66,6 @@ func newMessageInput(cfg *config.Config, chatView *View) *messageInput {
mentionsList: newMentionsList(cfg),
}
mi.Box = ui.ConfigureBox(mi.Box, &cfg.Theme)
mi.SetInputCapture(mi.onInputCapture)
mi.
SetPlaceholder(tview.NewLine(tview.NewSegment("Select a channel to start chatting", tcell.StyleDefault.Dim(true)))).
SetClipboard(
@ -93,7 +92,7 @@ func (mi *messageInput) stopTypingTimer() {
}
}
func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
func (mi *messageInput) handleInput(event *tcell.EventKey) *tcell.EventKey {
switch {
case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Paste.Keybind):
mi.paste()
@ -144,19 +143,18 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
if mi.cfg.AutocompleteLimit > 0 {
if mi.chatView.GetVisible(mentionsListLayerName) {
handler := mi.mentionsList.InputHandler()
switch {
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Up.Keybind):
handler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil)
mi.mentionsList.InputHandler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil)
return nil
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Down.Keybind):
handler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone), nil)
mi.mentionsList.InputHandler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone), nil)
return nil
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Top.Keybind):
handler(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone), nil)
mi.mentionsList.InputHandler(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone), nil)
return nil
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Bottom.Keybind):
handler(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone), nil)
mi.mentionsList.InputHandler(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone), nil)
return nil
}
}
@ -167,6 +165,14 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
return event
}
func (mi *messageInput) InputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
event = mi.handleInput(event)
if event == nil {
return
}
mi.TextArea.InputHandler(event, setFocus)
}
func (mi *messageInput) paste() {
if data := clipboard.Read(clipboard.FmtImage); data != nil {
name := "clipboard.png"

View file

@ -52,6 +52,8 @@ type messagesList struct {
// itemByID caches unselected message TextViews.
itemByID map[discord.MessageID]*tview.TextView
attachmentsPicker *attachmentsPicker
fetchingMembers struct {
mu sync.Mutex
value bool
@ -83,6 +85,7 @@ func newMessagesList(cfg *config.Config, chatView *View) *messagesList {
renderer: markdown.NewRenderer(cfg),
itemByID: make(map[discord.MessageID]*tview.TextView),
}
ml.attachmentsPicker = newAttachmentsPicker(cfg, chatView)
ml.Box = ui.ConfigureBox(ml.Box, &cfg.Theme)
ml.SetTitle("Messages")
@ -94,7 +97,6 @@ func newMessagesList(cfg *config.Config, chatView *View) *messagesList {
SetTrackStyle(cfg.Theme.ScrollBar.TrackStyle.Style).
SetThumbStyle(cfg.Theme.ScrollBar.ThumbStyle.Style).
SetGlyphSet(cfg.Theme.ScrollBar.GlyphSet.GlyphSet))
ml.SetInputCapture(ml.onInputCapture)
return ml
}
@ -526,7 +528,7 @@ func (ml *messagesList) selectedMessage() (*discord.Message, error) {
return &ml.messages[cursor], nil
}
func (ml *messagesList) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
func (ml *messagesList) handleInput(event *tcell.EventKey) *tcell.EventKey {
switch {
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollUp.Keybind):
ml.ScrollUp()
@ -592,6 +594,14 @@ func (ml *messagesList) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
return event
}
func (ml *messagesList) InputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
event = ml.handleInput(event)
if event == nil {
return
}
ml.List.InputHandler(event, setFocus)
}
func (ml *messagesList) selectUp() {
messages := ml.messages
if len(messages) == 0 {
@ -788,19 +798,8 @@ func extractURLs(content string) []string {
}
func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord.Attachment) {
type attachmentAction struct {
label string
shortcut rune
open func()
}
closeList := func() {
ml.chatView.RemoveLayer(attachmentsListLayerName)
ml.chatView.app.SetFocus(ml)
}
var actions []attachmentAction
for i, a := range attachments {
var items []attachmentItem
for _, a := range attachments {
attachment := a
action := func() {
if strings.HasPrefix(attachment.ContentType, "image/") {
@ -809,103 +808,30 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
go ml.openURL(attachment.URL)
}
}
actions = append(actions, attachmentAction{
label: attachment.Filename,
shortcut: rune('a' + i),
open: action,
items = append(items, attachmentItem{
label: attachment.Filename,
open: action,
})
}
for i, u := range urls {
for _, u := range urls {
url := u
actions = append(actions, attachmentAction{
label: url,
shortcut: rune('1' + i),
open: func() { go ml.openURL(url) },
items = append(items, attachmentItem{
label: url,
open: func() { go ml.openURL(url) },
})
}
normalItems := make([]*tview.TextView, len(actions))
selectedItems := make([]*tview.TextView, len(actions))
for i, action := range actions {
normalItems[i] = tview.NewTextView().
SetScrollable(false).
SetWrap(false).
SetWordWrap(false).
SetLines([]tview.Line{{{Text: action.label, Style: tcell.StyleDefault}}})
selectedItems[i] = tview.NewTextView().
SetScrollable(false).
SetWrap(false).
SetWordWrap(false).
SetLines([]tview.Line{{{Text: action.label, Style: tcell.StyleDefault.Reverse(true)}}})
}
list := tview.NewList().
SetSnapToItems(true).
SetBuilder(func(index int, cursor int) tview.ListItem {
if index < 0 || index >= len(actions) {
return nil
}
if index == cursor {
return selectedItems[index]
}
return normalItems[index]
})
list.Box = ui.ConfigureBox(list.Box, &ml.cfg.Theme)
if len(actions) > 0 {
list.SetCursor(0)
}
list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch {
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectUp.Keybind):
return tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone)
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectDown.Keybind):
return tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone)
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectTop.Keybind):
return tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone)
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectBottom.Keybind):
return tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone)
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Cancel.Keybind):
closeList()
return nil
}
if event.Key() == tcell.KeyEnter || event.Key() == tcell.KeyRune && event.Str() == " " {
index := list.Cursor()
if index >= 0 && index < len(actions) {
actions[index].open()
closeList()
}
return nil
}
if event.Key() == tcell.KeyRune {
key := event.Str()
if key == "" {
return event
}
ch := []rune(key)[0]
for index, action := range actions {
if action.shortcut == ch {
list.SetCursor(index)
actions[index].open()
closeList()
return nil
}
}
}
return event
})
ml.attachmentsPicker.SetItems(items)
ml.chatView.
AddLayer(
ui.Centered(list, 0, 0),
ui.Centered(ml.attachmentsPicker, 0, 0),
layers.WithName(attachmentsListLayerName),
layers.WithResize(true),
layers.WithVisible(true),
layers.WithOverlay(),
).
SendToFront(attachmentsListLayerName)
ml.chatView.app.SetFocus(ml.attachmentsPicker)
}
func (ml *messagesList) openAttachment(attachment discord.Attachment) {

View file

@ -91,7 +91,6 @@ func NewView(app *tview.Application, cfg *config.Config, onLogout func()) *View
v.help.SetBorderPadding(0, 0, cfg.Help.Padding[0], cfg.Help.Padding[1])
v.SetBackgroundLayerStyle(v.cfg.Theme.Dialog.BackgroundStyle.Style)
v.SetInputCapture(v.onInputCapture)
v.buildLayout()
return v
}
@ -222,7 +221,7 @@ func (v *View) focusNext() {
}
}
func (v *View) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
func (v *View) handleInput(event *tcell.EventKey) *tcell.EventKey {
switch {
case keybind.Matches(event, v.cfg.Keybinds.ToggleHelp.Keybind):
v.help.SetShowAll(!v.help.ShowAll())
@ -267,6 +266,14 @@ func (v *View) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
return event
}
func (v *View) InputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
event = v.handleInput(event)
if event == nil {
return
}
v.Layers.InputHandler(event, setFocus)
}
func (v *View) updateHelpHeight() {
height := 1
if v.help.ShowAll() {

View file

@ -58,21 +58,22 @@ func newQRLogin(app *tview.Application, cfg *config.Config, done func(token stri
SetChangedFunc(func() {
q.app.QueueUpdateDraw(func() {})
}).
SetTitle("Login with QR").
SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
if ev.Key() == tcell.KeyEsc {
q.stop()
if q.done != nil {
q.done("", nil)
}
return nil
}
return ev
})
SetTitle("Login with QR")
return q
}
func (q *qrLogin) InputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
if event.Key() == tcell.KeyEsc {
q.stop()
if q.done != nil {
q.done("", nil)
}
return
}
q.TextView.InputHandler(event, setFocus)
}
func (q *qrLogin) start() {
ctx, cancel := context.WithCancel(context.Background())
q.cancel = cancel

View file

@ -0,0 +1,5 @@
//go:build !unix
package root
func (v *View) suspend() {}

View file

@ -1,6 +1,6 @@
//go:build unix
package app
package root
import (
"os"
@ -8,8 +8,8 @@ import (
"syscall"
)
func (a *App) suspend() {
a.inner.Suspend(func() {
func (v *View) suspend() {
v.app.Suspend(func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGCONT)
defer signal.Stop(c)

209
internal/ui/root/view.go Normal file
View file

@ -0,0 +1,209 @@
package root
import (
"fmt"
"log/slog"
"os"
"github.com/ayn2op/discordo/internal/clipboard"
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/discordo/internal/consts"
"github.com/ayn2op/discordo/internal/keyring"
"github.com/ayn2op/discordo/internal/ui/chat"
"github.com/ayn2op/discordo/internal/ui/login"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/keybind"
"github.com/gdamore/tcell/v3"
)
type View struct {
*tview.Box
app *tview.Application
inner tview.Primitive
chat *chat.View
cfg *config.Config
}
func NewView(cfg *config.Config) *View {
tview.Styles = tview.Theme{}
v := &View{
Box: tview.NewBox(),
app: tview.NewApplication(),
cfg: cfg,
}
if err := clipboard.Init(); err != nil {
slog.Error("failed to init clipboard", "err", err)
}
return v
}
func (v *View) Run() error {
token := os.Getenv("DISCORDO_TOKEN")
if token == "" {
t, err := keyring.GetToken()
if err != nil {
slog.Info("failed to retrieve token from keyring", "err", err)
}
token = t
}
screen, err := tcell.NewScreen()
if err != nil {
return fmt.Errorf("failed to create screen: %w", err)
}
if err := screen.Init(); err != nil {
return fmt.Errorf("failed to init screen: %w", err)
}
if v.cfg.Mouse {
screen.EnableMouse()
}
screen.SetTitle(consts.Name)
screen.EnablePaste()
screen.EnableFocus()
v.app.SetScreen(screen)
v.app.SetRoot(v)
if token == "" {
v.showLoginView()
} else {
if err := v.showChatView(token); err != nil {
return err
}
}
v.app.SetFocus(v)
err = v.app.Run()
v.closeChatViewState()
return err
}
func (v *View) showLoginView() {
loginForm := login.NewForm(v.app, v.cfg, func(token string) {
if err := v.showChatView(token); err != nil {
slog.Error("failed to show chat view", "err", err)
}
})
v.inner = loginForm
v.MarkDirty()
v.app.SetFocus(v)
}
func (v *View) showChatView(token string) error {
v.chat = chat.NewView(v.app, v.cfg, v.showLoginView)
if err := v.chat.OpenState(token); err != nil {
return err
}
v.inner = v.chat
v.MarkDirty()
v.app.SetFocus(v)
return nil
}
func (v *View) quit() {
v.closeChatViewState()
v.app.Stop()
}
func (v *View) closeChatViewState() {
if v.chat != nil {
if err := v.chat.CloseState(); err != nil {
slog.Error("failed to close the session", "err", err)
}
v.chat = nil
}
}
func (v *View) Draw(screen tcell.Screen) {
if v.inner == nil {
return
}
x, y, width, height := v.GetRect()
v.inner.SetRect(x, y, width, height)
v.inner.Draw(screen)
}
func (v *View) InputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
switch {
case keybind.Matches(event, v.cfg.Keybinds.Suspend.Keybind):
v.suspend()
return
case keybind.Matches(event, v.cfg.Keybinds.Quit.Keybind):
v.quit()
return
}
if v.inner != nil {
v.inner.InputHandler(event, setFocus)
}
}
func (v *View) Focus(delegate func(p tview.Primitive)) {
if v.inner != nil {
delegate(v.inner)
return
}
v.Box.Focus(delegate)
}
func (v *View) HasFocus() bool {
if v.inner != nil && v.inner.HasFocus() {
return true
}
return v.Box.HasFocus()
}
func (v *View) Blur() {
if v.inner != nil {
v.inner.Blur()
}
v.Box.Blur()
}
func (v *View) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
return v.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
if v.inner == nil {
return false, nil
}
handler := v.inner.MouseHandler()
if handler == nil {
return false, nil
}
return handler(action, event, setFocus)
})
}
func (v *View) PasteHandler() func(text string, setFocus func(p tview.Primitive)) {
return v.WrapPasteHandler(func(text string, setFocus func(p tview.Primitive)) {
if v.inner == nil {
return
}
handler := v.inner.PasteHandler()
if handler == nil {
return
}
handler(text, setFocus)
})
}
func (v *View) IsDirty() bool {
if v.Box.IsDirty() {
return true
}
if v.inner == nil {
return false
}
return v.inner.IsDirty()
}
func (v *View) MarkClean() {
v.Box.MarkClean()
if v.inner != nil {
v.inner.MarkClean()
}
}

View file

@ -43,8 +43,7 @@ func New() *Picker {
SetLabel("> ").
SetBorders(tview.BordersBottom).
SetBorderSet(borderSet).
SetBorderStyle(tcell.StyleDefault.Dim(true)).
SetInputCapture(p.onInputCapture)
SetBorderStyle(tcell.StyleDefault.Dim(true))
p.
SetDirection(tview.FlexRow).
@ -149,24 +148,23 @@ func (p *Picker) onInputChanged(text string) {
p.setFilteredItems(fuzzied)
}
func (p *Picker) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
func (p *Picker) handleInput(event *tcell.EventKey) *tcell.EventKey {
if p.keyMap == nil {
return nil
return event
}
handler := p.list.InputHandler()
switch {
case keybind.Matches(event, p.keyMap.Up):
handler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil)
p.list.InputHandler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil)
return nil
case keybind.Matches(event, p.keyMap.Down):
handler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone), nil)
p.list.InputHandler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone), nil)
return nil
case keybind.Matches(event, p.keyMap.Top):
handler(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone), nil)
p.list.InputHandler(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone), nil)
return nil
case keybind.Matches(event, p.keyMap.Bottom):
handler(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone), nil)
p.list.InputHandler(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone), nil)
case keybind.Matches(event, p.keyMap.Select):
p.onListSelected(p.list.Cursor())
return nil
@ -180,3 +178,11 @@ func (p *Picker) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
return event
}
func (p *Picker) InputHandler(event *tcell.EventKey, setFocus func(p2 tview.Primitive)) {
event = p.handleInput(event)
if event == nil {
return
}
p.Flex.InputHandler(event, setFocus)
}