tui: a library page (5) — the shared instructions, MCP servers and skills magpie gives the agents, and RTK: ↵ lists the agents that can have one, each switched on or off in place; e edits the instructions in $EDITOR; the servers and skills found in the agents are listed too, i brings one into the library; d twice takes one out. The same data as the app's Library. Asked on Discord
This commit is contained in:
3 files changed
+475
-23
No files matched your search
@@ -0,0 +1,429 @@
|
||||
package tui
|
||||
|
||||
// The library page: the instructions, MCP servers and skills magpie gives
|
||||
// the agents, which agents get each, what the agents have that the library
|
||||
// doesn't, and RTK's hook in each — what the app's Library view does, in
|
||||
// the terminal.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/yetone/magpie/internal/agent"
|
||||
"github.com/yetone/magpie/internal/library"
|
||||
)
|
||||
|
||||
// libRow is one line of the page.
|
||||
type libRow struct {
|
||||
kind string // instructions, mcp, skill, found-mcp, found-skill, rtk
|
||||
name string
|
||||
about string // what it runs, or what it is
|
||||
on []string // the agents that get it (or have it, when found)
|
||||
own bool // a server the agent's app puts in itself: nothing to bring in
|
||||
bad int // agents it couldn't be given to
|
||||
}
|
||||
|
||||
// libKinds are what the library can give, by row kind.
|
||||
var libKinds = map[string]string{"instructions": "instructions", "mcp": "mcp", "skill": "skills"}
|
||||
|
||||
func (m *model) reloadLibrary() {
|
||||
m.lib, m.libAgents, m.libErr = nil, nil, ""
|
||||
v, err := library.Read(nil)
|
||||
if err != nil {
|
||||
m.libErr = err.Error()
|
||||
return
|
||||
}
|
||||
m.libView = v
|
||||
m.libAgents = v.Agents
|
||||
ins := libRow{kind: "instructions", name: "shared instructions"}
|
||||
if lines := strings.TrimRight(v.Instructions.Shared, "\n"); strings.TrimSpace(lines) == "" {
|
||||
ins.about = "none yet · e writes them"
|
||||
} else {
|
||||
ins.about = fmt.Sprintf("%d line%s", strings.Count(lines, "\n")+1, plural(strings.Count(lines, "\n")+1))
|
||||
}
|
||||
for _, a := range v.Instructions.Agents {
|
||||
if a.On {
|
||||
ins.on = append(ins.on, a.Agent)
|
||||
}
|
||||
if a.Edited {
|
||||
ins.bad++
|
||||
}
|
||||
}
|
||||
m.lib = append(m.lib, ins)
|
||||
for _, s := range v.Servers {
|
||||
what := s.URL
|
||||
if s.Command != "" {
|
||||
what = strings.TrimSpace(s.Command + " " + strings.Join(s.Args, " "))
|
||||
}
|
||||
m.lib = append(m.lib, libRow{kind: "mcp", name: s.Name, about: what, on: s.Agents, bad: len(s.Problems)})
|
||||
}
|
||||
for _, s := range v.Skills {
|
||||
r := libRow{kind: "skill", name: s.Name, about: s.Description, on: s.Agents, bad: len(s.Problems)}
|
||||
if s.Missing {
|
||||
r.about, r.bad = "its folder is gone", r.bad+1
|
||||
}
|
||||
m.lib = append(m.lib, r)
|
||||
}
|
||||
for _, f := range v.FoundServers {
|
||||
m.lib = append(m.lib, libRow{kind: "found-mcp", name: f.Server.Name, on: f.Server.Agents, own: f.Own})
|
||||
}
|
||||
for _, f := range v.FoundSkills {
|
||||
m.lib = append(m.lib, libRow{kind: "found-skill", name: f.Name, about: f.Description, on: f.Agents})
|
||||
}
|
||||
if rv := library.ReadRTK(); rv.Path != "" || len(rv.Agents) > 0 {
|
||||
r := libRow{kind: "rtk", name: "RTK"}
|
||||
switch {
|
||||
case rv.Path == "":
|
||||
r.about = "not installed · " + rv.URL
|
||||
case rv.Gain != nil:
|
||||
r.about = fmt.Sprintf("%s · %s tokens saved", rv.Version, fmtTokens(int(rv.Gain.Saved)))
|
||||
default:
|
||||
r.about = rv.Version
|
||||
}
|
||||
for _, a := range rv.Agents {
|
||||
if a.On {
|
||||
r.on = append(r.on, a.ID)
|
||||
}
|
||||
}
|
||||
m.lib = append(m.lib, r)
|
||||
}
|
||||
m.lrow = clamp(m.lrow, len(m.lib))
|
||||
}
|
||||
|
||||
// libName is an agent's name, by its id.
|
||||
func (m model) libName(id string) string {
|
||||
for _, a := range m.libAgents {
|
||||
if a.ID == id {
|
||||
return a.Name
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (m model) updateLibrary(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
n := len(m.lib)
|
||||
key := msg.String()
|
||||
if key != "d" {
|
||||
m.confirm = ""
|
||||
}
|
||||
switch key {
|
||||
case "j", "down":
|
||||
if n > 0 {
|
||||
m.lrow = (m.lrow + 1) % n
|
||||
}
|
||||
return m, nil
|
||||
case "k", "up":
|
||||
if n > 0 {
|
||||
m.lrow = (m.lrow + n - 1) % n
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
if n == 0 {
|
||||
return m, nil
|
||||
}
|
||||
r := m.lib[m.lrow]
|
||||
switch key {
|
||||
case "enter", " ":
|
||||
switch r.kind {
|
||||
case "instructions", "mcp", "skill", "rtk":
|
||||
m.openLibAgents(r)
|
||||
case "found-mcp", "found-skill":
|
||||
m.flash, m.flashOK = "i brings "+r.name+" into the library, for magpie to give it to other agents too", true
|
||||
}
|
||||
case "e":
|
||||
if r.kind != "instructions" {
|
||||
return m, nil
|
||||
}
|
||||
return m, editInstructions()
|
||||
case "i":
|
||||
if r.own {
|
||||
m.flash, m.flashOK = r.name+" is put there by the agent's app itself, each time it starts: it stays as it is", false
|
||||
return m, nil
|
||||
}
|
||||
var do func(string) (*library.Result, error)
|
||||
switch r.kind {
|
||||
case "found-mcp":
|
||||
do = library.ImportServer
|
||||
case "found-skill":
|
||||
do = library.ImportSkill
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
return m, libCmd(func() (*library.Result, error) { return do(r.name) }, "brought "+r.name+" into the library")
|
||||
case "d":
|
||||
var do func(string) (*library.Result, error)
|
||||
switch r.kind {
|
||||
case "mcp":
|
||||
do = library.RemoveServer
|
||||
case "skill":
|
||||
do = library.RemoveSkill
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
if m.confirm != "library/"+r.kind+"/"+r.name {
|
||||
m.confirm = "library/" + r.kind + "/" + r.name
|
||||
m.flash, m.flashOK = "press d again to take "+r.name+" out of the library and every agent", false
|
||||
return m, nil
|
||||
}
|
||||
m.confirm = ""
|
||||
return m, libCmd(func() (*library.Result, error) { return do(r.name) }, "removed "+r.name)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// libCmd runs a library change, saying what it did to the agents.
|
||||
func libCmd(do func() (*library.Result, error), done string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
res, err := do()
|
||||
if err != nil {
|
||||
return flashMsg{text: err.Error()}
|
||||
}
|
||||
text, ok := libSaid(res, done)
|
||||
return flashMsg{text: text, ok: ok}
|
||||
}
|
||||
}
|
||||
|
||||
// libSaid is a change's result in a line.
|
||||
func libSaid(res *library.Result, done string) (string, bool) {
|
||||
if len(res.Changed) > 0 {
|
||||
done += " · written into " + strings.Join(res.Changed, ", ")
|
||||
}
|
||||
for _, p := range res.Problems {
|
||||
done += " · " + p.Agent + ": " + p.Error
|
||||
}
|
||||
return done, len(res.Problems) == 0
|
||||
}
|
||||
|
||||
// libOptions are the agents that can get a row's kind, those that get it
|
||||
// marked.
|
||||
func (m model) libOptions(r libRow) []agent.Option {
|
||||
var out []agent.Option
|
||||
add := func(id, name string) {
|
||||
note := "○ off"
|
||||
if slices.Contains(r.on, id) {
|
||||
note = "● on"
|
||||
}
|
||||
out = append(out, agent.Option{Value: id, Note: note + " " + name})
|
||||
}
|
||||
if r.kind == "rtk" {
|
||||
for _, a := range library.ReadRTK().Agents {
|
||||
add(a.ID, a.Name)
|
||||
}
|
||||
return out
|
||||
}
|
||||
for _, a := range m.libAgents {
|
||||
has := map[string]bool{"instructions": a.Instructions != "", "mcp": a.MCP != "", "skill": a.Skills != ""}[r.kind]
|
||||
if has {
|
||||
add(a.ID, a.Name)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// openLibAgents lists the agents a row can go to; enter gives it to one or
|
||||
// takes it away, and the list stays open.
|
||||
func (m *model) openLibAgents(r libRow) {
|
||||
crumbs := []string{"library", r.name, "agents"}
|
||||
m.pk = picker{
|
||||
crumbs: crumbs,
|
||||
input: newInput("filter agents"),
|
||||
items: m.libOptions(r),
|
||||
empty: "no agent here has a place for it",
|
||||
toggle: func(id string) ([]agent.Option, string, bool) {
|
||||
on := !slices.Contains(r.on, id)
|
||||
var (
|
||||
text string
|
||||
ok = true
|
||||
)
|
||||
if r.kind == "rtk" {
|
||||
v, err := library.SetRTK(id, on)
|
||||
if err != nil {
|
||||
return nil, err.Error(), false
|
||||
}
|
||||
r.on = nil
|
||||
for _, a := range v.Agents {
|
||||
if a.On {
|
||||
r.on = append(r.on, a.ID)
|
||||
}
|
||||
}
|
||||
text = "RTK " + map[bool]string{true: "on", false: "off"}[on] + " for " + m.libName(id)
|
||||
if len(v.Restart) > 0 {
|
||||
text += " · restart " + strings.Join(v.Restart, ", ") + " for it to take effect"
|
||||
}
|
||||
} else {
|
||||
agents := slices.Clone(r.on)
|
||||
if on {
|
||||
agents = append(agents, id)
|
||||
} else {
|
||||
agents = slices.DeleteFunc(agents, func(a string) bool { return a == id })
|
||||
}
|
||||
var (
|
||||
res *library.Result
|
||||
err error
|
||||
)
|
||||
switch r.kind {
|
||||
case "instructions":
|
||||
res, err = library.SaveInstructions(library.InstructionsChange{Agents: agents})
|
||||
case "mcp":
|
||||
res, err = library.ServerAgents(r.name, agents)
|
||||
case "skill":
|
||||
res, err = library.SkillAgents(r.name, agents)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err.Error(), false
|
||||
}
|
||||
r.on = agents
|
||||
verb := "given to "
|
||||
if !on {
|
||||
verb = "taken from "
|
||||
}
|
||||
text, ok = libSaid(&library.Result{Problems: res.Problems}, r.name+" "+verb+m.libName(id))
|
||||
}
|
||||
return m.libOptions(r), text, ok
|
||||
},
|
||||
}
|
||||
m.pk.refilter()
|
||||
m.mode = modePick
|
||||
m.back = modeList
|
||||
}
|
||||
|
||||
// editedMsg is the shared instructions as the editor left them.
|
||||
type editedMsg struct {
|
||||
text string
|
||||
err error
|
||||
}
|
||||
|
||||
// editInstructions opens the shared instructions in $VISUAL or $EDITOR
|
||||
// and saves what it leaves, which writes it into the agents that get it.
|
||||
func editInstructions() tea.Cmd {
|
||||
v, err := library.ReadInstructions()
|
||||
if err != nil {
|
||||
return func() tea.Msg { return flashMsg{text: err.Error()} }
|
||||
}
|
||||
f, err := os.CreateTemp("", "magpie-instructions-*.md")
|
||||
if err != nil {
|
||||
return func() tea.Msg { return flashMsg{text: err.Error()} }
|
||||
}
|
||||
f.WriteString(v.Shared)
|
||||
f.Close()
|
||||
ed := os.Getenv("VISUAL")
|
||||
if ed == "" {
|
||||
ed = os.Getenv("EDITOR")
|
||||
}
|
||||
if ed == "" {
|
||||
ed = "vi"
|
||||
if runtime.GOOS == "windows" {
|
||||
ed = "notepad"
|
||||
}
|
||||
}
|
||||
// the editor may come with flags: code -w
|
||||
words := strings.Fields(ed)
|
||||
c := exec.Command(words[0], append(words[1:], f.Name())...)
|
||||
return tea.ExecProcess(c, func(err error) tea.Msg {
|
||||
defer os.Remove(f.Name())
|
||||
if err != nil {
|
||||
return editedMsg{err: err}
|
||||
}
|
||||
b, err := os.ReadFile(f.Name())
|
||||
if err != nil {
|
||||
return editedMsg{err: err}
|
||||
}
|
||||
if string(b) == v.Shared {
|
||||
return flashMsg{text: "the instructions are as they were", ok: true}
|
||||
}
|
||||
return editedMsg{text: string(b)}
|
||||
})
|
||||
}
|
||||
|
||||
func saveInstructions(text string) tea.Cmd {
|
||||
return libCmd(func() (*library.Result, error) {
|
||||
return library.SaveInstructions(library.InstructionsChange{Shared: &text})
|
||||
}, "instructions saved")
|
||||
}
|
||||
|
||||
func (m model) viewLibrary() string {
|
||||
var b strings.Builder
|
||||
b.WriteString(m.header())
|
||||
b.WriteString("\n\n")
|
||||
if m.libErr != "" {
|
||||
b.WriteString(pad + " " + sBad.Render(m.libErr))
|
||||
return b.String()
|
||||
}
|
||||
nameW := 0
|
||||
for _, r := range m.lib {
|
||||
nameW = max(nameW, len([]rune(r.name)))
|
||||
}
|
||||
nameW = min(nameW, 32)
|
||||
heads := map[string]string{
|
||||
"instructions": "instructions · what every agent reads before a conversation",
|
||||
"mcp": "mcp servers · magpie library mcp add <name> <url | command> adds one",
|
||||
"skill": "skills · added from the app's market, or a folder magpie keeps",
|
||||
"found-mcp": "mcp servers in your agents, not in the library",
|
||||
"found-skill": "skills in your agents, not in the library",
|
||||
"rtk": "rtk · its hook in each agent, by RTK's own installer",
|
||||
}
|
||||
// the rows around the cursor, as many as fit
|
||||
visible := max(4, m.h-6)
|
||||
start := 0
|
||||
if m.lrow >= visible-2 {
|
||||
start = m.lrow - visible + 3
|
||||
}
|
||||
last, lines := "", 0
|
||||
for i := start; i < len(m.lib) && lines < visible; i++ {
|
||||
r := m.lib[i]
|
||||
if r.kind != last {
|
||||
if last != "" || start > 0 {
|
||||
b.WriteString("\n")
|
||||
lines++
|
||||
}
|
||||
b.WriteString(pad + " " + sFaint.Render(heads[r.kind]) + "\n")
|
||||
lines++
|
||||
last = r.kind
|
||||
}
|
||||
marker, name := " ", sText.Render(padRight(trunc(r.name, nameW), nameW))
|
||||
if i == m.lrow {
|
||||
marker, name = sCursor.Render("▸ "), sNameOn.Render(padRight(trunc(r.name, nameW), nameW))
|
||||
}
|
||||
var agents []string
|
||||
for _, id := range r.on {
|
||||
agents = append(agents, m.libName(id))
|
||||
}
|
||||
on := sMuted.Render(strings.Join(agents, ", "))
|
||||
switch {
|
||||
case r.own:
|
||||
on = sFaint.Render(strings.Join(agents, ", ") + "'s own")
|
||||
case len(agents) == 0 && strings.HasPrefix(r.kind, "found-"):
|
||||
case len(agents) == 0:
|
||||
on = sFaint.Render("no agent")
|
||||
}
|
||||
line := pad + marker + name + " " + on
|
||||
if r.bad > 0 {
|
||||
line += " " + sBad.Render(fmt.Sprintf("! %d", r.bad))
|
||||
}
|
||||
if r.about != "" {
|
||||
if room := m.w - lipgloss.Width(line) - 4; room > 8 {
|
||||
line += " " + sFaint.Render(trunc(r.about, room))
|
||||
}
|
||||
}
|
||||
b.WriteString(line + "\n")
|
||||
lines++
|
||||
}
|
||||
b.WriteString("\n" + pad + " " + sFaint.Render("kept in "+tilde(m.libView.Dir)))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// trunc cuts s to n runes, with an ellipsis.
|
||||
func trunc(s string, n int) string {
|
||||
s = strings.Join(strings.Fields(s), " ")
|
||||
if r := []rune(s); len(r) > n {
|
||||
return string(r[:max(1, n-1)]) + "…"
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -28,9 +28,10 @@ const (
|
||||
pageProviders
|
||||
pageGroups
|
||||
pageUsage
|
||||
pageLibrary
|
||||
)
|
||||
|
||||
var pageNames = []string{"agents", "providers", "groups", "usage"}
|
||||
var pageNames = []string{"agents", "providers", "groups", "usage", "library"}
|
||||
|
||||
// ask is a line to type: a key, a family, a group's name.
|
||||
type ask struct {
|
||||
|
||||
+44
-22
@@ -1,6 +1,7 @@
|
||||
// Package tui is magpie in the terminal: one row per agent, arrow keys to pick
|
||||
// a field, enter to change it; and pages for providers, routing groups and
|
||||
// usage beside it (pages.go), 1–4 to go between them.
|
||||
// usage beside it (pages.go), the library (library.go), 1–5 to go between
|
||||
// them.
|
||||
package tui
|
||||
|
||||
import (
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
|
||||
"github.com/yetone/magpie/internal/agent"
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/library"
|
||||
"github.com/yetone/magpie/internal/profile"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/settings"
|
||||
@@ -94,20 +96,25 @@ type model struct {
|
||||
w, h int
|
||||
syncing bool
|
||||
|
||||
page page
|
||||
back mode // where esc goes from a picker or a line to type
|
||||
ask ask
|
||||
confirm string // what a second d removes
|
||||
provs []provider.Provider
|
||||
prow int
|
||||
bal map[string]string
|
||||
asked bool // balances were asked for
|
||||
groups []provider.Group
|
||||
grow int
|
||||
gid string // the group open
|
||||
gsel int // its member picked
|
||||
period usage.Period
|
||||
sum usage.Summary
|
||||
page page
|
||||
back mode // where esc goes from a picker or a line to type
|
||||
ask ask
|
||||
confirm string // what a second d removes
|
||||
provs []provider.Provider
|
||||
prow int
|
||||
bal map[string]string
|
||||
asked bool // balances were asked for
|
||||
groups []provider.Group
|
||||
grow int
|
||||
gid string // the group open
|
||||
gsel int // its member picked
|
||||
period usage.Period
|
||||
sum usage.Summary
|
||||
lib []libRow
|
||||
lrow int
|
||||
libView *library.View
|
||||
libAgents []library.AgentView
|
||||
libErr string
|
||||
}
|
||||
|
||||
type flashMsg struct {
|
||||
@@ -153,6 +160,8 @@ func (m *model) reload() {
|
||||
}
|
||||
case pageUsage:
|
||||
m.sum = usage.Summarize(m.period)
|
||||
case pageLibrary:
|
||||
m.reloadLibrary()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +213,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case pickMsg:
|
||||
m.openMemberPicker(msg)
|
||||
return m, nil
|
||||
case editedMsg:
|
||||
if msg.err != nil {
|
||||
m.flash, m.flashOK = msg.err.Error(), false
|
||||
return m, nil
|
||||
}
|
||||
return m, saveInstructions(msg.text)
|
||||
case syncedMsg:
|
||||
m.syncing = false
|
||||
if msg.err != nil {
|
||||
@@ -223,7 +238,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, m.goTo(p)
|
||||
}
|
||||
switch m.page {
|
||||
case pageProviders, pageGroups, pageUsage:
|
||||
case pageProviders, pageGroups, pageUsage, pageLibrary:
|
||||
if s := msg.String(); s == "q" || s == "esc" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
@@ -242,6 +257,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m.updateGroups(msg)
|
||||
case pageUsage:
|
||||
return m.updateUsage(msg)
|
||||
case pageLibrary:
|
||||
m.flash = ""
|
||||
return m.updateLibrary(msg)
|
||||
}
|
||||
return m.updateList(msg)
|
||||
case modeAsk:
|
||||
@@ -258,12 +276,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// pageKey is the page a key goes to: 1–4, or ] and [ for the next and
|
||||
// pageKey is the page a key goes to: 1–5, or ] and [ for the next and
|
||||
// the one before.
|
||||
func (m model) pageKey(k string) (page, bool) {
|
||||
n := page(len(pageNames))
|
||||
switch k {
|
||||
case "1", "2", "3", "4":
|
||||
case "1", "2", "3", "4", "5":
|
||||
return page(k[0] - '1'), true
|
||||
case "]":
|
||||
return (m.page + 1) % n, true
|
||||
@@ -386,6 +404,7 @@ func (m model) updatePicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
m.mode = m.back
|
||||
m.reload()
|
||||
return m, nil
|
||||
case "down", "ctrl+n":
|
||||
if len(m.pk.match) > 0 {
|
||||
@@ -588,16 +607,19 @@ func (m model) View() string {
|
||||
switch m.page {
|
||||
case pageProviders:
|
||||
body = m.viewProviders()
|
||||
footer = hints("↑↓", "provider", "↵", "models", "e", "key", "a", "add", "f", "family", "u", "list/groups only", "t", "test", "b", "balances", "d", "remove", "1–4", "pages")
|
||||
footer = hints("↑↓", "provider", "↵", "models", "e", "key", "a", "add", "f", "family", "u", "list/groups only", "t", "test", "b", "balances", "d", "remove", "1–5", "pages")
|
||||
case pageGroups:
|
||||
body = m.viewGroups()
|
||||
footer = hints("↑↓", "group", "↵", "open", "n", "new", "o", "routing", "d", "remove", "1–4", "pages", "q", "quit")
|
||||
footer = hints("↑↓", "group", "↵", "open", "n", "new", "o", "routing", "d", "remove", "1–5", "pages", "q", "quit")
|
||||
case pageLibrary:
|
||||
body = m.viewLibrary()
|
||||
footer = hints("↑↓", "item", "↵", "agents", "e", "edit instructions", "i", "bring in", "d", "remove", "r", "reload", "1–5", "pages", "q", "quit")
|
||||
case pageUsage:
|
||||
body = m.viewUsage()
|
||||
footer = hints("←→", "period", "t w m A", "today · 7 days · 30 days · all", "r", "reload", "1–4", "pages", "q", "quit")
|
||||
footer = hints("←→", "period", "t w m A", "today · 7 days · 30 days · all", "r", "reload", "1–5", "pages", "q", "quit")
|
||||
default:
|
||||
body = m.viewList()
|
||||
footer = hints("↑↓", "agent", "←→", "field", "↵", "change", "s", "save profile", "p", "profiles", "1–4", "pages", "q", "quit")
|
||||
footer = hints("↑↓", "agent", "←→", "field", "↵", "change", "s", "save profile", "p", "profiles", "1–5", "pages", "q", "quit")
|
||||
}
|
||||
case modePick, modeProfiles:
|
||||
body = m.viewPicker()
|
||||
|
||||
Reference in new issue
Block a user