Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .vscode/keymaster.code-snippets
Original file line number Diff line number Diff line change
@@ -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"
}
}
1 change: 1 addition & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
Expand Down
25 changes: 13 additions & 12 deletions ui/tui/models/components/router/controll.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// }
41 changes: 41 additions & 0 deletions ui/tui/models/components/router/handle.go
Original file line number Diff line number Diff line change
@@ -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()
}
28 changes: 0 additions & 28 deletions ui/tui/models/components/router/helper.go

This file was deleted.

39 changes: 12 additions & 27 deletions ui/tui/models/components/router/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,38 @@
// 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
Count int
}
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
}
135 changes: 42 additions & 93 deletions ui/tui/models/components/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading
Loading