diff --git a/.vscode/keymaster.code-snippets b/.vscode/keymaster.code-snippets new file mode 100644 index 0000000..c6e62ac --- /dev/null +++ b/.vscode/keymaster.code-snippets @@ -0,0 +1,14 @@ +{ + "Keymaster Workspace Header": { + "scope": "go", + "prefix": "kmheader", + "body": [ + "// Copyright (c) ${CURRENT_YEAR} Keymaster Team", + "// Keymaster - SSH key management system", + "// This source code is licensed under the MIT license found in the LICENSE file.", + "package ${TM_DIRECTORY/.*[\\/\\\\](\\w+)/$1/}$0", + "" + ], + "description": "Project-specific header for Keymaster" + } +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 69b5fba..0fbe5e5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,6 +21,7 @@ "program": "${workspaceFolder}/ui/tui/maintest", "cwd": "${workspaceFolder}", "console": "integratedTerminal", + "hideSystemGoroutines": true, "buildFlags": [ "-ldflags=-X github.com/toeirei/keymaster/buildvars.Version=development" ] diff --git a/ui/tui/models/components/router/controll.go b/ui/tui/models/components/router/controll.go index 6b11ef1..d9047af 100644 --- a/ui/tui/models/components/router/controll.go +++ b/ui/tui/models/components/router/controll.go @@ -3,25 +3,26 @@ // This source code is licensed under the MIT license found in the LICENSE file. package router -// TODO rewrite with util.Model in mind +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/toeirei/keymaster/ui/tui/util" +) -import tea "github.com/charmbracelet/bubbletea" - -type RouterControll struct { +type Controll struct { rid int } -func (rc *RouterControll) PushCmd(model tea.Model) tea.Cmd { - return func() tea.Msg { return PushMsg{rid: rc.rid, Model: model} } +func (c *Controll) Push(model *util.Model) tea.Cmd { + return func() tea.Msg { return PushMsg{rid: c.rid, Model: model} } } -func (rc *RouterControll) PopCmd(count int) tea.Cmd { - return func() tea.Msg { return PopMsg{rid: rc.rid, Count: count} } +func (c *Controll) Pop(count int) tea.Cmd { + return func() tea.Msg { return PopMsg{rid: c.rid, Count: count} } } -func (rc *RouterControll) ChangeCmd(model tea.Model) tea.Cmd { - return func() tea.Msg { return ChangeMsg{rid: rc.rid, Model: model} } +func (c *Controll) Change(model *util.Model) tea.Cmd { + return func() tea.Msg { return ChangeMsg{rid: c.rid, Model: model} } } -// func (rc *RouterControll) IsMsgOwner(msg tea.Msg) bool { +// func (c *RouterControll) IsMsgOwner(msg tea.Msg) bool { // rmsg, ok := msg.(RouterMsg) -// return ok && rmsg.routerId() == rc.rid +// return ok && rmsg.routerId() == c.rid // } diff --git a/ui/tui/models/components/router/handle.go b/ui/tui/models/components/router/handle.go new file mode 100644 index 0000000..f248b73 --- /dev/null +++ b/ui/tui/models/components/router/handle.go @@ -0,0 +1,41 @@ +// Copyright (c) 2026 Keymaster Team +// Keymaster - SSH key management system +// This source code is licensed under the MIT license found in the LICENSE file. +package router + +import ( + tea "github.com/charmbracelet/bubbletea" +) + +// handle PushMsg +func (m *Model) handlePush(msg PushMsg) tea.Cmd { + // blur recent model + (*m.activeModelGet()).Blur() + // push new model + m.model_stack = append(m.model_stack, msg.Model) + // initialize pushed model + return m.activeModelInit() +} + +// handle PopMsg +func (m *Model) handlePop(msg PopMsg) tea.Cmd { + // pop and blur old models + for range msg.Count { + if len(m.model_stack) <= 1 { + break + } + (*m.activeModelPop()).Blur() + } + // focus active model + return m.activeModelFocus() +} + +// handle ChangeMsg +func (m *Model) handleChange(msg ChangeMsg) tea.Cmd { + // destroy recent model + (*m.activeModelGet()).Blur() + // set new model + m.activeModelSet(msg.Model) + // initialize set model + return m.activeModelInit() +} diff --git a/ui/tui/models/components/router/helper.go b/ui/tui/models/components/router/helper.go deleted file mode 100644 index 47f8396..0000000 --- a/ui/tui/models/components/router/helper.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2026 Keymaster Team -// Keymaster - SSH key management system -// This source code is licensed under the MIT license found in the LICENSE file. -package router - -// TODO rewrite with util.Model in mind - -import tea "github.com/charmbracelet/bubbletea" - -func (r *Router) activeModelGet() tea.Model { - return r.model_stack[len(r.model_stack)-1] -} - -func (r *Router) activeModelSet(model tea.Model) { - r.model_stack[len(r.model_stack)-1] = model -} - -func (r *Router) activeModelPop() tea.Model { - model := r.model_stack[len(r.model_stack)-1] - r.model_stack = r.model_stack[:len(r.model_stack)-1] - return model -} - -func (r *Router) activeModelUpdate(msg tea.Msg) tea.Cmd { - model, cmd := r.activeModelGet().Update(msg) - r.activeModelSet(model) - return cmd -} diff --git a/ui/tui/models/components/router/msg.go b/ui/tui/models/components/router/msg.go index 5f79f38..645adff 100644 --- a/ui/tui/models/components/router/msg.go +++ b/ui/tui/models/components/router/msg.go @@ -3,30 +3,23 @@ // This source code is licensed under the MIT license found in the LICENSE file. package router -// TODO rewrite with util.Model in mind - import ( - tea "github.com/charmbracelet/bubbletea" + "github.com/toeirei/keymaster/ui/tui/util" ) // Router invoked messages +// Router -> Model + type InitMsg struct { - RouterControll RouterControll -} -type SuspendMsg struct { - rid int -} -type ResumeMsg struct { - rid int -} -type DestroyMsg struct { - rid int + RouterControll Controll } // RouterControll invoked messages +// Model-RouterControll -> Router + type PushMsg struct { rid int - Model tea.Model + Model *util.Model } type PopMsg struct { rid int @@ -34,22 +27,14 @@ type PopMsg struct { } type ChangeMsg struct { rid int - Model tea.Model + Model *util.Model } -func (m InitMsg) routerId() int { return m.RouterControll.rid } -func (m SuspendMsg) routerId() int { return m.rid } -func (m ResumeMsg) routerId() int { return m.rid } -func (m DestroyMsg) routerId() int { return m.rid } -func (m PushMsg) routerId() int { return m.rid } -func (m PopMsg) routerId() int { return m.rid } -func (m ChangeMsg) routerId() int { return m.rid } +func (m InitMsg) routerId() int { return m.RouterControll.rid } +func (m PushMsg) routerId() int { return m.rid } +func (m PopMsg) routerId() int { return m.rid } +func (m ChangeMsg) routerId() int { return m.rid } type RouterMsg interface { routerId() int } - -func IsRouterMsg(msg tea.Msg) bool { - _, ok := msg.(RouterMsg) - return ok -} diff --git a/ui/tui/models/components/router/router.go b/ui/tui/models/components/router/router.go index fc2a917..b48da9f 100644 --- a/ui/tui/models/components/router/router.go +++ b/ui/tui/models/components/router/router.go @@ -3,140 +3,89 @@ // This source code is licensed under the MIT license found in the LICENSE file. package router -// TODO rewrite with util.Model in mind - import ( + "github.com/charmbracelet/bubbles/help" tea "github.com/charmbracelet/bubbletea" "github.com/toeirei/keymaster/ui/tui/util" ) -var routerId = 1 +var routerId = 1 // TODO change to atomic int... just to be sure -type Router struct { +type Model struct { id int size util.Size - model_stack []tea.Model + model_stack []*util.Model } -var _ tea.Model = (*Router)(nil) - -func New(initial_model tea.Model) (Router, RouterControll) { +func New(initial_model *util.Model) (*Model, Controll) { routerId++ - return Router{ + return &Model{ id: routerId - 1, - model_stack: []tea.Model{initial_model}, - }, RouterControll{ + model_stack: []*util.Model{initial_model}, + }, Controll{ rid: routerId - 1, } } -func (r Router) Init() tea.Cmd { +func (m Model) Init() tea.Cmd { return tea.Batch( - r.activeModelGet().Init(), - r.activeModelUpdate(InitMsg{RouterControll: RouterControll{rid: r.id}}), + (*m.activeModelGet()).Init(), + m.activeModelUpdate(InitMsg{RouterControll: Controll{rid: m.id}}), + m.activeModelUpdate(tea.WindowSizeMsg(m.size)), ) } -func (r Router) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd +func (m *Model) Update(msg tea.Msg) tea.Cmd { + var cmd tea.Cmd - if r.size.Update(msg) { - // pass window size update - cmds = append(cmds, r.activeModelUpdate(msg)) - } else if r.isMsgOwner(msg) { + if m.size.Update(msg) { + // pass window size messages + cmd = m.activeModelUpdate(msg) + } else if m.isMsgOwner(msg) { // handle controll messages meant for this router switch msg := msg.(type) { case PushMsg: - cmds = append(cmds, r.handlePush(msg)...) + cmd = m.handlePush(msg) case PopMsg: - cmds = append(cmds, r.handlePop(msg)...) + cmd = m.handlePop(msg) case ChangeMsg: - cmds = append(cmds, r.handleChange(msg)...) + cmd = m.handleChange(msg) } } else if IsRouterMsg(msg) { - // rewrite info messages from other routers - switch msg := msg.(type) { - case InitMsg: - // do not pass init messages, to prevent childs from obtaining parent routers RouterControll - case SuspendMsg: - cmds = append(cmds, r.handleSuspend(msg)) - case ResumeMsg: - cmds = append(cmds, r.handleResume(msg)) - case DestroyMsg: - cmds = append(cmds, r.handleDestroy(msg)) - default: - // pass controll messages for child routers - cmds = append(cmds, r.activeModelUpdate(msg)) + // do not pass init messages, to prevent childs from obtaining parent routers RouterControll + if _, ok := msg.(InitMsg); !ok { + // pass other controll messages for child routers + cmd = m.activeModelUpdate(msg) } } else { - // pass other update - cmds = append(cmds, r.activeModelUpdate(msg)) + // pass other messages + cmd = m.activeModelUpdate(msg) } - return r, tea.Batch(cmds...) -} - -func (r Router) View() string { - return r.activeModelGet().View() -} - -// handle PushMsg -func (r *Router) handlePush(msg PushMsg) []tea.Cmd { - var cmds []tea.Cmd - // suspend recent model - cmds = append(cmds, r.activeModelUpdate(SuspendMsg{rid: r.id})) - // push new model - r.model_stack = append(r.model_stack, msg.Model) - // initialize pushed model - cmds = append(cmds, msg.Model.Init()) - cmds = append(cmds, r.activeModelUpdate(InitMsg{RouterControll: RouterControll{rid: r.id}})) - return append(cmds, r.activeModelUpdate(tea.WindowSizeMsg(r.size))) + return cmd } -// handle PopMsg -func (r *Router) handlePop(msg PopMsg) []tea.Cmd { - var cmds []tea.Cmd - // pop and destroy old models - for range msg.Count { - if len(r.model_stack) <= 1 { - break - } - _, cmd := r.activeModelPop().Update(DestroyMsg{rid: r.id}) - cmds = append(cmds, cmd) - } - // resume active model - return append(cmds, r.activeModelUpdate(ResumeMsg{rid: r.id})) +func (m Model) View() string { + return (*m.activeModelGet()).View() } -// handle ChangeMsg -func (r *Router) handleChange(msg ChangeMsg) []tea.Cmd { - var cmds []tea.Cmd - // destroy recent model - cmds = append(cmds, r.activeModelUpdate(DestroyMsg{rid: r.id})) - // set new model - r.activeModelSet(msg.Model) - // initialize set model - cmds = append(cmds, msg.Model.Init()) - cmds = append(cmds, r.activeModelUpdate(InitMsg{RouterControll: RouterControll{rid: r.id}})) - return append(cmds, r.activeModelUpdate(tea.WindowSizeMsg(r.size))) +func (m *Model) Focus() (tea.Cmd, help.KeyMap) { + return (*m.activeModelGet()).Focus() } -// handle SuspendMsg (from other router) -func (r *Router) handleSuspend(_ SuspendMsg) tea.Cmd { - return r.activeModelUpdate(SuspendMsg{rid: r.id}) +func (m *Model) Blur() { + (*m.activeModelGet()).Blur() } -// handle ResumeMsg (from other router) -func (r *Router) handleResume(_ ResumeMsg) tea.Cmd { - return r.activeModelUpdate(ResumeMsg{rid: r.id}) -} +// *Model implements util.Model +var _ util.Model = (*Model)(nil) -// handle DestroyMsg (from other router) -func (r *Router) handleDestroy(_ DestroyMsg) tea.Cmd { - return r.activeModelUpdate(DestroyMsg{rid: r.id}) +func (m *Model) isMsgOwner(msg tea.Msg) bool { + rmsg, ok := msg.(RouterMsg) + return ok && rmsg.routerId() == m.id } -func (r *Router) isMsgOwner(msg tea.Msg) bool { - rmsg, ok := msg.(RouterMsg) - return ok && rmsg.routerId() == r.id +func IsRouterMsg(msg tea.Msg) bool { + _, ok := msg.(RouterMsg) + return ok } diff --git a/ui/tui/models/components/router/util.go b/ui/tui/models/components/router/util.go new file mode 100644 index 0000000..4f6e0d5 --- /dev/null +++ b/ui/tui/models/components/router/util.go @@ -0,0 +1,41 @@ +// Copyright (c) 2026 Keymaster Team +// Keymaster - SSH key management system +// This source code is licensed under the MIT license found in the LICENSE file. +package router + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/toeirei/keymaster/ui/tui/util" +) + +func (m *Model) activeModelGet() *util.Model { + return m.model_stack[len(m.model_stack)-1] +} + +func (m *Model) activeModelSet(model *util.Model) { + m.model_stack[len(m.model_stack)-1] = model +} + +func (m *Model) activeModelPop() *util.Model { + model := m.activeModelGet() + m.model_stack = m.model_stack[:len(m.model_stack)-1] + return model +} + +func (m *Model) activeModelUpdate(msg tea.Msg) tea.Cmd { + return (*m.activeModelGet()).Update(msg) +} + +func (m *Model) activeModelFocus() tea.Cmd { + cmd, keyMap := (*m.activeModelGet()).Focus() + return tea.Batch(cmd, util.AnnounceKeyMapCmd(keyMap)) +} + +func (m *Model) activeModelInit() tea.Cmd { + return tea.Sequence( + (*m.activeModelGet()).Init(), + m.activeModelUpdate(InitMsg{RouterControll: Controll{rid: m.id}}), + m.activeModelUpdate(tea.WindowSizeMsg(m.size)), + m.activeModelFocus(), + ) +} diff --git a/ui/tui/models/components/stack/new.go b/ui/tui/models/components/stack/new.go index e0c9618..d719bd1 100644 --- a/ui/tui/models/components/stack/new.go +++ b/ui/tui/models/components/stack/new.go @@ -61,3 +61,9 @@ func WithFocus(focus Focus) NewOpt { stack.focussedIndex = focus } } + +func WithFocusNext() NewOpt { + return func(stack *Model) { + stack.focussedIndex = Focus(len(stack.items)) + } +} diff --git a/ui/tui/models/views/content/content.go b/ui/tui/models/views/content/content.go new file mode 100644 index 0000000..79cc34d --- /dev/null +++ b/ui/tui/models/views/content/content.go @@ -0,0 +1,152 @@ +// Copyright (c) 2026 Keymaster Team +// Keymaster - SSH key management system +// This source code is licensed under the MIT license found in the LICENSE file. +package content + +import ( + "github.com/charmbracelet/bubbles/help" + tea "github.com/charmbracelet/bubbletea" + "github.com/toeirei/keymaster/ui/tui/models/components/menu" + "github.com/toeirei/keymaster/ui/tui/models/components/popup" + "github.com/toeirei/keymaster/ui/tui/models/components/router" + "github.com/toeirei/keymaster/ui/tui/models/components/stack" + "github.com/toeirei/keymaster/ui/tui/models/views/debug" + "github.com/toeirei/keymaster/ui/tui/models/views/testpopup1" + "github.com/toeirei/keymaster/ui/tui/util" +) + +type Model struct { + router *router.Model + routerControll router.Controll +} + +func New() *Model { + // router { + // stack { + // menu + // debug // TODO replace with dashboard later + // } + // } + + _menu := util.ModelPointer(menu.New( + menu.WithItem("dashboard", "Dashboard"), + menu.WithItem("test", "Tests", + menu.WithItem("test.popup1", "Popup Test 1"), + ), + menu.WithItem("projects", "Projects", + menu.WithItem("proj_active", "Active Projects", + menu.WithItem("proj_a", "Project Alpha", + menu.WithItem("a_tasks", "Task List"), + menu.WithItem("a_milestones", "Milestones"), + ), + menu.WithItem("proj_b", "Project Beta"), + ), + menu.WithItem("proj_archived", "Archive"), + ), + menu.WithItem("users", "User Management", + menu.WithItem("u_list", "All Users"), + menu.WithItem("u_roles", "Role Definitions", + menu.WithItem("role_admin", "Administrators", + menu.WithItem("perm_full", "Full Permissions"), + ), + menu.WithItem("role_editor", "Editors"), + ), + ), + menu.WithItem("analytics", "Analytics", + menu.WithItem("an_sales", "Sales Reports", + menu.WithItem("q1_sales", "Q1 Report"), + menu.WithItem("q2_sales", "Q2 Report"), + ), + menu.WithItem("an_traffic", "Web Traffic"), + ), + menu.WithItem("billing", "Billing", + menu.WithItem("bill_inv", "Invoices"), + menu.WithItem("bill_meth", "Payment Methods"), + ), + menu.WithItem("settings", "Settings", + menu.WithItem("set_gen", "General"), + menu.WithItem("set_sec", "Security", + menu.WithItem("sec_2fa", "Two-Factor Auth", + menu.WithItem("2fa_sms", "SMS Setup"), + menu.WithItem("2fa_app", "Authenticator App"), + ), + ), + ), + menu.WithItem("inventory", "Inventory with a name"), + menu.WithItem("logistics", "Logistics", + menu.WithItem("log_shipping", "Shipping", + menu.WithItem("ship_int", "International", + menu.WithItem("ship_customs", "Customs Forms"), + ), + ), + ), + menu.WithItem("marketing", "Marketing"), + menu.WithItem("support", "Support Tickets", + menu.WithItem("sup_open", "Open Tickets"), + menu.WithItem("sup_closed", "History"), + ), + menu.WithItem("hr", "Human Resources", + menu.WithItem("hr_payroll", "Payroll"), + menu.WithItem("hr_benefits", "Benefits"), + ), + menu.WithItem("legal", "Legal Compliance"), + menu.WithItem("it_assets", "IT Assets", + menu.WithItem("it_hw", "Hardware", + menu.WithItem("hw_laptops", "Laptops"), + menu.WithItem("hw_servers", "Servers"), + ), + ), + menu.WithItem("api", "API Management", + menu.WithItem("api_keys", "Access Keys"), + menu.WithItem("api_docs", "Documentation"), + ), + menu.WithItem("feedback", "User Feedback"), + )) + _debug := util.ModelPointer(debug.New()) + _stack := util.ModelPointer(stack.New( + stack.WithOrientation(stack.Horizontal), + stack.WithFocusNext(), + stack.WithItem(_menu, menu.SizeConfig), + // TODO replace with dashboard when ready + stack.WithItem(_debug, stack.VariableSize(1)), + )) + _router, routerControll := router.New(_stack) + + return &Model{ + router: _router, + routerControll: routerControll, + } +} + +func (m *Model) Init() tea.Cmd { + return m.router.Init() +} + +func (m *Model) Update(msg tea.Msg) tea.Cmd { + // handle menu messages + if msg, ok := msg.(menu.ItemSelected); ok { + switch msg.Id { + case "test.popup1": + // popup example + return popup.Open(util.ModelPointer(testpopup1.New())) + } + } + + // pass other messages to stack + return m.router.Update(msg) +} + +func (m *Model) View() string { + return m.router.View() +} + +func (m *Model) Focus() (tea.Cmd, help.KeyMap) { + return m.router.Focus() +} + +func (m *Model) Blur() { + m.router.Blur() +} + +// *Model implements util.Model +var _ util.Model = (*Model)(nil) diff --git a/ui/tui/models/views/root/root.go b/ui/tui/models/views/root/root.go index 2f1f7b0..d0fde49 100644 --- a/ui/tui/models/views/root/root.go +++ b/ui/tui/models/views/root/root.go @@ -10,13 +10,11 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/toeirei/keymaster/buildvars" "github.com/toeirei/keymaster/ui/tui/models/components/header" - "github.com/toeirei/keymaster/ui/tui/models/components/menu" "github.com/toeirei/keymaster/ui/tui/models/components/popup" "github.com/toeirei/keymaster/ui/tui/models/components/stack" windowtitle "github.com/toeirei/keymaster/ui/tui/models/helpers/title" - "github.com/toeirei/keymaster/ui/tui/models/views/debug" + "github.com/toeirei/keymaster/ui/tui/models/views/content" "github.com/toeirei/keymaster/ui/tui/models/views/footer" - "github.com/toeirei/keymaster/ui/tui/models/views/testpopup1" "github.com/toeirei/keymaster/ui/tui/util" ) @@ -24,118 +22,34 @@ const title string = "Keymaster" type Model struct { stack *stack.Model - menu *util.Model footer *util.Model titleHandler *windowtitle.TitleHandler } func New() *Model { - _header := header.New() - _menu := menu.New( - menu.WithItem("dashboard", "Dashboard"), - menu.WithItem("test", "Tests", - menu.WithItem("test.popup1", "Popup Test 1"), - ), - menu.WithItem("projects", "Projects", - menu.WithItem("proj_active", "Active Projects", - menu.WithItem("proj_a", "Project Alpha", - menu.WithItem("a_tasks", "Task List"), - menu.WithItem("a_milestones", "Milestones"), - ), - menu.WithItem("proj_b", "Project Beta"), - ), - menu.WithItem("proj_archived", "Archive"), - ), - menu.WithItem("users", "User Management", - menu.WithItem("u_list", "All Users"), - menu.WithItem("u_roles", "Role Definitions", - menu.WithItem("role_admin", "Administrators", - menu.WithItem("perm_full", "Full Permissions"), - ), - menu.WithItem("role_editor", "Editors"), - ), - ), - menu.WithItem("analytics", "Analytics", - menu.WithItem("an_sales", "Sales Reports", - menu.WithItem("q1_sales", "Q1 Report"), - menu.WithItem("q2_sales", "Q2 Report"), - ), - menu.WithItem("an_traffic", "Web Traffic"), - ), - menu.WithItem("billing", "Billing", - menu.WithItem("bill_inv", "Invoices"), - menu.WithItem("bill_meth", "Payment Methods"), - ), - menu.WithItem("settings", "Settings", - menu.WithItem("set_gen", "General"), - menu.WithItem("set_sec", "Security", - menu.WithItem("sec_2fa", "Two-Factor Auth", - menu.WithItem("2fa_sms", "SMS Setup"), - menu.WithItem("2fa_app", "Authenticator App"), - ), - ), - ), - menu.WithItem("inventory", "Inventory with a name"), - menu.WithItem("logistics", "Logistics", - menu.WithItem("log_shipping", "Shipping", - menu.WithItem("ship_int", "International", - menu.WithItem("ship_customs", "Customs Forms"), - ), - ), - ), - menu.WithItem("marketing", "Marketing"), - menu.WithItem("support", "Support Tickets", - menu.WithItem("sup_open", "Open Tickets"), - menu.WithItem("sup_closed", "History"), - ), - menu.WithItem("hr", "Human Resources", - menu.WithItem("hr_payroll", "Payroll"), - menu.WithItem("hr_benefits", "Benefits"), - ), - menu.WithItem("legal", "Legal Compliance"), - menu.WithItem("it_assets", "IT Assets", - menu.WithItem("it_hw", "Hardware", - menu.WithItem("hw_laptops", "Laptops"), - menu.WithItem("hw_servers", "Servers"), - ), - ), - menu.WithItem("api", "API Management", - menu.WithItem("api_keys", "Access Keys"), - menu.WithItem("api_docs", "Documentation"), - ), - menu.WithItem("feedback", "User Feedback"), - ) - _footer := footer.New(&BaseKeyMap) - - // create model pointers for multiple references - _menu_ptr := util.ModelPointer(_menu) - _footer_ptr := util.ModelPointer(_footer) + _header := util.ModelPointer(header.New()) + _footer := util.ModelPointer(footer.New(&BaseKeyMap)) version := "unknown version" if len(buildvars.Version) > 0 { version = buildvars.Version } + titleHandler := windowtitle.NewHandler(fmt.Sprintf("%s %s", title, version), " | ") return &Model{ stack: stack.New( stack.WithOrientation(stack.Vertical), - stack.WithFocus(stack.Focus(1)), - stack.WithItem(util.ModelPointer(_header), header.SizeConfig), + stack.WithItem(_header, header.SizeConfig), + stack.WithFocusNext(), stack.WithItem( util.ModelPointer(popup.NewInjector( - util.ModelPointer(stack.New( - stack.WithOrientation(stack.Horizontal), - stack.WithFocus(stack.Focus(0)), - stack.WithItem(_menu_ptr, menu.SizeConfig), - stack.WithItem(util.ModelPointer(debug.New()), stack.VariableSize(1)), - )), + util.ModelPointer(content.New()), )), stack.VariableSize(1)), - stack.WithItem(_footer_ptr, footer.SizeConfig), + stack.WithItem(_footer, footer.SizeConfig), ), - menu: _menu_ptr, - footer: _footer_ptr, - titleHandler: windowtitle.NewHandler(fmt.Sprintf("%s %s", title, version), " | "), + footer: _footer, + titleHandler: titleHandler, } } @@ -163,14 +77,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.stack.Update(msg) } - // handle menu messages - if msg, ok := msg.(menu.ItemSelected); ok { - switch msg.Id { - case "test.popup1": - // popup example - return m, popup.Open(util.ModelPointer(testpopup1.New())) - } - } // handle window title messages if cmd := m.titleHandler.Handle(msg); cmd != nil { return m, cmd diff --git a/ui/tui/util/focus.go b/ui/tui/util/focus.go index 569d8a5..b5e471f 100644 --- a/ui/tui/util/focus.go +++ b/ui/tui/util/focus.go @@ -14,25 +14,26 @@ import ( ) type Focusable interface { + // TODO consider passing baseKeyMap instead of recieving one Focus() (tea.Cmd, help.KeyMap) Blur() } -func TryFocusModel(m *Model) (tea.Cmd, help.KeyMap, error) { +func TryFocusTeaModel(m *tea.Model) (tea.Cmd, help.KeyMap, error) { _m := *m if focusable, ok := _m.(Focusable); ok { cmd, keyMap := focusable.Focus() - *m = focusable.(Model) + *m = focusable.(tea.Model) return cmd, keyMap, nil } else { return nil, nil, fmt.Errorf("type %T does not implement Focusable interface", m) } } -func TryBlurModel(m *Model) error { +func TryBlurTeaModel(m *tea.Model) error { _m := *m if focusable, ok := _m.(Focusable); ok { focusable.Blur() - *m = focusable.(Model) + *m = focusable.(tea.Model) return nil } else { return fmt.Errorf("type %T does not implement Focusable interface", m) @@ -43,6 +44,7 @@ type AnnounceKeyMapMsg struct { KeyMap help.KeyMap } +// TODO consider only using it when reaching the deepest point in the Model tree func AnnounceKeyMapCmd(k help.KeyMap) tea.Cmd { return func() tea.Msg { return AnnounceKeyMapMsg{KeyMap: k}