From 2aa214c0984293343154b7bb8518c4be42dc444f Mon Sep 17 00:00:00 2001 From: dawn <90008@klbr.net> Date: Sun, 27 Sep 2026 19:07:50 +0300 Subject: [PATCH] all: share the repeated ssh, viewer and theme plumbing Signed-off-by: dawn <90008@klbr.net> --- internal/app/app.go | 39 ++++----- internal/app/picker_screen_test.go | 2 +- internal/app/ui.go | 125 ++++++++++++----------------- internal/protocol/protocol.go | 3 +- internal/remote/remote.go | 57 ++++++------- internal/session/session.go | 83 ++++++++----------- internal/session/session_test.go | 2 +- internal/termcolor/termcolor.go | 12 +-- main.go | 5 +- 9 files changed, 132 insertions(+), 196 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index ea1837a..0ed71e9 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,6 +1,7 @@ package app import ( + "cmp" "context" "encoding/json" "errors" @@ -28,15 +29,14 @@ func Find(ctx context.Context, query string) []session.Session { func findSessions(ctx context.Context, query string, peers []remote.Peer) []session.Session { raw := strings.TrimSpace(query) q := strings.ToLower(raw) - self := strings.ToLower(session.ShortHostname()) t := target.Parse(raw) - id, host := strings.ToLower(t.ID), strings.ToLower(t.Host) + id := strings.ToLower(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 t.ID != "" || t.User != "" { - if host == self || host == "localhost" { + if isLocal(t.Host) { return matchID(session.ListLocal()) } peer := lo.ContainsBy(peers, func(p remote.Peer) bool { return strings.EqualFold(p.Name, t.Host) }) || len(remote.KnownDests(t.Host)) > 1 @@ -256,28 +256,22 @@ func Spawn(addr string, cmdArgs []string) error { if len(cmdArgs) == 0 { return errors.New("usage: tobi [host.[id]] -- [args...]") } - - self := strings.ToLower(session.ShortHostname()) - t := target.Parse(strings.TrimSpace(addr)) - host := lo.Ternary(t.Host != "", t.Host, self) - id := lo.Ternary(t.ID != "", t.ID, session.GenerateID()) + t := target.Parse(addr) + t.Host = cmp.Or(t.Host, strings.ToLower(session.ShortHostname())) + id := cmp.Or(t.ID, session.GenerateID()) if !session.ValidateID(id) { return fmt.Errorf("invalid session id: %q (must be 1-64 alphanumeric, - or _)", id) } - - if t.User == "" && (strings.EqualFold(host, self) || strings.EqualFold(host, "localhost")) { + if t.User == "" && isLocal(t.Host) { if err := session.EnsureOwner(id, cmdArgs); err != nil { return err } - host = session.ShortHostname() - } else { - t.Host = host - host = t.Dest() - if err := remote.SpawnRemote(host, id, cmdArgs); err != nil { - return err - } + return connectOrHop(session.Session{ID: id, Host: session.ShortHostname()}) + } + if err := remote.SpawnRemote(t.Dest(), id, cmdArgs); err != nil { + return err } - return connectOrHop(session.Session{ID: id, Host: host}) + return connectOrHop(session.Session{ID: id, Host: t.Dest()}) } func Complete(rawPrefix string) error { @@ -296,15 +290,12 @@ func Complete(rawPrefix string) error { } func hostCandidates(prefix string, peers []remote.Peer) []session.Session { - t := target.Parse(strings.TrimSpace(prefix)) - q := strings.ToLower(t.Host) - name := lo.Ternary(t.User != "", - func(n string) string { return t.User + "@" + n }, - func(n string) string { return n }) + t := target.Parse(prefix) + q, user := strings.ToLower(t.Host), lo.Ternary(t.User != "", t.User+"@", "") return lo.FilterMap( append([]remote.Peer{{Name: session.ShortHostname()}}, peers...), func(p remote.Peer, _ int) (session.Session, bool) { - return session.Session{Host: name(p.Name)}, strings.HasPrefix(strings.ToLower(p.Name), q) + return session.Session{Host: user + p.Name}, strings.HasPrefix(strings.ToLower(p.Name), q) }, ) } diff --git a/internal/app/picker_screen_test.go b/internal/app/picker_screen_test.go index d084561..9bfd36f 100644 --- a/internal/app/picker_screen_test.go +++ b/internal/app/picker_screen_test.go @@ -29,7 +29,7 @@ func TestLongTablesKeepTheFilterAndCursorOnScreen(t *testing.T) { for i := range hosts { m.rows = append(m.rows, hostRow{name: fmt.Sprintf("host%03d", i), probing: true, started: time.Now()}) } - m.probes, m.pending = make(chan probeResult, hosts), hosts + m.probes = make(chan probeResult, hosts) m.move(0) master, tty, err := pty.Open() diff --git a/internal/app/ui.go b/internal/app/ui.go index 5080c53..819e3fd 100644 --- a/internal/app/ui.go +++ b/internal/app/ui.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "image/color" "math/rand/v2" "os" "slices" @@ -37,28 +38,33 @@ type theme struct { } func newTheme(dark bool) theme { - c := lipgloss.LightDark(dark) - fg := func(pair [2]string) lipgloss.Style { - return lipgloss.NewStyle().Foreground(c(lipgloss.Color(pair[0]), lipgloss.Color(pair[1]))) - } + ld := lipgloss.LightDark(dark) + c := func(l, d string) color.Color { return ld(lipgloss.Color(l), lipgloss.Color(d)) } + fg := func(l, d string) lipgloss.Style { return lipgloss.NewStyle().Foreground(c(l, d)) } t := theme{ - text: fg([2]string{"#4c4f69", "#cdd6f4"}), - dim: fg([2]string{"#9ca0b0", "#6c7086"}), - lavender: fg([2]string{"#7287fd", "#b4befe"}), - mauve: fg([2]string{"#8839ef", "#cba6f7"}), - mint: fg([2]string{"#40a02b", "#a6e3a1"}), - yellow: fg([2]string{"#df8e1d", "#f9e2af"}), - salmon: fg([2]string{"#d20f39", "#f38ba8"}), - pill: lipgloss.NewStyle(). - Background(c(lipgloss.Color("#7287fd"), lipgloss.Color("#b4befe"))). - Foreground(c(lipgloss.Color("#eff1f5"), lipgloss.Color("#1e1e2e"))).Padding(0, 1), - pick: lipgloss.NewStyle().Background(c(lipgloss.Color("#ccd0da"), lipgloss.Color("#313244"))), - plain: lipgloss.NewStyle(), + text: fg("#4c4f69", "#cdd6f4"), + dim: fg("#9ca0b0", "#6c7086"), + lavender: fg("#7287fd", "#b4befe"), + mauve: fg("#8839ef", "#cba6f7"), + mint: fg("#40a02b", "#a6e3a1"), + yellow: fg("#df8e1d", "#f9e2af"), + salmon: fg("#d20f39", "#f38ba8"), + pill: lipgloss.NewStyle().Background(c("#7287fd", "#b4befe")).Foreground(c("#eff1f5", "#1e1e2e")).Padding(0, 1), + pick: lipgloss.NewStyle().Background(c("#ccd0da", "#313244")), + plain: lipgloss.NewStyle(), } t.faint, t.border = t.dim.Faint(true), t.dim return t } +// setTheme restyles the picker and its filter for a light or dark terminal +func (m *pickerModel) setTheme(dark bool) { + m.th = newTheme(dark) + st := textinput.DefaultStyles(dark) + st.Focused.Placeholder, st.Blurred.Placeholder = m.th.faint, m.th.faint + m.filter.SetStyles(st) +} + type hostRow struct { name string local bool @@ -92,7 +98,6 @@ type pickerModel struct { lookDir int // -1 left, +1 right; persists after the blink holdTicks int // how long the gaze-hold lasts after a blink probes chan probeResult - pending int picked *pickEntry killing map[string]bool // keyed by canonical name, in flight notice string // the last kill failure, until the next key @@ -122,21 +127,14 @@ func (m pickerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.filter.SetCursor(m.filter.Position()) // recompute the scroll offset for the new width case tea.BackgroundColorMsg: - m.th = newTheme(msg.IsDark()) - st := textinput.DefaultStyles(msg.IsDark()) - st.Focused.Placeholder, st.Blurred.Placeholder = m.th.faint, m.th.faint - m.filter.SetStyles(st) + m.setTheme(msg.IsDark()) case probeResult: - r := &m.rows[msg.row] - r.probing = false - r.unreachable = errors.Is(msg.err, remote.ErrUnreachable) - r.duration = msg.duration byNewest(msg.sessions) - r.sessions = msg.sessions - m.pending-- + r := &m.rows[msg.row] + r.probing, r.unreachable, r.duration, r.sessions = false, errors.Is(msg.err, remote.ErrUnreachable), msg.duration, msg.sessions m.move(0) - if m.pending > 0 { + if lo.SomeBy(m.rows, func(row hostRow) bool { return row.probing }) { return m, waitForProbe(m.probes) } return m, nil @@ -314,9 +312,6 @@ func (m pickerModel) view() string { b.WriteString(m.th.salmon.MaxWidth(w).Render(m.notice)) return b.String() } - hint := func(k, act string) string { - return m.th.dim.Render("[") + m.th.text.Render(k) + m.th.dim.Render("] "+act) - } hs := m.hints() span := func(hs []pickerHint) int { return lo.SumBy(hs, pickerHint.width) + 3*(len(hs)-1) } for len(hs) > 1 && span(hs) > w { @@ -327,7 +322,7 @@ func (m pickerModel) view() string { if i > 0 { b.WriteString(strings.Repeat(" ", free/gaps+lo.Ternary(i <= free%gaps, 1, 0))) } - b.WriteString(hint(h.k, h.act)) + b.WriteString(m.th.dim.Render("[") + m.th.text.Render(h.k) + m.th.dim.Render("] "+h.act)) } return b.String() } @@ -455,58 +450,35 @@ func (m pickerModel) drawHostTable(b *strings.Builder, w int, rows []hostRow) { func runPicker(local []session.Session, initial string) error { peersCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - peers := remote.DiscoverPeers(peersCtx) + peers := remote.DiscoverPeers(peersCtx) // never includes this host cancel() - self := strings.ToLower(session.ShortHostname()) - probes := make(chan probeResult, len(peers)) filter := textinput.New() filter.Prompt = "› " filter.Placeholder = "search sessions…" filter.Focus() filter.SetWidth(startWidth - 8) - th := newTheme(true) - st := textinput.DefaultStyles(true) - st.Focused.Placeholder, st.Blurred.Placeholder = th.faint, th.faint - filter.SetStyles(st) - - if initial = strings.TrimSpace(initial); initial != "" { - filter.SetValue(initial) - } + filter.SetValue(strings.TrimSpace(initial)) m := pickerModel{ filter: filter, width: startWidth, - th: th, - probes: probes, + probes: make(chan probeResult, len(peers)), spinner: spinner.New(spinner.WithSpinner(spinner.Dot)), - rows: []hostRow{{name: self, local: true, sessions: local}}, + rows: []hostRow{{name: strings.ToLower(session.ShortHostname()), local: true, sessions: local}}, } + m.setTheme(true) // dark until the terminal answers the background query m.lookDir, m.nextBlink = []int{-1, 1}[rand.IntN(2)], 30+rand.IntN(31) - online := lo.Filter(peers, func(p remote.Peer, _ int) bool { return p.Online && !strings.EqualFold(p.Name, self) }) - var offline []remote.Peer - if os.Getenv("TOBI_OFFLINE") == "1" { - offline = lo.Reject(peers, func(p remote.Peer, _ int) bool { return p.Online || strings.EqualFold(p.Name, self) }) - } + online, offline := lo.FilterReject(peers, func(p remote.Peer, _ int) bool { return p.Online }) for _, p := range online { for _, dest := range remote.ProbeDests(p.Name) { - m.rows = append(m.rows, hostRow{name: dest, probing: true}) + go probe(m.probes, len(m.rows), dest) + m.rows = append(m.rows, hostRow{name: dest, probing: true, started: time.Now()}) } } - for _, p := range offline { - m.rows = append(m.rows, hostRow{name: p.Name, peerOffline: true}) - } - for i := range m.rows { - if !m.rows[i].probing { - continue + if os.Getenv("TOBI_OFFLINE") == "1" { + for _, p := range offline { + m.rows = append(m.rows, hostRow{name: p.Name, peerOffline: true}) } - m.pending++ - m.rows[i].started = time.Now() - go func(row int, name string) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - list, err := remote.QueryRemote(ctx, name) - probes <- probeResult{row: row, sessions: list, err: err, duration: probeClock(time.Since(m.rows[row].started))} - }(i, m.rows[i].name) } m.move(0) @@ -515,15 +487,22 @@ func runPicker(local []session.Session, initial string) error { return err } fin := model.(pickerModel) - picked := fin.picked - switch { - case picked == nil: + switch p := fin.picked; { + case p == nil: return nil - case picked.sess != nil: - return connectOrHop(*picked.sess) + case p.sess != nil: + return connectOrHop(*p.sess) } - fmt.Fprintln(os.Stderr, fin.th.dim.Render("starting a shell on "+picked.host+"…")) - return Spawn(picked.host, loginShell) + fmt.Fprintln(os.Stderr, fin.th.dim.Render("starting a shell on "+fin.picked.host+"…")) + return Spawn(fin.picked.host, loginShell) +} + +func probe(probes chan<- probeResult, row int, dest string) { + start := time.Now() + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + list, err := remote.QueryRemote(ctx, dest) + probes <- probeResult{row: row, sessions: list, err: err, duration: probeClock(time.Since(start))} } // the target's own login shell, named so the picker need not show the wrapper diff --git a/internal/protocol/protocol.go b/internal/protocol/protocol.go index b297007..be57503 100644 --- a/internal/protocol/protocol.go +++ b/internal/protocol/protocol.go @@ -37,8 +37,7 @@ func ReadFrame(r io.Reader) (FrameType, []byte, error) { } func WriteFrame(w io.Writer, t FrameType, payload []byte) error { - head := append([]byte{byte(t)}, binary.BigEndian.AppendUint32(nil, uint32(len(payload)))...) - _, err := w.Write(append(head, payload...)) + _, err := w.Write(append(binary.BigEndian.AppendUint32([]byte{byte(t)}, uint32(len(payload))), payload...)) return err } diff --git a/internal/remote/remote.go b/internal/remote/remote.go index d46e312..a7f8a15 100644 --- a/internal/remote/remote.go +++ b/internal/remote/remote.go @@ -89,9 +89,7 @@ func RememberHost(dest string) { return } m[t.Host] = append([]string{dest}, slices.DeleteFunc(m[t.Host], func(d string) bool { return d == dest })...) - if len(m[t.Host]) > 8 { - m[t.Host] = m[t.Host][:8] - } + m[t.Host] = m[t.Host][:min(len(m[t.Host]), 8)] saveHosts(m) } @@ -224,7 +222,7 @@ func sshRun(ctx context.Context, dest string, in io.Reader, opts sshOpts) (strin c.Stdout, c.Stderr = &out, &errBuf if err := c.Run(); err != nil { var ee *exec.ExitError - if (errors.As(err, &ee) && ee.ExitCode() == 255) || errors.Is(err, context.DeadlineExceeded) || ctx.Err() != nil { + if (errors.As(err, &ee) && ee.ExitCode() == 255) || ctx.Err() != nil { err = fmt.Errorf("%w: %w", ErrUnreachable, err) } if msg := strings.TrimSpace(errBuf.String()); msg != "" { @@ -235,6 +233,16 @@ func sshRun(ctx context.Context, dest string, in io.Reader, opts sshOpts) (strin return out.String(), nil } +// sshConfirmed runs opts.cmd and wants marker in its output: a remote that +// exits clean without printing it did not do the work +func sshConfirmed(ctx context.Context, dest string, in io.Reader, opts sshOpts, marker, missing string) error { + out, err := sshRun(ctx, dest, in, opts) + if err == nil && !strings.Contains(out, marker) { + return errors.New(missing) + } + return err +} + func getBinaryPayload(goos, arch string) ([]byte, error) { if data, err := embeddedPayloads.ReadFile(fmt.Sprintf("payloads/tobi-%s-%s.zst", goos, arch)); err == nil && len(data) > 0 { if zr, err := zstd.NewReader(bytes.NewReader(data)); err == nil { @@ -243,11 +251,7 @@ func getBinaryPayload(goos, arch string) ([]byte, error) { } } if goos == runtime.GOOS && arch == runtime.GOARCH { - self, err := os.Executable() - if err != nil { - self = os.Args[0] - } - return os.ReadFile(self) + return os.ReadFile(session.Executable()) } return nil, fmt.Errorf("no binary available for %s/%s (build the carrier slices: nix build or scripts/build-universal.sh)", goos, arch) } @@ -291,13 +295,10 @@ func EnsureRemote(ctx context.Context, host string) error { if err != nil { return fmt.Errorf("prepare tobi for %s (%s/%s): %w", dest, osName, arch, err) } - out, err = sshRun(ctx, dest, bytes.NewReader(bin), sshOpts{cmd: uploadCmd + " && echo " + uploadMarker, batch: true, timeout: "5"}) - if err != nil { + upload := sshOpts{cmd: uploadCmd + " && echo " + uploadMarker, batch: true, timeout: "5"} + if err := sshConfirmed(ctx, dest, bytes.NewReader(bin), upload, uploadMarker, "upload did not complete"); err != nil { return fmt.Errorf("upload tobi to %s: %w", dest, err) } - if !strings.Contains(out, uploadMarker) { - return fmt.Errorf("upload tobi to %s: upload did not complete", dest) - } remoteInstalled.Store(dest, true) return nil } @@ -456,37 +457,25 @@ func runSpawnRPC(r io.Reader) error { // an older remote tobi takes "kill" for an attach and exits clean, so only the marker proves it func KillRemote(ctx context.Context, host, id string) error { dest := hostDest(host) - out, err := sshRun(ctx, dest, nil, sshOpts{cmd: rpcCommand("kill", id), batch: true, timeout: "5", master: true}) - if err == nil && !strings.Contains(out, killMarker) { - err = errors.New("remote tobi is too old to kill sessions") - } - if err != nil { + kill := sshOpts{cmd: rpcCommand("kill", id), batch: true, timeout: "5", master: true} + if err := sshConfirmed(ctx, dest, nil, kill, killMarker, "remote tobi is too old to kill sessions"); err != nil { return fmt.Errorf("kill %s.%s: %w", dest, id, err) } return nil } func SpawnRemote(host, id string, cmdArgs []string) error { - if !session.ValidateID(id) { - return fmt.Errorf("invalid session id: %q", id) - } - if len(cmdArgs) == 0 { - return errors.New("empty command") + req := spawnRequest{ID: id, Argv: cmdArgs} + if err := validateSpawnRequest(req); err != nil { + return err } dest := hostDest(host) if err := EnsureRemote(context.Background(), dest); err != nil { return err } - req, err := json.Marshal(spawnRequest{ID: id, Argv: cmdArgs}) - if err != nil { - return err - } - out, err := sshRun(context.Background(), dest, bytes.NewReader(append(req, '\n')), sshOpts{ - cmd: rpcCommand("spawn", "") + " && echo " + spawnMarker, timeout: "5", master: true}) - if err == nil && !strings.Contains(out, spawnMarker) { - err = fmt.Errorf("did not confirm: %s", strings.TrimSpace(out)) - } - if err != nil { + body, _ := json.Marshal(req) // strings only, it cannot fail + spawn := sshOpts{cmd: rpcCommand("spawn", "") + " && echo " + spawnMarker, timeout: "5", master: true} + if err := sshConfirmed(context.Background(), dest, bytes.NewReader(append(body, '\n')), spawn, spawnMarker, "did not confirm"); err != nil { return fmt.Errorf("spawn on %s: %w", dest, err) } return nil diff --git a/internal/session/session.go b/internal/session/session.go index d881f18..e58282c 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -109,18 +109,13 @@ func EnsureOwner(id string, argv []string) error { return errors.New("empty command") } sockPath := SocketPath(id) - if conn, err := net.DialTimeout("unix", sockPath, 30*time.Millisecond); err == nil { - _ = conn.Close() + if answers(sockPath, 30*time.Millisecond) { return nil } if _, err := exec.LookPath(argv[0]); err != nil { return err } - self, _ := os.Executable() - if self == "" { - self = os.Args[0] - } - cmd := exec.Command(self, argv...) + cmd := exec.Command(Executable(), argv...) cmd.Env = append(os.Environ(), "TOBI_OWNER=1", "TOBI_ID="+id) cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} // the owner is detached, so its stderr is the only way it can explain itself @@ -139,8 +134,7 @@ func EnsureOwner(id string, argv []string) error { return fmt.Errorf("owner for session %q %s", id, reason) } for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); time.Sleep(10 * time.Millisecond) { - if conn, err := net.DialTimeout("unix", sockPath, 20*time.Millisecond); err == nil { - _ = conn.Close() + if answers(sockPath, 20*time.Millisecond) { return nil } select { @@ -152,6 +146,14 @@ func EnsureOwner(id string, argv []string) error { return fail("did not start") } +// Executable is this binary, to re-run as an owner or ship to a remote +func Executable() string { + if self, err := os.Executable(); err == nil { + return self + } + return os.Args[0] +} + // read while the child may still be writing, so the copier must not be raced type syncBuffer struct { mu sync.Mutex @@ -274,7 +276,7 @@ func Kill(id string) error { switch { case err != nil: return err - case !answers(sock): + case !answers(sock, 250*time.Millisecond): return nil case pid <= 0: return fmt.Errorf("session %q outlived the kill and its owner pid is unknown", id) @@ -283,15 +285,15 @@ func Kill(id string) error { return fmt.Errorf("signal the owner of %q: %w", id, err) } for deadline := time.Now().Add(time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) { - if !answers(sock) { + if !answers(sock, 250*time.Millisecond) { return nil } } return fmt.Errorf("session %q outlived SIGTERM", id) } -func answers(sock string) bool { - c, err := net.DialTimeout("unix", sock, 250*time.Millisecond) +func answers(sock string, timeout time.Duration) bool { + c, err := net.DialTimeout("unix", sock, timeout) if err == nil { _ = c.Close() } @@ -327,6 +329,15 @@ type viewerClient struct { colors map[int]termcolor.Color } +// send queues msg, dropping a viewer too slow to keep up +func (vc *viewerClient) send(msg outMsg) { + select { + case vc.ch <- msg: + default: + vc.Close() + } +} + func (vc *viewerClient) Close() { vc.once.Do(func() { close(vc.done) @@ -385,27 +396,16 @@ func (o *Owner) applySizeLocked() { func (o *Owner) outputLocked(p []byte) { for vc := range o.viewers { - select { - case vc.ch <- outMsg{t: protocol.TypeOutput, p: p}: - default: - vc.Close() - } + vc.send(outMsg{t: protocol.TypeOutput, p: p}) } } func (o *Owner) hop(p []byte) { o.mu.Lock() - viewers := make([]*viewerClient, 0, len(o.viewers)) - for vc := range o.viewers { - viewers = append(viewers, vc) - } + viewers := lo.Keys(o.viewers) o.mu.Unlock() for _, vc := range viewers { - select { - case vc.ch <- outMsg{t: protocol.TypeHop, p: p}: - default: - vc.Close() - } + vc.send(outMsg{t: protocol.TypeHop, p: p}) } } @@ -599,18 +599,8 @@ func (o *Owner) handleConn(conn net.Conn) { } cols, rows, _ := protocol.DecodeSize(payload) - if cols < 1 { - cols = 80 - } - if rows < 1 { - rows = 24 - } - var colors map[int]termcolor.Color - if len(payload) > 4 { - colors, _ = termcolor.DecodeColors(payload[4:]) - } - - vc, snap := o.addViewer(conn, cols, rows, colors) + colors, _ := termcolor.DecodeColors(payload[min(4, len(payload)):]) + vc, snap := o.addViewer(conn, cmp.Or(cols, 80), cmp.Or(rows, 24), colors) if vc == nil { _ = conn.Close() return @@ -723,21 +713,14 @@ func RunOwner(id string, command []string) error { }) vt.OnColor(func(events []xterm.ColorEvent) { for _, ev := range events { - if ev.Type != xterm.ColorRequestReport { - continue - } - i := ev.Index - if i < 0 || i > int(xterm.SpecialColorCursor) { + if ev.Type != xterm.ColorRequestReport || ev.Index < 0 || ev.Index > int(xterm.SpecialColorCursor) { continue } owner.colorMu.Lock() - sets := make([]map[int]termcolor.Color, 0, len(owner.colorSets)) - for _, set := range owner.colorSets { - sets = append(sets, set) - } + sets := lo.Values(owner.colorSets) owner.colorMu.Unlock() - if c, ok := termcolor.Agreed(i, sets); ok { - fmt.Fprint(owner.ptyFile, termcolor.Reply(i, c)) + if c, ok := termcolor.Agreed(ev.Index, sets); ok { + fmt.Fprint(owner.ptyFile, termcolor.Reply(ev.Index, c)) } } }) diff --git a/internal/session/session_test.go b/internal/session/session_test.go index da6d89d..4b463f7 100644 --- a/internal/session/session_test.go +++ b/internal/session/session_test.go @@ -122,7 +122,7 @@ func TestKillSignalsAnOwnerThatPredatesIt(t *testing.T) { t.Fatal(err) } t.Cleanup(func() { _ = owner.Process.Kill() }) - for i := 0; i < 100 && !answers(SocketPath(id)); i++ { + for i := 0; i < 100 && !answers(SocketPath(id), 250*time.Millisecond); i++ { time.Sleep(20 * time.Millisecond) } if err := Kill(id); err != nil { diff --git a/internal/termcolor/termcolor.go b/internal/termcolor/termcolor.go index b6abc2b..8b58140 100644 --- a/internal/termcolor/termcolor.go +++ b/internal/termcolor/termcolor.go @@ -94,10 +94,7 @@ func parseReply(body string) (int, Color, bool) { if len(parts) != 2 { return 0, Color{}, false } - n, err := strconv.Atoi(parts[0]) - if err != nil { - return 0, Color{}, false - } + n, _ := strconv.Atoi(parts[0]) // one of the case labels c, ok := parseColor(parts[1]) return Foreground + n - 10, c, ok } @@ -159,10 +156,9 @@ func EncodeColors(colors map[int]Color) []byte { b := []byte{colorsVersion, byte(len(keys))} for _, k := range keys { c := colors[k] - b = binary.BigEndian.AppendUint16(b, uint16(k)) - b = binary.BigEndian.AppendUint16(b, c.R) - b = binary.BigEndian.AppendUint16(b, c.G) - b = binary.BigEndian.AppendUint16(b, c.B) + for _, v := range []uint16{uint16(k), c.R, c.G, c.B} { + b = binary.BigEndian.AppendUint16(b, v) + } } return b } diff --git a/main.go b/main.go index 0dd6ab1..ccbe1ec 100644 --- a/main.go +++ b/main.go @@ -35,7 +35,7 @@ requires tssh (or ssh) and tailscale in PATH. ` func dispatch(args []string) error { - switch { + switch op := os.Getenv("TOBI_RPC"); { case os.Getenv("TOBI_COMPLETE") == "1": return app.Complete(strings.Join(args, " ")) case os.Getenv("TOBI_OWNER") == "1": @@ -44,8 +44,7 @@ func dispatch(args []string) error { os.Unsetenv(k) } return wrapErr("owner", session.RunOwner(id, args)) - case os.Getenv("TOBI_RPC") != "": - op := os.Getenv("TOBI_RPC") + case op != "": return wrapErr("rpc "+op, remote.RunRPC(op, os.Getenv("TOBI_ID"))) } if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") { -- knotmirror 0.0.1