This commit is contained in:
Shen Junzheng
2025-03-12 01:19:35 +08:00
parent 160040f3e1
commit 78cce92ce3
70 changed files with 10134 additions and 1 deletions

162
internal/ui/menu/menu.go Normal file
View File

@@ -0,0 +1,162 @@
package menu
import (
"fmt"
"sort"
"github.com/sjzar/chatlog/internal/ui/style"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type Item struct {
Index int
Key string
Name string
Description string
Hidden bool
Selected func(i *Item)
}
type Menu struct {
*tview.Box
title string
table *tview.Table
items []*Item
}
func New(title string) *Menu {
menu := &Menu{
Box: tview.NewBox(),
title: title,
items: make([]*Item, 0),
table: tview.NewTable(),
}
menu.table.SetBorders(false)
menu.table.SetSelectable(true, false)
menu.table.SetTitle(fmt.Sprintf("[::b]%s", menu.title))
menu.table.SetBorderColor(style.BorderColor)
menu.table.SetBackgroundColor(style.BgColor)
menu.table.SetTitleColor(style.FgColor)
menu.table.SetFixed(1, 0)
menu.table.Select(1, 0).SetSelectedFunc(func(row, column int) {
if row == 0 {
return // 忽略表头
}
item, ok := menu.table.GetCell(row, 0).GetReference().(*Item)
if ok {
if item.Selected != nil {
item.Selected(item)
}
}
})
menu.setTableHeader()
return menu
}
func (m *Menu) setTableHeader() {
m.table.SetCell(0, 0, tview.NewTableCell(fmt.Sprintf("[black::b]%s", "命令")).
SetExpansion(1).
SetBackgroundColor(style.PageHeaderBgColor).
SetTextColor(style.PageHeaderFgColor).
SetAlign(tview.AlignLeft).
SetSelectable(false))
m.table.SetCell(0, 1, tview.NewTableCell(fmt.Sprintf("[black::b]%s", "说明")).
SetExpansion(2).
SetBackgroundColor(style.PageHeaderBgColor).
SetTextColor(style.PageHeaderFgColor).
SetAlign(tview.AlignLeft).
SetSelectable(false))
}
func (m *Menu) AddItem(item *Item) {
m.items = append(m.items, item)
sort.Sort(SortItems(m.items))
m.refresh()
}
func (m *Menu) SetItems(items []*Item) {
m.items = items
m.refresh()
}
func (m *Menu) GetItems() []*Item {
return m.items
}
func (m *Menu) refresh() {
m.table.Clear()
m.setTableHeader()
row := 1
for _, item := range m.items {
if item.Hidden {
continue
}
m.table.SetCell(row, 0, tview.NewTableCell(item.Name).
SetTextColor(style.FgColor).
SetBackgroundColor(style.BgColor).
SetReference(item).
SetAlign(tview.AlignLeft))
m.table.SetCell(row, 1, tview.NewTableCell(item.Description).
SetTextColor(style.FgColor).
SetBackgroundColor(style.BgColor).
SetReference(item).
SetAlign(tview.AlignLeft))
row++
}
}
func (m *Menu) Draw(screen tcell.Screen) {
m.refresh()
m.Box.DrawForSubclass(screen, m)
m.Box.SetBorder(false)
menuViewX, menuViewY, menuViewW, menuViewH := m.GetInnerRect()
m.table.SetRect(menuViewX, menuViewY, menuViewW, menuViewH)
m.table.SetBorder(true).SetBorderColor(style.BorderColor)
m.table.Draw(screen)
}
func (m *Menu) Focus(delegate func(p tview.Primitive)) {
delegate(m.table)
}
// HasFocus returns whether or not this primitive has focus
func (m *Menu) HasFocus() bool {
// Check if the active menu has focus
return m.table.HasFocus()
}
func (m *Menu) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
// 将事件传递给表格
if handler := m.table.InputHandler(); handler != nil {
handler(event, setFocus)
}
})
}
type SortItems []*Item
func (l SortItems) Len() int {
return len(l)
}
func (l SortItems) Less(i, j int) bool {
return l[i].Index < l[j].Index
}
func (l SortItems) Swap(i, j int) {
l[i], l[j] = l[j], l[i]
}

232
internal/ui/menu/submenu.go Normal file
View File

@@ -0,0 +1,232 @@
package menu
import (
"fmt"
"sort"
"github.com/sjzar/chatlog/internal/ui/style"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
const (
// DialogPadding dialog inner paddign.
DialogPadding = 3
// DialogFormHeight dialog "Enter"/"Cancel" form height.
DialogHelpHeight = 1
// DialogMinWidth dialog min width.
DialogMinWidth = 40
// TableHeightOffset table height offset for border.
TableHeightOffset = 3
cmdWidthOffset = 6
)
type SubMenu struct {
*tview.Box
title string
layout *tview.Flex
table *tview.Table
width int
height int
items []*Item
cancelHandler func()
}
func NewSubMenu(title string) *SubMenu {
subMenu := &SubMenu{
Box: tview.NewBox(),
title: title,
items: make([]*Item, 0),
layout: tview.NewFlex(),
table: tview.NewTable(),
}
subMenu.table.SetBorders(false)
subMenu.table.SetSelectable(true, false)
subMenu.table.SetBorderColor(style.DialogBorderColor)
subMenu.table.SetBackgroundColor(style.DialogBgColor)
subMenu.table.SetTitleColor(style.DialogFgColor)
subMenu.table.SetFixed(1, 1)
subMenu.table.Select(1, 0).SetSelectedFunc(func(row, column int) {
if row == 0 {
return // 忽略表头
}
item := subMenu.items[row-1]
if item.Selected != nil {
item.Selected(item)
}
})
subMenu.setTableHeader()
// 帮助信息
helpText := tview.NewTextView()
helpText.SetDynamicColors(true)
helpText.SetTextAlign(tview.AlignCenter)
helpText.SetTextColor(style.DialogFgColor)
helpText.SetBackgroundColor(style.DialogBgColor)
fmt.Fprintf(helpText,
"[%s::b]↑/↓[%s::b]: 导航 [%s::b]Enter[%s::b]: 选择 [%s::b]ESC[%s::b]: 返回",
style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
)
// 布局
tableLayout := tview.NewFlex().SetDirection(tview.FlexColumn)
tableLayout.AddItem(EmptyBoxSpace(style.DialogBgColor), 1, 0, true)
tableLayout.AddItem(subMenu.table, 0, 1, true)
tableLayout.AddItem(EmptyBoxSpace(style.DialogBgColor), 1, 0, true)
subMenu.layout.SetDirection(tview.FlexRow)
subMenu.layout.SetTitle(fmt.Sprintf("[::b]%s", subMenu.title))
subMenu.layout.SetTitleColor(style.DialogFgColor)
subMenu.layout.SetTitleAlign(tview.AlignCenter)
subMenu.layout.AddItem(tableLayout, 0, 1, true)
subMenu.layout.AddItem(helpText, DialogHelpHeight, 0, true)
subMenu.layout.SetBorder(true)
subMenu.layout.SetBorderColor(style.DialogBorderColor)
subMenu.layout.SetBackgroundColor(style.DialogBgColor)
return subMenu
}
func (m *SubMenu) setTableHeader() {
m.table.SetCell(0, 0, tview.NewTableCell(fmt.Sprintf("[%s::b]%s", style.GetColorHex(style.TableHeaderFgColor), "命令")).
SetExpansion(1).
SetBackgroundColor(style.TableHeaderBgColor).
SetTextColor(style.TableHeaderFgColor).
SetAlign(tview.AlignLeft).
SetSelectable(false))
m.table.SetCell(0, 1, tview.NewTableCell(fmt.Sprintf("[%s::b]%s", style.GetColorHex(style.TableHeaderFgColor), "说明")).
SetExpansion(1).
SetBackgroundColor(style.TableHeaderBgColor).
SetTextColor(style.TableHeaderFgColor).
SetAlign(tview.AlignLeft).
SetSelectable(false))
}
func (m *SubMenu) AddItem(item *Item) {
m.items = append(m.items, item)
sort.Sort(SortItems(m.items))
m.refresh()
}
func (m *SubMenu) SetItems(items []*Item) {
m.items = items
m.refresh()
}
func (m *SubMenu) SetCancelFunc(handler func()) *SubMenu {
m.cancelHandler = handler
return m
}
func (m *SubMenu) refresh() {
m.table.Clear()
m.setTableHeader()
col1Width := 0
col2Width := 0
row := 1
for _, item := range m.items {
if item.Hidden {
continue
}
m.table.SetCell(row, 0, tview.NewTableCell(item.Name).
SetTextColor(style.DialogFgColor).
SetBackgroundColor(style.DialogBgColor).
SetReference(item).
SetAlign(tview.AlignLeft))
m.table.SetCell(row, 1, tview.NewTableCell(item.Description).
SetTextColor(style.DialogFgColor).
SetBackgroundColor(style.DialogBgColor).
SetReference(item).
SetAlign(tview.AlignLeft))
if len(item.Name) > col1Width {
col1Width = len(item.Name)
}
if len(item.Description) > col2Width {
col2Width = len(item.Description)
}
row++
}
m.width = col1Width + col2Width + 2 + cmdWidthOffset
m.height = len(m.items) + TableHeightOffset + DialogHelpHeight + 1
}
func (m *SubMenu) Draw(screen tcell.Screen) {
m.refresh()
m.Box.DrawForSubclass(screen, m)
m.layout.Draw(screen)
}
func (m *SubMenu) SetRect(x, y, width, height int) {
ws := (width - m.width) / 2
hs := ((height - m.height) / 2)
dy := y + hs
bWidth := m.width
if m.width > width {
ws = 0
bWidth = width - 1
}
bHeight := m.height
if m.height >= height {
dy = y + 1
bHeight = height - 1
}
m.Box.SetRect(x+ws, dy, bWidth, bHeight)
x, y, width, height = m.Box.GetInnerRect()
m.layout.SetRect(x, y, width, height)
}
func (m *SubMenu) Focus(delegate func(p tview.Primitive)) {
delegate(m.table)
}
// HasFocus returns whether or not this primitive has focus
func (m *SubMenu) HasFocus() bool {
// Check if the active menu has focus
return m.table.HasFocus()
}
func (m *SubMenu) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
if event.Key() == tcell.KeyEscape && m.cancelHandler != nil {
m.cancelHandler()
return
}
// 将事件传递给表格
if handler := m.table.InputHandler(); handler != nil {
handler(event, setFocus)
}
})
}
func EmptyBoxSpace(bgColor tcell.Color) *tview.Box {
box := tview.NewBox()
box.SetBackgroundColor(bgColor)
box.SetBorder(false)
return box
}