-
Notifications
You must be signed in to change notification settings - Fork 1
feat(cli): share surface — mir share ls/revoke, guest state, overview (G1d) #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ import ( | |
|
|
||
| "github.com/srcful/terminal-relay/go/internal/client" | ||
| "github.com/srcful/terminal-relay/go/internal/defaults" | ||
| "github.com/srcful/terminal-relay/go/internal/identity" | ||
| "github.com/srcful/terminal-relay/go/internal/noise" | ||
| "github.com/srcful/terminal-relay/go/internal/peer" | ||
| ) | ||
|
|
@@ -76,6 +77,8 @@ func (a *app) cmdOverview() error { | |
| dir: dir, | ||
| idn: idn, | ||
| pump: pump, | ||
| fd: fd, | ||
| raw: oldState, | ||
| model: &overviewModel{ | ||
| Binary: a.binary, | ||
| Status: "loading your machines…", | ||
|
|
@@ -130,6 +133,8 @@ type overviewState struct { | |
| dir string | ||
| idn *client.Identity | ||
| pump *stdinPump | ||
| fd int // stdin fd, for suspending raw mode around the share ceremony | ||
| raw *term.State // the pre-overview terminal state to restore | ||
| model *overviewModel | ||
| machines []client.Machine // row i -> machines[i] | ||
| fresh map[string]bool // machine ids first seen while this overview is up | ||
|
|
@@ -198,13 +203,23 @@ func (ov *overviewState) refresh(ctx context.Context, first bool) bool { | |
| } | ||
| rows := make([]overviewRow, 0, len(merged)) | ||
| for _, m := range merged { | ||
| rows = append(rows, overviewRow{ | ||
| row := overviewRow{ | ||
| Name: m.Name, | ||
| MachineID: m.MachineID, | ||
| Online: online[m.MachineID], | ||
| New: ov.fresh[m.MachineID], | ||
| WindowsLine: ov.windows[m.Name], | ||
| }) | ||
| } | ||
| if m.Owner != "" { | ||
| // A share someone gave this identity: mark it and let the grant | ||
| // speak for its state — the registry never knows it. | ||
| row.Shared = true | ||
| row.WindowsLine = "shared with you" | ||
| if g := client.GuestGrantFor(ov.dir, m.MachineID); g != nil { | ||
| row.WindowsLine = fmt.Sprintf("shared with you · %s · %s", modeWord(g.Mode), expiryPhrase(g.NA, false, time.Now())) | ||
| } | ||
| } | ||
| rows = append(rows, row) | ||
| } | ||
| changed := first || !rowsEqual(ov.model.Rows, rows) | ||
| ov.machines = merged | ||
|
|
@@ -266,15 +281,33 @@ func (ov *overviewState) handleKey(ctx context.Context, ev keyEvent) (done bool, | |
| return false, err | ||
| } | ||
| } | ||
| case ovShare: | ||
| if row, ok := ov.model.Selected(); ok { | ||
| if row.Shared { | ||
| ov.model.Status = "that's a share — only its owner can share it onward" | ||
| break | ||
| } | ||
| if i := ov.model.Cursor; i >= 0 && i < len(ov.machines) { | ||
| ov.share(ctx, ov.machines[i]) | ||
| } | ||
| } | ||
| case ovRename: | ||
| if row, ok := ov.model.Selected(); ok { | ||
| if row.Shared { | ||
| ov.model.Status = "that's a share — only the owner can rename it; it expires on its own" | ||
| break | ||
| } | ||
| ov.prompt = promptRename | ||
| ov.input = nil | ||
| ov.model.Prompt = "new name for " + row.Name + ": " | ||
| ov.model.Input = "" | ||
| } | ||
| case ovRetire: | ||
| if row, ok := ov.model.Selected(); ok { | ||
| if row.Shared { | ||
| ov.model.Status = "that's a share — it expires on its own; nothing to retire" | ||
| break | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename prompt drops the letter sMedium Severity
Additional Locations (1)Reviewed by Cursor Bugbot for commit 7b4fdd2. Configure here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overview attach skips expired-share checkMedium Severity
Additional Locations (1)Reviewed by Cursor Bugbot for commit 7b4fdd2. Configure here. |
||
| ov.prompt = promptRetire | ||
| ov.input = nil | ||
| ov.model.Prompt = "Retire " + row.Name + "? It disappears from every device; the machine and its tmux keep running; `" + | ||
|
|
@@ -557,3 +590,50 @@ func (f *detachFilter) Read(p []byte) (int, error) { | |
| f.buf = out | ||
| } | ||
| } | ||
|
|
||
| // pumpReader adapts the overview's stdin pump to a plain io.Reader for the | ||
| // share ceremony's line prompts (the terminal is back in cooked mode there, so | ||
| // the kernel line-buffers and each Read hands over a full line). | ||
| type pumpReader struct { | ||
| pump *stdinPump | ||
| buf []byte | ||
| } | ||
|
|
||
| func (r *pumpReader) Read(p []byte) (int, error) { | ||
| if len(r.buf) == 0 { | ||
| chunk, ok := <-r.pump.ch | ||
| if !ok { | ||
| return 0, io.EOF | ||
| } | ||
| r.buf = chunk | ||
| } | ||
| n := copy(p, r.buf) | ||
| r.buf = r.buf[n:] | ||
| return n, nil | ||
| } | ||
|
|
||
| // share runs the mint ceremony for the selected machine: leave the alt screen | ||
| // and raw mode (the ceremony prints a QR and asks questions), run it with the | ||
| // share defaults (read-only, 1 h, session main — flags need the command form), | ||
| // then come back to the overview. | ||
| func (ov *overviewState) share(ctx context.Context, m client.Machine) { | ||
| a := ov.app | ||
| fmt.Fprint(a.out, altScreenOff) | ||
| _ = term.Restore(ov.fd, ov.raw) | ||
|
|
||
| sa := *a | ||
| sa.in = &pumpReader{pump: ov.pump} | ||
| err := sa.shareResolved(ctx, ov.dir, ov.idn, m, identity.GrantDefaultTTL, false, "main", defaults.WebURL(), ov.ice()) | ||
|
|
||
| if _, rerr := term.MakeRaw(ov.fd); rerr != nil && err == nil { | ||
| err = rerr | ||
| } | ||
| fmt.Fprint(a.out, altScreenOn) | ||
| switch { | ||
| case err != nil: | ||
| ov.model.Status = err.Error() | ||
| default: | ||
| ov.model.Status = "shared — `" + a.binary + " share ls` lists it" | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overview share reports success on declineMedium Severity Overview Additional Locations (1)Reviewed by Cursor Bugbot for commit 7b4fdd2. Configure here. |
||
| ov.draw() | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a guest opens the overview after a grant expires and presses Enter on this shared row, the overview's
attachpath bypasses the expiry check added tocmdAttach. It therefore enters the reconnect loop and retries the agent's refusal for roughly the full failure budget instead of immediately explaining that the share ended; validateGuestGrantFor(...).ValidAt(...)before making the row attachable.Useful? React with 👍 / 👎.