From 89d19dc828cb743c51aed72826acf3ac7a7e031d Mon Sep 17 00:00:00 2001 From: Jannes Dailidow Date: Wed, 21 Jan 2026 20:52:38 +0100 Subject: [PATCH 1/4] added keymaster copyright header snippet --- .vscode/keymaster.code-snippets | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .vscode/keymaster.code-snippets 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" + } +} From 2943919b4c76eb2ff5f1a5f68202fd7a46df378e Mon Sep 17 00:00:00 2001 From: Jannes Dailidow Date: Wed, 21 Jan 2026 22:32:06 +0100 Subject: [PATCH 2/4] adapted router component to noew Model structure... testing still needed --- ui/tui/models/components/router/controll.go | 11 +- ui/tui/models/components/router/handle.go | 41 +++++++ ui/tui/models/components/router/helper.go | 28 ----- ui/tui/models/components/router/msg.go | 35 ++---- ui/tui/models/components/router/router.go | 113 ++++++-------------- ui/tui/models/components/router/util.go | 41 +++++++ 6 files changed, 132 insertions(+), 137 deletions(-) create mode 100644 ui/tui/models/components/router/handle.go delete mode 100644 ui/tui/models/components/router/helper.go create mode 100644 ui/tui/models/components/router/util.go diff --git a/ui/tui/models/components/router/controll.go b/ui/tui/models/components/router/controll.go index 6b11ef1..c9691fd 100644 --- a/ui/tui/models/components/router/controll.go +++ b/ui/tui/models/components/router/controll.go @@ -5,19 +5,22 @@ package router // TODO rewrite with util.Model in mind -import tea "github.com/charmbracelet/bubbletea" +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/toeirei/keymaster/ui/tui/util" +) type RouterControll struct { rid int } -func (rc *RouterControll) PushCmd(model tea.Model) tea.Cmd { +func (rc *RouterControll) Push(model *util.Model) tea.Cmd { return func() tea.Msg { return PushMsg{rid: rc.rid, Model: model} } } -func (rc *RouterControll) PopCmd(count int) tea.Cmd { +func (rc *RouterControll) Pop(count int) tea.Cmd { return func() tea.Msg { return PopMsg{rid: rc.rid, Count: count} } } -func (rc *RouterControll) ChangeCmd(model tea.Model) tea.Cmd { +func (rc *RouterControll) Change(model *util.Model) tea.Cmd { return func() tea.Msg { return ChangeMsg{rid: rc.rid, Model: model} } } diff --git a/ui/tui/models/components/router/handle.go b/ui/tui/models/components/router/handle.go new file mode 100644 index 0000000..37329ea --- /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 (r *Router) handlePush(msg PushMsg) tea.Cmd { + // blur recent model + (*r.activeModelGet()).Blur() + // push new model + r.model_stack = append(r.model_stack, msg.Model) + // initialize pushed model + return r.activeModelInit() +} + +// handle PopMsg +func (r *Router) handlePop(msg PopMsg) tea.Cmd { + // pop and blur old models + for range msg.Count { + if len(r.model_stack) <= 1 { + break + } + (*r.activeModelPop()).Blur() + } + // focus active model + return r.activeModelFocus() +} + +// handle ChangeMsg +func (r *Router) handleChange(msg ChangeMsg) tea.Cmd { + // destroy recent model + (*r.activeModelGet()).Blur() + // set new model + r.activeModelSet(msg.Model) + // initialize set model + return r.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..2a60240 100644 --- a/ui/tui/models/components/router/msg.go +++ b/ui/tui/models/components/router/msg.go @@ -6,27 +6,22 @@ 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 invoked messages +// Model-RouterControll -> Router + type PushMsg struct { rid int - Model tea.Model + Model *util.Model } type PopMsg struct { rid int @@ -34,22 +29,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..c816029 100644 --- a/ui/tui/models/components/router/router.go +++ b/ui/tui/models/components/router/router.go @@ -6,6 +6,7 @@ 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" ) @@ -15,16 +16,14 @@ var routerId = 1 type Router 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) (Router, RouterControll) { routerId++ return Router{ id: routerId - 1, - model_stack: []tea.Model{initial_model}, + model_stack: []*util.Model{initial_model}, }, RouterControll{ rid: routerId - 1, } @@ -32,111 +31,63 @@ func New(initial_model tea.Model) (Router, RouterControll) { func (r Router) Init() tea.Cmd { return tea.Batch( - r.activeModelGet().Init(), + (*r.activeModelGet()).Init(), r.activeModelUpdate(InitMsg{RouterControll: RouterControll{rid: r.id}}), + r.activeModelUpdate(tea.WindowSizeMsg(r.size)), ) } -func (r Router) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd +func (r *Router) 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)) + // pass window size messages + cmd = r.activeModelUpdate(msg) } else if r.isMsgOwner(msg) { // handle controll messages meant for this router switch msg := msg.(type) { case PushMsg: - cmds = append(cmds, r.handlePush(msg)...) + cmd = r.handlePush(msg) case PopMsg: - cmds = append(cmds, r.handlePop(msg)...) + cmd = r.handlePop(msg) case ChangeMsg: - cmds = append(cmds, r.handleChange(msg)...) + cmd = r.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 = r.activeModelUpdate(msg) } } else { - // pass other update - cmds = append(cmds, r.activeModelUpdate(msg)) + // pass other messages + cmd = r.activeModelUpdate(msg) } - return r, tea.Batch(cmds...) + return cmd } func (r Router) View() string { - return r.activeModelGet().View() + 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))) +func (r *Router) Focus() (tea.Cmd, help.KeyMap) { + return (*r.activeModelGet()).Focus() } -// 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 (r *Router) Blur() { + (*r.activeModelGet()).Blur() } -// 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))) -} - -// handle SuspendMsg (from other router) -func (r *Router) handleSuspend(_ SuspendMsg) tea.Cmd { - return r.activeModelUpdate(SuspendMsg{rid: r.id}) -} - -// handle ResumeMsg (from other router) -func (r *Router) handleResume(_ ResumeMsg) tea.Cmd { - return r.activeModelUpdate(ResumeMsg{rid: r.id}) -} - -// handle DestroyMsg (from other router) -func (r *Router) handleDestroy(_ DestroyMsg) tea.Cmd { - return r.activeModelUpdate(DestroyMsg{rid: r.id}) -} +// *Model implements util.Model +var _ util.Model = (*Router)(nil) 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..3c5bf5a --- /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 (r *Router) activeModelGet() *util.Model { + return r.model_stack[len(r.model_stack)-1] +} + +func (r *Router) activeModelSet(model *util.Model) { + r.model_stack[len(r.model_stack)-1] = model +} + +func (r *Router) activeModelPop() *util.Model { + model := r.activeModelGet() + r.model_stack = r.model_stack[:len(r.model_stack)-1] + return model +} + +func (r *Router) activeModelUpdate(msg tea.Msg) tea.Cmd { + return (*r.activeModelGet()).Update(msg) +} + +func (r *Router) activeModelFocus() tea.Cmd { + cmd, keyMap := (*r.activeModelGet()).Focus() + return tea.Batch(cmd, util.AnnounceKeyMapCmd(keyMap)) +} + +func (r *Router) activeModelInit() tea.Cmd { + return tea.Sequence( + (*r.activeModelGet()).Init(), + r.activeModelUpdate(InitMsg{RouterControll: RouterControll{rid: r.id}}), + r.activeModelUpdate(tea.WindowSizeMsg(r.size)), + r.activeModelFocus(), + ) +} From 4dcbcef1795283b49ec3ccfa4833f89b655636d1 Mon Sep 17 00:00:00 2001 From: Jannes Dailidow Date: Thu, 22 Jan 2026 00:06:22 +0100 Subject: [PATCH 3/4] hide system goroutines when debugging --- .vscode/launch.json | 1 + 1 file changed, 1 insertion(+) 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" ] From e085e866d64b11f84f312dc7d48d9bc33be34ef5 Mon Sep 17 00:00:00 2001 From: Jannes Dailidow Date: Thu, 22 Jan 2026 00:06:59 +0100 Subject: [PATCH 4/4] restructured tui base content --- ui/tui/models/components/router/controll.go | 20 ++- ui/tui/models/components/router/handle.go | 24 ++-- ui/tui/models/components/router/msg.go | 4 +- ui/tui/models/components/router/router.go | 56 ++++---- ui/tui/models/components/router/util.go | 32 ++--- ui/tui/models/components/stack/new.go | 6 + ui/tui/models/views/content/content.go | 152 ++++++++++++++++++++ ui/tui/models/views/root/root.go | 114 ++------------- ui/tui/util/focus.go | 10 +- 9 files changed, 239 insertions(+), 179 deletions(-) create mode 100644 ui/tui/models/views/content/content.go diff --git a/ui/tui/models/components/router/controll.go b/ui/tui/models/components/router/controll.go index c9691fd..d9047af 100644 --- a/ui/tui/models/components/router/controll.go +++ b/ui/tui/models/components/router/controll.go @@ -3,28 +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" ) -type RouterControll struct { +type Controll struct { rid int } -func (rc *RouterControll) Push(model *util.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) Pop(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) Change(model *util.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 index 37329ea..f248b73 100644 --- a/ui/tui/models/components/router/handle.go +++ b/ui/tui/models/components/router/handle.go @@ -8,34 +8,34 @@ import ( ) // handle PushMsg -func (r *Router) handlePush(msg PushMsg) tea.Cmd { +func (m *Model) handlePush(msg PushMsg) tea.Cmd { // blur recent model - (*r.activeModelGet()).Blur() + (*m.activeModelGet()).Blur() // push new model - r.model_stack = append(r.model_stack, msg.Model) + m.model_stack = append(m.model_stack, msg.Model) // initialize pushed model - return r.activeModelInit() + return m.activeModelInit() } // handle PopMsg -func (r *Router) handlePop(msg PopMsg) tea.Cmd { +func (m *Model) handlePop(msg PopMsg) tea.Cmd { // pop and blur old models for range msg.Count { - if len(r.model_stack) <= 1 { + if len(m.model_stack) <= 1 { break } - (*r.activeModelPop()).Blur() + (*m.activeModelPop()).Blur() } // focus active model - return r.activeModelFocus() + return m.activeModelFocus() } // handle ChangeMsg -func (r *Router) handleChange(msg ChangeMsg) tea.Cmd { +func (m *Model) handleChange(msg ChangeMsg) tea.Cmd { // destroy recent model - (*r.activeModelGet()).Blur() + (*m.activeModelGet()).Blur() // set new model - r.activeModelSet(msg.Model) + m.activeModelSet(msg.Model) // initialize set model - return r.activeModelInit() + return m.activeModelInit() } diff --git a/ui/tui/models/components/router/msg.go b/ui/tui/models/components/router/msg.go index 2a60240..645adff 100644 --- a/ui/tui/models/components/router/msg.go +++ b/ui/tui/models/components/router/msg.go @@ -3,8 +3,6 @@ // 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/toeirei/keymaster/ui/tui/util" ) @@ -13,7 +11,7 @@ import ( // Router -> Model type InitMsg struct { - RouterControll RouterControll + RouterControll Controll } // RouterControll invoked messages diff --git a/ui/tui/models/components/router/router.go b/ui/tui/models/components/router/router.go index c816029..b48da9f 100644 --- a/ui/tui/models/components/router/router.go +++ b/ui/tui/models/components/router/router.go @@ -3,88 +3,86 @@ // 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 []*util.Model } -func New(initial_model *util.Model) (Router, RouterControll) { +func New(initial_model *util.Model) (*Model, Controll) { routerId++ - return Router{ + return &Model{ id: routerId - 1, model_stack: []*util.Model{initial_model}, - }, RouterControll{ + }, 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}}), - r.activeModelUpdate(tea.WindowSizeMsg(r.size)), + (*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.Cmd { +func (m *Model) Update(msg tea.Msg) tea.Cmd { var cmd tea.Cmd - if r.size.Update(msg) { + if m.size.Update(msg) { // pass window size messages - cmd = r.activeModelUpdate(msg) - } else if r.isMsgOwner(msg) { + cmd = m.activeModelUpdate(msg) + } else if m.isMsgOwner(msg) { // handle controll messages meant for this router switch msg := msg.(type) { case PushMsg: - cmd = r.handlePush(msg) + cmd = m.handlePush(msg) case PopMsg: - cmd = r.handlePop(msg) + cmd = m.handlePop(msg) case ChangeMsg: - cmd = r.handleChange(msg) + cmd = m.handleChange(msg) } } else if IsRouterMsg(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 = r.activeModelUpdate(msg) + cmd = m.activeModelUpdate(msg) } } else { // pass other messages - cmd = r.activeModelUpdate(msg) + cmd = m.activeModelUpdate(msg) } return cmd } -func (r Router) View() string { - return (*r.activeModelGet()).View() +func (m Model) View() string { + return (*m.activeModelGet()).View() } -func (r *Router) Focus() (tea.Cmd, help.KeyMap) { - return (*r.activeModelGet()).Focus() +func (m *Model) Focus() (tea.Cmd, help.KeyMap) { + return (*m.activeModelGet()).Focus() } -func (r *Router) Blur() { - (*r.activeModelGet()).Blur() +func (m *Model) Blur() { + (*m.activeModelGet()).Blur() } // *Model implements util.Model -var _ util.Model = (*Router)(nil) +var _ util.Model = (*Model)(nil) -func (r *Router) isMsgOwner(msg tea.Msg) bool { +func (m *Model) isMsgOwner(msg tea.Msg) bool { rmsg, ok := msg.(RouterMsg) - return ok && rmsg.routerId() == r.id + return ok && rmsg.routerId() == m.id } func IsRouterMsg(msg tea.Msg) bool { diff --git a/ui/tui/models/components/router/util.go b/ui/tui/models/components/router/util.go index 3c5bf5a..4f6e0d5 100644 --- a/ui/tui/models/components/router/util.go +++ b/ui/tui/models/components/router/util.go @@ -8,34 +8,34 @@ import ( "github.com/toeirei/keymaster/ui/tui/util" ) -func (r *Router) activeModelGet() *util.Model { - return r.model_stack[len(r.model_stack)-1] +func (m *Model) activeModelGet() *util.Model { + return m.model_stack[len(m.model_stack)-1] } -func (r *Router) activeModelSet(model *util.Model) { - r.model_stack[len(r.model_stack)-1] = model +func (m *Model) activeModelSet(model *util.Model) { + m.model_stack[len(m.model_stack)-1] = model } -func (r *Router) activeModelPop() *util.Model { - model := r.activeModelGet() - r.model_stack = r.model_stack[:len(r.model_stack)-1] +func (m *Model) activeModelPop() *util.Model { + model := m.activeModelGet() + m.model_stack = m.model_stack[:len(m.model_stack)-1] return model } -func (r *Router) activeModelUpdate(msg tea.Msg) tea.Cmd { - return (*r.activeModelGet()).Update(msg) +func (m *Model) activeModelUpdate(msg tea.Msg) tea.Cmd { + return (*m.activeModelGet()).Update(msg) } -func (r *Router) activeModelFocus() tea.Cmd { - cmd, keyMap := (*r.activeModelGet()).Focus() +func (m *Model) activeModelFocus() tea.Cmd { + cmd, keyMap := (*m.activeModelGet()).Focus() return tea.Batch(cmd, util.AnnounceKeyMapCmd(keyMap)) } -func (r *Router) activeModelInit() tea.Cmd { +func (m *Model) activeModelInit() tea.Cmd { return tea.Sequence( - (*r.activeModelGet()).Init(), - r.activeModelUpdate(InitMsg{RouterControll: RouterControll{rid: r.id}}), - r.activeModelUpdate(tea.WindowSizeMsg(r.size)), - r.activeModelFocus(), + (*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}