From ba5688fd9ccc59de55749e47d7530f038bbf7244 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 13:10:11 +0000 Subject: [PATCH] Setup: no idle timeout in the sysop's full-screen setup The setup on a caller's line ended after idle_timeout, and modemmenu -setup on the console quit after a minute in an open dialog or pull-down. The tui Desk now takes Idle < 0 as no limit, and a Deadline that still ends a wait; the setup uses both, so only a hangup or the session's time_limit ends it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01B82eRTXXCusbxh6h6Btenm --- cmd/modemmenu/setup.go | 20 +++++++++++--------- docs/setup.md | 6 ++++-- internal/tui/desk.go | 4 +++- internal/tui/keys.go | 26 ++++++++++++++++++++++---- internal/tui/tui_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 16 deletions(-) diff --git a/cmd/modemmenu/setup.go b/cmd/modemmenu/setup.go index 1d71b96..e5a8548 100644 --- a/cmd/modemmenu/setup.go +++ b/cmd/modemmenu/setup.go @@ -438,7 +438,11 @@ func (u *setupUI) run() error { if err := d.Show(scr); err != nil { return err } - k, alt, err := d.Key(-1) + wait := time.Duration(-1) // no idle limit: the sysop's setup waits + if u.size != nil { + wait = time.Minute // the size looked at again + } + k, alt, err := d.Key(wait) if errors.Is(err, term.ErrTimeout) && u.size != nil { continue // at the console: no one to hang up on } @@ -913,7 +917,7 @@ func runSetupLocal(path string) int { return 1 } u.size = size - d.Idle = time.Minute // the size looked at again + d.Idle = -1 // the setup never times out u.who = "on the console" if err := u.run(); err != nil { fmt.Fprintln(os.Stderr, "modemmenu:", err) @@ -968,15 +972,13 @@ func setupItem(t *term.Term, cfg *settings, it *menuItem, s *session, deadline t } u.who = "user=" + s.User logf("setup: %s by user=%s", cfg.path, s.User) - if !deadline.IsZero() && time.Until(deadline) < d.Idle { - d.Idle = max(time.Until(deadline), time.Second) - } + // no idle limit for the sysop's setup: only the session's time limit + // (time_limit) ends it, or a hangup + d.Idle = -1 + d.Deadline = deadline err = u.run() if errors.Is(err, term.ErrTimeout) { - if !deadline.IsZero() && !time.Now().Before(deadline) { - return errTimeLimit - } - return t.Write("\r\nIdle too long: the setup ended (what was not saved is not).\r\n") + return errTimeLimit } return err } diff --git a/docs/setup.md b/docs/setup.md index 0512900..f02716c 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -56,8 +56,10 @@ groups = sysops It must have `users` or `groups` (modemmenu refuses one without), and is never a guest's. It needs an ANSI terminal of 40x14 at least (80x24 is -better); others are told so. The idle timeout and the call's time limit -hold in it; what was not saved when the call ends is lost. The file is +better); others are told so. The idle timeout does not hold in it: the +setup waits for the sysop however long they take. The call's time limit +(`time_limit`) still does, and what was not saved when the call ends is +lost. The file is written as the user modemmenu runs as, which must be able to write it. ## Terminals diff --git a/internal/tui/desk.go b/internal/tui/desk.go index f78edbf..aacd2c9 100644 --- a/internal/tui/desk.go +++ b/internal/tui/desk.go @@ -28,8 +28,10 @@ type Desk struct { Shade bool Menus []Menu // the menu bar's pull-downs; none: a bar with Name // how long a key may take where the screens wait without a limit of - // their own (0: a day); ErrTimeout after + // their own (0: a day, < 0: no limit); ErrTimeout after Idle time.Duration + // a time no wait goes past (zero: none); ErrTimeout at it + Deadline time.Time on *Screen // what the terminal shows (nil: not known) last *Screen // what was shown last diff --git a/internal/tui/keys.go b/internal/tui/keys.go index d86b2ef..7ff467e 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -184,14 +184,32 @@ func (d *Desk) next(wait time.Duration) (byte, error) { } if wait < 0 { wait = d.Idle - if wait <= 0 { + if wait == 0 { wait = 24 * time.Hour } } - if wait == 0 { - return 0, ErrTimeout + forever := wait < 0 // Idle < 0: until a key, a hangup or the Deadline + for { + w := wait + if forever { + w = 24 * time.Hour + } + if !d.Deadline.IsZero() { + left := time.Until(d.Deadline) + if left <= 0 { + return 0, ErrTimeout + } + w = min(w, left) + } + if w == 0 { + return 0, ErrTimeout + } + b, err := d.Line.ReadOne(w) + if forever && errors.Is(err, ErrTimeout) { + continue + } + return b, err } - return d.Line.ReadOne(wait) } func (d *Desk) unread(b byte) { d.pending = append([]byte{b}, d.pending...) } diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index 2b5b1a8..4fcab05 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -168,3 +168,42 @@ func TestPullDown(t *testing.T) { t.Errorf("menu keys") } } + +// slowLine: times out a few times before its one key +type slowLine struct { + quiet int + waits []time.Duration +} + +func (l *slowLine) ReadOne(d time.Duration) (byte, error) { + l.waits = append(l.waits, d) + if l.quiet > 0 { + l.quiet-- + return 0, ErrTimeout + } + return 'x', nil +} + +func (l *slowLine) WriteRaw(string) error { return nil } + +// TestNoIdle: Idle < 0 waits through the line's timeouts for a key; +// a Deadline still ends the wait +func TestNoIdle(t *testing.T) { + l := &slowLine{quiet: 3} + d := &Desk{Line: l, Idle: -1} + if k, _, err := d.Key(-1); err != nil || k != 'x' { + t.Fatalf("Key = %q, %v; want 'x'", k, err) + } + if len(l.waits) != 4 { + t.Fatalf("%d reads, want 4", len(l.waits)) + } + l = &slowLine{quiet: 1 << 30} + d = &Desk{Line: l, Idle: -1, Deadline: time.Now().Add(-time.Second)} + if _, _, err := d.Key(-1); err != ErrTimeout { + t.Fatalf("past the deadline: %v, want ErrTimeout", err) + } + d = &Desk{Line: &slowLine{quiet: 1}, Idle: time.Minute} + if _, _, err := d.Key(-1); err != ErrTimeout { + t.Fatalf("Idle a minute: %v, want ErrTimeout", err) + } +}