DEVELOPMENT ENVIRONMENT

~liljamo/tamma

5b814f37be149156d3ad21462ebd9fe78ac9a36f — Jonni Liljamo 16 days ago 57e0bc5
feat: change "host" to more generic "target"
4 files changed, 57 insertions(+), 57 deletions(-)

M main.go
M types/action.go
M types/keymaps.go
R types/{host.go => target.go}
M main.go => main.go +37 -37
@@ 25,7 25,7 @@ import (
type state int

const (
	hostSelect state = iota
	targetSelect state = iota
	actionSelect
)



@@ 34,8 34,8 @@ type model struct {
	height         int
	err            string
	state          state
	hostList       list.Model
	selectedHost   types.HostItem
	targetList     list.Model
	selectedTarget types.TargetItem
	actionList     list.Model
	selectedAction types.ActionItem
	actionListKeys *types.ActionListKeyMap


@@ 59,16 59,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
			return m, tea.Quit
		case "enter":
			switch m.state {
			case hostSelect:
				i, ok := m.hostList.SelectedItem().(types.HostItem)
			case targetSelect:
				i, ok := m.targetList.SelectedItem().(types.TargetItem)
				if ok {
					m.selectedHost = i
					m.selectedTarget = i
					m.state = actionSelect
				}
			case actionSelect:
				a, ok := m.actionList.SelectedItem().(types.ActionItem)
				if ok {
					execString, err := a.ExecString(m.selectedHost)
					execString, err := a.ExecString(m.selectedTarget)
					if err != nil {
						m.err = err.Error()
					} else {


@@ 81,12 81,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
			}
		case "q":
			switch m.state {
			case hostSelect:
			case targetSelect:
				return m, tea.Quit
			case actionSelect:
				if m.actionList.FilterState() != list.Filtering {
					m.actionList.ResetSelected()
					m.state = hostSelect
					m.state = targetSelect
				}
			}
		case "c":


@@ 96,7 96,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
					if os.Getenv("WAYLAND_DISPLAY") != "" {
						a, ok := m.actionList.SelectedItem().(types.ActionItem)
						if ok {
							execString, err := a.ExecString(m.selectedHost)
							execString, err := a.ExecString(m.selectedTarget)
							if err != nil {
								m.err = err.Error()
							} else {


@@ 117,9 117,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	}

	switch m.state {
	case hostSelect:
	case targetSelect:
		var cmd tea.Cmd
		m.hostList, cmd = m.hostList.Update(msg)
		m.targetList, cmd = m.targetList.Update(msg)
		return m, cmd
	case actionSelect:
		var cmd tea.Cmd


@@ 135,8 135,8 @@ func (m model) View() string {

	msg := "\n"
	switch m.state {
	case hostSelect:
		msg += m.hostList.View()
	case targetSelect:
		msg += m.targetList.View()
		msg += "\n"
	case actionSelect:
		alb := m.actionList.View()


@@ 147,7 147,7 @@ func (m model) View() string {
		// have no results, thus this check is needed.
		selectedAction, ok := m.actionList.SelectedItem().(types.ActionItem)
		if ok {
			execString, err := selectedAction.ExecString(m.selectedHost)
			execString, err := selectedAction.ExecString(m.selectedTarget)
			if err != nil {
				msg += fmt.Sprintf("error in ExecString(): %s\n", err.Error())
			}


@@ 164,7 164,7 @@ func (m model) View() string {
}

type config struct {
	Hosts   []types.HostItem   `yaml:"hosts"`
	Targets []types.TargetItem `yaml:"targets"`
	Actions []types.ActionItem `yaml:"actions"`
}



@@ 183,34 183,34 @@ func main() {
		os.Exit(1)
	}

	hostListItems := []list.Item{}
	for _, i := range config.Hosts {
		hostListItems = append(hostListItems, i)
	targetListItems := []list.Item{}
	for _, i := range config.Targets {
		targetListItems = append(targetListItems, i)
	}

	hostListH := len(hostListItems)
	if hostListH > 10 {
		hostListH = 10
	targetListH := len(targetListItems)
	if targetListH > 10 {
		targetListH = 10
	}
	hostListKeys := types.NewHostListKeyMap()
	hostList := list.New(hostListItems, types.HostItemDelegate{}, 48, hostListH+4)
	hostList.Title = "Select host:"
	hostList.SetShowHelp(true)
	hostList.SetFilteringEnabled(true)
	hostList.SetShowStatusBar(false)
	hostList.DisableQuitKeybindings()
	hostList.AdditionalShortHelpKeys = func() []key.Binding {
	targetListKeys := types.NewTargetListKeyMap()
	targetList := list.New(targetListItems, types.TargetItemDelegate{}, 48, targetListH+4)
	targetList.Title = "Select target:"
	targetList.SetShowHelp(true)
	targetList.SetFilteringEnabled(true)
	targetList.SetShowStatusBar(false)
	targetList.DisableQuitKeybindings()
	targetList.AdditionalShortHelpKeys = func() []key.Binding {
		return []key.Binding{
			hostListKeys.Quit,
			targetListKeys.Quit,
		}
	}
	hostList.AdditionalFullHelpKeys = func() []key.Binding {
	targetList.AdditionalFullHelpKeys = func() []key.Binding {
		return []key.Binding{
			hostListKeys.Quit,
			targetListKeys.Quit,
		}
	}
	hostList.Styles.Title = styles.ListTitle
	hostList.Styles.PaginationStyle = styles.ListPaginaton
	targetList.Styles.Title = styles.ListTitle
	targetList.Styles.PaginationStyle = styles.ListPaginaton

	actionListItems := []list.Item{}
	for _, i := range config.Actions {


@@ 247,8 247,8 @@ func main() {
	actionList.Styles.PaginationStyle = styles.ListPaginaton

	p := tea.NewProgram(model{
		state:          hostSelect,
		hostList:       hostList,
		state:          targetSelect,
		targetList:     targetList,
		actionList:     actionList,
		actionListKeys: actionListKeys,
	}, tea.WithAltScreen())

M types/action.go => types/action.go +3 -3
@@ 1,5 1,5 @@
/*
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2025 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under GPL-3.0-only, see NOTICE and LICENSE for
 * more information.


@@ 27,8 27,8 @@ type ActionItem struct {
}

// ExecString returns ExecTemplate with template variables filled in.
func (i ActionItem) ExecString(host HostItem) (string, error) {
	i.A["host"] = host
func (i ActionItem) ExecString(target TargetItem) (string, error) {
	i.A["target"] = target
	tmpl, err := template.New(i.Name).Parse(i.ExecTemplate)
	if err != nil {
		return "", err

M types/keymaps.go => types/keymaps.go +6 -6
@@ 1,5 1,5 @@
/*
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2025 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under GPL-3.0-only, see NOTICE and LICENSE for
 * more information.


@@ 11,14 11,14 @@ import (
	"github.com/charmbracelet/bubbles/key"
)

// HostListKeyMap defines key bindings for the host list.
type HostListKeyMap struct {
// TargetListKeyMap defines key bindings for the target list.
type TargetListKeyMap struct {
	Quit key.Binding
}

// NewHostListKeyMap returns a new HostListKeyMap.
func NewHostListKeyMap() *HostListKeyMap {
	return &HostListKeyMap{
// NewTargetListKeyMap returns a new TargetListKeyMap.
func NewTargetListKeyMap() *TargetListKeyMap {
	return &TargetListKeyMap{
		Quit: key.NewBinding(
			key.WithKeys("q"),
			key.WithHelp("q", "quit"),

R types/host.go => types/target.go +11 -11
@@ 1,5 1,5 @@
/*
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2025 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under GPL-3.0-only, see NOTICE and LICENSE for
 * more information.


@@ 17,31 17,31 @@ import (
	tea "github.com/charmbracelet/bubbletea"
)

// HostItem represents a host item.
type HostItem struct {
// TargetItem represents a target item.
type TargetItem struct {
	Name string
	IP   string
	Data map[string]interface{}
}

// FilterValue returns the value to filter on.
func (i HostItem) FilterValue() string { return i.Name }
func (i TargetItem) FilterValue() string { return i.Name }

// HostItemDelegate represents the visual of a HostItem.
type HostItemDelegate struct{}
// TargetItemDelegate represents the visual of a TargetItem.
type TargetItemDelegate struct{}

// Height returns the needed height.
func (d HostItemDelegate) Height() int { return 1 }
func (d TargetItemDelegate) Height() int { return 1 }

// Spacing returns the needed spacing.
func (d HostItemDelegate) Spacing() int { return 0 }
func (d TargetItemDelegate) Spacing() int { return 0 }

// Update is the tea update loop.
func (d HostItemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }
func (d TargetItemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }

// Render renders the component.
func (d HostItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
	i, ok := listItem.(HostItem)
func (d TargetItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
	i, ok := listItem.(TargetItem)
	if !ok {
		return
	}