shared target parser and session ranker; seed the picker filter from a plain query
15 unmodified lines16171819202122222324252627282930232425263536272829303132333443353637384739404941424352444546474819 unmodified lines6869707180818272737475767778798081858283878884858687888990959192979899100101102939410495969798991001011021031041051061071081091101111127 unmodified lines12012112212312412312512612712812951 unmodified lines18118218318418318518618718818918819019019119219319419519619719819119219319419520019619719819920020120220320920420520620720814 unmodified lines15 unmodified lines "github.com/samber/lo" "golang.org/x/term" "tobi/internal/remote" "tobi/internal/session" "tobi/internal/target")
// bareHost strips a user@ prefix; comparisons use the bare name, ssh// destinations keep the full user@hostfunc bareHost(host string) string { if _, bare, ok := strings.Cut(host, "@"); ok { return bare } return host}
func Find(ctx context.Context, query string) []session.Session { q := strings.ToLower(strings.TrimSpace(query)) self, peers := strings.ToLower(session.ShortHostname()), remote.DiscoverPeersFunc(ctx)
if lastDot := strings.LastIndex(q, "."); lastDot != -1 { host, id := q[:lastDot], q[lastDot+1:] if t := target.Parse(q); t.ID != "" || strings.Contains(q, "@") { host, id := t.Host, t.ID matchID := func(list []session.Session) []session.Session { return lo.Filter(list, func(s session.Session, _ int) bool { return strings.HasPrefix(strings.ToLower(s.ID), id) }) }
if bare := bareHost(host); bare == self || bare == "localhost" { if host == self || host == "localhost" { return matchID(session.ListLocal()) }
if lo.ContainsBy(peers, func(p remote.Peer) bool { return strings.EqualFold(p.Name, bareHost(host)) }) { if lo.ContainsBy(peers, func(p remote.Peer) bool { return strings.EqualFold(p.Name, host) }) { if session.ValidateID(id) { return []session.Session{{Host: host, ID: id}} return []session.Session{{Host: t.Dest(), ID: id}} }
return matchID(remote.QueryHost(ctx, host)) return matchID(remote.QueryHost(ctx, t.Dest())) } }
all := session.ListLocal()19 unmodified lines for range targetPeers { all = append(all, <-lists...) }
slices.SortFunc(all, func(a, b session.Session) int { return b.StartedAt.Compare(a.StartedAt) }) byNewest(all) return rankSessions(all, q)}
// rankSessions is the one matcher/ranker behind CLI resolution and the// picker filter: empty queries keep order, direct prefix hits win, fuzzy// matching falls back, canonical dedupe keeps the best rankfunc rankSessions(list []session.Session, query string) []session.Session { q := strings.ToLower(strings.TrimSpace(query)) if q == "" { return all return list }
direct := lo.Filter(all, func(s session.Session, _ int) bool { direct := lo.Filter(list, func(s session.Session, _ int) bool { return strings.HasPrefix(strings.ToLower(s.Canonical()), q) || strings.HasPrefix(strings.ToLower(s.ID), q) || strings.HasPrefix(strings.ToLower(s.Command), q) || strings.Contains(strings.ToLower(s.Cwd), q) }) if len(direct) > 0 { return direct return lo.UniqBy(direct, (session.Session).Canonical) }
targets := lo.Map(all, func(s session.Session, _ int) string { return s.MatchString() }) matches := fuzzy.Find(q, targets)
matches := fuzzy.Find(q, lo.Map(list, func(s session.Session, _ int) string { return s.MatchString() })) return lo.UniqBy(lo.Map(matches, func(m fuzzy.Match, _ int) session.Session { return all[m.Index] return list[m.Index] }), (session.Session).Canonical)}
// byNewest orders newest first with a stable id tiebreakfunc byNewest(s []session.Session) { slices.SortFunc(s, func(a, b session.Session) int { if c := b.StartedAt.Compare(a.StartedAt); c != 0 { return c } return strings.Compare(a.ID, b.ID) })}
func connectOrHop(target session.Session) error { if cs := os.Getenv("TOBI_CONTROL"); cs != "" { return session.SendHop(cs, target.Canonical()) }7 unmodified lines return connectOrHop(sessions[0]) } return errors.New("no sessions found") } // a plain query seeds the picker filter (readme behavior) return runPicker(session.ListLocal()) return runPicker(session.ListLocal(), query)}func RunHopLoop(target session.Session) error { if rawState, err := term.MakeRaw(int(os.Stdin.Fd())); err == nil { defer func() { _ = term.Restore(int(os.Stdin.Fd()), rawState) }()51 unmodified lines return err }}
func Spawn(target string, cmdArgs []string) error {func Spawn(addr string, cmdArgs []string) error { if len(cmdArgs) == 0 { return errors.New("usage: tobi [host.[id]] -- <command> [args...]") }
target = strings.TrimSpace(target) selfHost := strings.ToLower(session.ShortHostname()) host, id := selfHost, ""
if h, i, ok := strings.Cut(target, "."); ok { if h != "" { host = strings.ToLower(h) } id = strings.ToLower(i) } else if target != "" { host = strings.ToLower(target) t := target.Parse(strings.ToLower(strings.TrimSpace(addr))) host, id := selfHost, t.ID if t.Host != "" { host = t.Host }
if id == "" { id = session.GenerateID() }
if !session.ValidateID(id) { return fmt.Errorf("invalid session id: %q (must be 1-64 alphanumeric, - or _)", id) }
if bare := bareHost(host); bare == selfHost || bare == "localhost" { if host == selfHost || host == "localhost" { if err := session.EnsureOwner(id, cmdArgs); err != nil { return err } host = session.ShortHostname()14 unmodified lines4 unmodified lines567899101112131415161817181920152 unmodified linesExpand all173174175176179180181182183184177178179180181190191192193194195196197198199200201202203204205206207182183184185186192 unmodified linesExpand all3793803813824083833843853863874 unmodified lines39239339439539639739839940040140272 unmodified lines4 unmodified lines "errors" "fmt" "math/rand/v2" "os" "sort" "strings" "time" "unicode/utf8"
"github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/sahilm/fuzzy" "github.com/samber/lo" "golang.org/x/term"
"tobi/internal/remote"152 unmodified linesExpand all } return m, nil}
func byNewest(s []session.Session) { sort.SliceStable(s, func(i, j int) bool { return s[i].StartedAt.After(s[j].StartedAt) || s[i].StartedAt.Equal(s[j].StartedAt) && s[i].ID < s[j].ID })}
func (m pickerModel) visible() []session.Session { var out []session.Session for _, r := range m.rows { out = append(out, r.sessions...) } q := strings.TrimSpace(m.filter.Value()) if q == "" { return out } // best match first; ties keep the underlying newest-first order targets := lo.Map(out, func(s session.Session, _ int) string { return s.MatchString() }) matches := fuzzy.Find(q, targets) sort.SliceStable(matches, func(i, j int) bool { return matches[i].Score > matches[j].Score }) ranked := make([]session.Session, 0, len(matches)) seen := map[string]bool{} for _, mt := range matches { s := out[mt.Index] if !seen[s.Canonical()] { seen[s.Canonical()] = true ranked = append(ranked, s) } } return ranked return rankSessions(out, m.filter.Value())}
func (m pickerModel) total() int { return lo.SumBy(m.rows, func(r hostRow) int { return len(r.sessions) })192 unmodified linesExpand all
return cursor}
func runPicker(local []session.Session) error {func runPicker(local []session.Session, initial string) error { peersCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) peers := remote.DiscoverPeersFunc(peersCtx) cancel()
4 unmodified lines filter.Placeholder = "search sessions…" filter.PlaceholderStyle = faintStyle filter.Focus()
if initial = strings.TrimSpace(initial); initial != "" { filter.SetValue(initial) } m := pickerModel{ filter: filter, width: 80, probes: probes,72 unmodified lines12345678910111213141516171819202122232425262728293031323334353637383940// Package target parses tobi's session addressing grammar://// host, host.id, user@host, user@host.id, .id//// session ids and host names never contain dots themselves, so the id is// whatever follows the final dot; a user@ prefix scopes the ssh destination.package target
import "strings"
// Target is a parsed session address.type Target struct { User string // ssh user, empty when unaddressed Host string // bare host name, possibly empty (".id" form) ID string // session id, empty when the target names no session}
// Parse splits an address into its user, bare host, and session id.func Parse(s string) Target { var t Target if u, rest, ok := strings.Cut(strings.TrimSpace(s), "@"); ok { t.User = u s = rest } if i := strings.LastIndex(s, "."); i != -1 { t.Host, t.ID = s[:i], s[i+1:] } else { t.Host = s } return t}
// Dest is the ssh destination: user@host when a user is addressed, bare// host otherwise.func (t Target) Dest() string { if t.User != "" { return t.User + "@" + t.Host } return t.Host}