From e54bb9c1c66a9cd4b24b6cce4376b288eb37ea80 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 07:53:45 +0000 Subject: [PATCH] Full-screen menu for ANSI callers, and modemmenu -setup for sysops The menu comes up full screen on a terminal that can have it (ANSI, or a VT that said what it is, 40x14 at least): the items in a window on a blue desk, a bar moved with the cursor keys, an item's key starting it at once, the welcome at the top of the window, the motd in a box, the user, rate, clock and minutes left on the bars. Others get the menu of lines; the login stays lines, so AutoPPP and the AOL client sniffing are as they were. [menu] fullscreen = auto|no, item help =. modemmenu -setup changes modemmenu.conf on full screens in the style of the 1990s BBS setup programs (the bbs's setup screens, ported): pull-down menus, cascading windows, the sample's help of each key, values typed in a box or picked from a list. Saving checks the file first and keeps its comments and order. An [item] of mode = setup (users or groups needed) gives it to a sysop who calls in. - internal/tui: the screen buffer and its diff rendering (from bbs textui/desk.go), UTF-8, code page 437, the VT line set and ASCII, keys, windows, pull-downs and dialogs - internal/confedit: changes to the file by line, comments kept - internal/confdoc: the sample's help, from tools/confref (shared) - term: a UTF-8 probe for ANSI terminals; [terminal] charset - below 9600 bit/s the desk is plain blue, cleared in a few bytes - the sample: comments for keys that took a neighbour's help - tests: the line menu's tests ask for fullscreen = no; one handoff test waits for the server's last log line Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01B82eRUujUBmci66AZkngmh --- .github/screens.png | Bin 0 -> 226534 bytes README.md | 3 + cmd/modemmenu/access_test.go | 2 +- cmd/modemmenu/dropfile_test.go | 2 +- cmd/modemmenu/e2e_test.go | 19 +- cmd/modemmenu/fullscreen.go | 262 ++++++++ cmd/modemmenu/fullscreen_test.go | 281 ++++++++ cmd/modemmenu/handoff_test.go | 8 +- cmd/modemmenu/launch.go | 2 + cmd/modemmenu/main.go | 32 +- cmd/modemmenu/serve_many_test.go | 2 +- cmd/modemmenu/server_test.go | 4 +- cmd/modemmenu/session.go | 5 + cmd/modemmenu/settings.go | 25 +- cmd/modemmenu/setup.go | 1001 ++++++++++++++++++++++++++++ cmd/modemmenu/terminal.go | 23 + cmd/modemmenu/terminal_test.go | 2 +- docs/README.md | 3 +- docs/configuration.md | 41 +- docs/reference.md | 23 +- docs/setup.md | 74 ++ internal/confdoc/confdoc.go | 199 ++++++ internal/confedit/confedit.go | 301 +++++++++ internal/confedit/confedit_test.go | 134 ++++ internal/term/detect.go | 27 + internal/tui/charset.go | 149 +++++ internal/tui/desk.go | 370 ++++++++++ internal/tui/dialog.go | 245 +++++++ internal/tui/keys.go | 197 ++++++ internal/tui/list.go | 320 +++++++++ internal/tui/screen.go | 345 ++++++++++ internal/tui/tui_test.go | 170 +++++ mkdocs.yml | 1 + samples/modemmenu.conf.sample | 29 + samples/samples.go | 13 + tools/confref/main.go | 223 +------ 36 files changed, 4307 insertions(+), 230 deletions(-) create mode 100644 .github/screens.png create mode 100644 cmd/modemmenu/fullscreen.go create mode 100644 cmd/modemmenu/fullscreen_test.go create mode 100644 cmd/modemmenu/setup.go create mode 100644 docs/setup.md create mode 100644 internal/confdoc/confdoc.go create mode 100644 internal/confedit/confedit.go create mode 100644 internal/confedit/confedit_test.go create mode 100644 internal/tui/charset.go create mode 100644 internal/tui/desk.go create mode 100644 internal/tui/dialog.go create mode 100644 internal/tui/keys.go create mode 100644 internal/tui/list.go create mode 100644 internal/tui/screen.go create mode 100644 internal/tui/tui_test.go create mode 100644 samples/samples.go diff --git a/.github/screens.png b/.github/screens.png new file mode 100644 index 0000000000000000000000000000000000000000..72d545df3a2fa4a77af9d1e65338b2872073ab8a GIT binary patch literal 226534 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

a>%z=50BBGQr|N|5`Jm*#5FU!fB*_O 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 %s; cat "$MODEM_SESSION_FILE" >> %s; echo args: {user} {baud} {d m, s := testPty(t) defer m.Close() - cmd := exec.Command(bin, "-config", conf) + cmd := exec.Command(bin, "-config", lineMenu(conf)) cmd.Stdin, cmd.Stdout, cmd.Stderr = s, s, s cmd.Env = append(os.Environ(), "DMODEM_RATE=33600/31200", "DMODEM_CALLERID=5559999", "DMODEM_DIALED=5551000", "DMODEM_CHANNEL=Local/s@test", "TMPDIR="+dir) @@ -287,7 +287,7 @@ func TestHangup(t *testing.T) { os.WriteFile(conf, []byte("[general]\nlog = none\n[file]\npath = "+users+ "\n[item ppp]\ncommand = exec true\n"), 0o600) m, s := testPty(t) - cmd := exec.Command(bin, "-config", conf) + cmd := exec.Command(bin, "-config", lineMenu(conf)) cmd.Stdin, cmd.Stdout, cmd.Stderr = s, s, s cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true} if err := cmd.Start(); err != nil { @@ -307,3 +307,18 @@ func TestHangup(t *testing.T) { t.Fatal("modemmenu is still running after the hangup") } } + +// lineMenu: the configuration at path with the menu of lines (not full +// screen), for the tests that read it as lines; path +func lineMenu(path string) string { + f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0) + if err != nil { + return path + } + defer f.Close() + b, _ := os.ReadFile(path) + if !bytes.Contains(b, []byte("fullscreen =")) { + f.WriteString("\n[menu]\nfullscreen = no\n") + } + return path +} diff --git a/cmd/modemmenu/fullscreen.go b/cmd/modemmenu/fullscreen.go new file mode 100644 index 0000000..4b2a27b --- /dev/null +++ b/cmd/modemmenu/fullscreen.go @@ -0,0 +1,262 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +package main + +import ( + "errors" + "fmt" + "io" + "regexp" + "strconv" + "strings" + "time" + + "github.com/D-Modem/menu/internal/config" + "github.com/D-Modem/menu/internal/term" + "github.com/D-Modem/menu/internal/tui" +) + +// The menu full screen, on a terminal that can have it (ANSI, or a VT +// that said what it is, of 40x14 at least): the items in a window on the +// desk, a bar moved with the cursor keys, an item's key picking it at +// once. Others get the menu of lines. The login stays lines, for the +// dialers and AOL clients that read and answer it. + +// deskFor is the full screen for the caller's terminal, nil when it +// cannot have one (or [menu] fullscreen = no). +func deskFor(t *term.Term, s *settings, sess *session) *tui.Desk { + if !s.fullscreen || t.PETSCII || (sess.Terminal != "ansi" && sess.Terminal != "vt") { + return nil + } + rows, _ := strconv.Atoi(sess.Rows) + cols, _ := strconv.Atoi(sess.Cols) + if rows == 0 || cols == 0 { + rows, cols = 24, 80 + } + if !tui.OK(rows, cols) { + return nil + } + d := &tui.Desk{Line: t, Rows: rows, Cols: cols, Idle: s.idleTimeout} + // the shaded desk costs a byte or three a cell: not below 9600 bit/s + if rate, err := strconv.Atoi(sess.TxRate); err != nil || rate >= 9600 { + d.Shade = true + } + // a DEC terminal itself has no colors; the emulators do + d.Mono = sess.hardware + // the lines: UTF-8 where it showed é as one character; else a VT's + // own set for what said it is a VT or xterm, code page 437 for the + // ANSI terminals of BBSs (which ask nothing of them) and SyncTERM + cs, ok := tui.ParseCharset(s.terminal.charset) + switch { + case ok: + case sess.charset == "utf8", sess.charset == "" && sess.Term == "xterm": + cs = tui.UTF8 + case sess.charset != "cp437" && (strings.HasPrefix(sess.Term, "vt") || sess.Term == "xterm"): + cs = tui.DEC + default: + cs = tui.CP437 + } + d.Charset = cs + return d +} + +// escapes: what a template's text has that is not to be read +var escapes = regexp.MustCompile(`\x1b\[[0-9;?]*[@-~]|\x1b.|[\x00-\x08\x0b-\x1f\x7f]`) + +// plainLines: a template's text as lines to put in a window, without its +// escapes and empty lines +func plainLines(s string) []string { + s = escapes.ReplaceAllString(strings.ReplaceAll(s, "\r", ""), "") + var out []string + for _, l := range strings.Split(s, "\n") { + if l = strings.TrimSpace(l); l != "" { + out = append(out, l) + } + } + return out +} + +// fullMenu: the menu full screen until the caller hangs up, says goodbye, +// the time is up or a selection ends with after = hangup; first (if not +// nil) starts at once. The desk is down when it returns. +func fullMenu(t *term.Term, s *settings, sess *session, lim *limiter, first *menuItem, d *tui.Desk, motd string) error { + defer d.Close() + items := s.visible(sess) + title := "Main menu" + if l := plainLines(config.Expand(s.menuTitle, sess.vars(false), nil)); len(l) > 0 { + title = l[0] + } + w := &tui.List{Title: title, Center: true, MinW: 44} + if l := plainLines(config.Expand(s.welcome, sess.vars(false), nil)); len(l) > 0 { + w.Notes = l + } + for _, it := range items { + w.Items = append(w.Items, tui.Entry{ID: it.name, Key: it.key, Label: it.label, Help: it.help}) + } + w.Items = append(w.Items, tui.Entry{Key: s.hangup[0], Label: "Goodbye (hang up)", Help: "Hang up the line."}) + w.Fix() + d.Name = sess.User + if sp := sess.Speed(); sp != "" { + d.Name += " at " + sp + } + // a box to show on the desk: the motd first, then what launch said + // went wrong + notice, noticeTitle, noticeKind := "", "", tui.Info + if m := strings.TrimSpace(escapes.ReplaceAllString(strings.ReplaceAll(motd, "\r", ""), "")); m != "" { + notice, noticeTitle = m, "Message of the day" + } + for { + lim.mark(sess, time.Now()) + left, limited := lim.left(time.Now()) + if limited && left <= 0 { + return errTimeLimit + } + idle := time.Now().Add(s.idleTimeout) + if limited && lim.deadline.Before(idle) { + idle = lim.deadline + } + it := first + first = nil + for it == nil { + now := time.Now() + left, limited := lim.left(now) + frame := func() *tui.Screen { + scr := d.Blank(-1) + d.Status(scr, now.Format("15:04")) + w.Draw(d, scr, true) + if e := w.Selected(); e != nil { + help := e.Help + if help == "" { + help = "Enter starts it, or press its key: " + e.Key + "." + } + d.HelpBox(scr, e.Label, help, false) + } + status := "" + if limited { + status = fmt.Sprintf("%d min left", int((left+30*time.Second)/time.Minute)) + } + d.KeyBar(scr, tui.Hints("↑↓", "Choose", "Enter", "Select", "Esc", "Goodbye", "F1", "Help"), status) + return scr + } + scr := frame() + if notice != "" { + if _, err := d.Message(scr, noticeKind, noticeTitle, notice); err != nil { + return err + } + notice = "" + continue + } + if err := d.Show(scr); err != nil { + return err + } + // a key until idle, the clock redrawn each minute + wait := min(time.Until(idle), time.Until(now.Truncate(time.Minute).Add(time.Minute))) + k, _, err := d.Key(max(wait, time.Millisecond)) + if errors.Is(err, term.ErrTimeout) { + if time.Now().Before(idle) { + continue + } + if limited && !time.Now().Before(lim.deadline) { + return errTimeLimit + } + return term.ErrTimeout + } + if err != nil { + return err + } + if terminated.Load() { + return term.ErrHangup + } + idle = time.Now().Add(s.idleTimeout) + if limited && lim.deadline.Before(idle) { + idle = lim.deadline + } + pick := false + switch k { + case tui.KeyEnter, tui.KeyRight: + pick = true + case 0x04: + return io.EOF + case tui.KeyEsc: + yes, err := d.Confirm(frame(), tui.Info, "Goodbye", "Hang up the line?") + if err != nil { + return err + } + if yes { + return nil + } + case tui.F(1), '?': + if _, err := d.Message(frame(), tui.Info, "Help", menuHelp); err != nil { + return err + } + default: + _, pick = w.Move(d, k) + } + if !pick { + continue + } + e := w.Selected() + if e == nil { + continue + } + if e.ID == "" { + return nil // goodbye + } + it = selectFrom(items, strings.ToUpper(e.Key)) + } + + // the selection has the line: the desk down, the screen clear + d.Close() + sess.Selection = it.name + lim.mark(sess, time.Now()) + logf("start %s user=%s mode=%s", it.name, sess.User, it.mode) + err := launch(t, s, it, sess, lim.deadline, logf) + if errors.Is(err, term.ErrHangup) || terminated.Load() { + return term.ErrHangup + } + if errors.Is(err, errTimeLimit) { + logf("%s: time limit", it.name) + return errTimeLimit + } + if err != nil { + logf("%s: %v", it.name, err) + notice, noticeTitle, noticeKind = it.label+" is not available now.", "Not available", tui.Alert + continue + } + logf("end %s user=%s", it.name, sess.User) + if it.after == "hangup" { + return nil + } + if pausesAfter(it) { + // what it wrote stays until the caller has read it + if err := pause(t, d, s, "Press any key for the menu"); err != nil { + return err + } + } + } +} + +// menuHelp: F1 in the full-screen menu +const menuHelp = "Move the bar with the cursor keys (or the first letter of an item) " + + "and press Enter, or press an item's key in [ ] to start it at once.\n\n" + + "Esc hangs up, after asking. ^L draws the screen again." + +// pausesAfter: whether what an item wrote is kept on the screen until a +// key (programs here; not a server that drew its own screens) +func pausesAfter(it *menuItem) bool { + switch it.mode { + case "exec", "passwd", "callers": + return true + } + return false +} + +// pause: "press a key" under what is on the screen, until one or idle +func pause(t *term.Term, d *tui.Desk, s *settings, say string) error { + if err := t.Write("\r\n\x1b[0;7m " + say + " \x1b[0m"); err != nil { + return err + } + _, _, err := d.Key(s.idleTimeout) + return err +} diff --git a/cmd/modemmenu/fullscreen_test.go b/cmd/modemmenu/fullscreen_test.go new file mode 100644 index 0000000..88f57e1 --- /dev/null +++ b/cmd/modemmenu/fullscreen_test.go @@ -0,0 +1,281 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "testing" + "time" + + "github.com/D-Modem/menu/internal/tui" +) + +// TestFullScreenMenu: an ANSI caller at 2400 bit/s gets the menu full +// screen (in code page 437, the desk plain): an item by its key, the +// pause after it, the sysop's setup and back, goodbye by its key; a +// caller whose terminal says nothing gets the menu of lines +func TestFullScreenMenu(t *testing.T) { + dir := t.TempDir() + bin := filepath.Join(dir, "modemmenu") + if out, err := exec.Command("go", "build", "-o", bin, ".").CombinedOutput(); err != nil { + t.Fatalf("build: %v\n%s", err, out) + } + users := filepath.Join(dir, "users") + os.WriteFile(users, []byte("alice:{PLAIN}secret\nbob:{PLAIN}pw\n"), 0o600) + conf := filepath.Join(dir, "modemmenu.conf") + os.WriteFile(conf, []byte(fmt.Sprintf(` +[general] +log = none +auth = file +fail_delay = 0 +motd = "The line is down on Sunday." +[file] +path = %s +[groups] +sysops = alice +[item board] +key = B +label = The board +help = Messages and files +mode = exec +command = echo hello from the board +after = menu +[item setup] +key = Z +label = Setup +mode = setup +groups = sysops +`, users)), 0o600) + + start := func(answer bool) *caller { + m, s := testPty(t) + t.Cleanup(func() { m.Close() }) + cmd := exec.Command(bin, "-config", conf) + cmd.Stdin, cmd.Stdout, cmd.Stderr = s, s, s + cmd.Env = append(os.Environ(), "DMODEM_RATE=2400") + cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true} + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + s.Close() + t.Cleanup(func() { cmd.Process.Kill(); cmd.Wait() }) + if answer { + return newCaller(t, m) + } + return newTermCaller(t, m, func([]byte) []byte { return nil }) + } + + c := start(true) + c.expect("login: ") + c.send("alice\r") + c.expect("Password: ") + c.send("secret\r") + // the desk, cleared in its blue, the motd in a box over the menu + got := c.expect("The line is down on Sunday.") + if !strings.Contains(got, "\x1b[0m\x1b[0;37;44m\x1b[2J") { + t.Errorf("no desk: %q", got) + } + got += c.expect(" OK ") + if strings.Contains(got, "░") || strings.Contains(got, "\xb0") || strings.Contains(got, "╔") { + t.Errorf("a shaded desk, or UTF-8, at 2400 bit/s: %q", got) + } + if !strings.Contains(got, "\xc9\xcd") { // ╔═ in code page 437 + t.Errorf("no code page 437 frame: %q", got) + } + c.send("\r") + c.expect("The board") + c.expect("Messages and files") + c.expect("Goodbye (hang up)") + c.send("b") + c.expect("\x1b[2J") + c.expect("hello from the board") + c.expect("Press any key for the menu") + c.send(" ") + c.expect("The board") + + // the setup, and F9 back to the menu + c.send("z") + c.expect("modemmenu Setup") + c.expect("Login and messages") + c.send("\x1b[20~") + c.expect("The board") + c.send("g") + c.expect("Goodbye.") + + // bob is no sysop: no setup on his menu, and its key does nothing + c = start(true) + c.expect("login: ") + c.send("bob\r") + c.expect("Password: ") + c.send("pw\r") + c.expect(" OK ") + c.send("\r") + got = c.expect("Goodbye (hang up)") + if strings.Contains(got, "Setup") { + t.Errorf("bob sees the setup: %q", got) + } + c.send("z") + time.Sleep(300 * time.Millisecond) + c.send("g") + got = c.expect("Goodbye.") + if strings.Contains(got, "modemmenu Setup") { + t.Errorf("bob got the setup: %q", got) + } + + // a terminal that answers nothing: the menu of lines + c = start(false) + c.expect("login: ") + c.send("alice\r") + c.expect("Password: ") + c.send("secret\r") + c.expect("[B] The board") + c.expect("Selection: ") + c.send("g\r") + c.expect("Goodbye.") +} + +// TestSetupNeedsSysops: an item of mode = setup is for some only +func TestSetupNeedsSysops(t *testing.T) { + dir := t.TempDir() + for _, c := range []struct { + item, err string + }{ + {"mode = setup", "needs users or groups"}, + {"mode = setup\nguest = yes\nusers = alice", "not for guests"}, + {"mode = setup\nusers = alice", ""}, + } { + p := filepath.Join(dir, "c.conf") + os.WriteFile(p, []byte("[general]\nlog = none\nauth = file\n[file]\npath = /dev/null\n[item s]\nkey = S\n"+c.item+"\n"), 0o600) + _, err := loadSettings(p) + if c.err == "" && err != nil || c.err != "" && (err == nil || !strings.Contains(err.Error(), c.err)) { + t.Errorf("%q: %v", c.item, err) + } + } +} + +// keyLine: keys queued for the setup, what it writes kept +type keyLine struct { + in []byte + out bytes.Buffer +} + +func (f *keyLine) ReadOne(time.Duration) (byte, error) { + if len(f.in) == 0 { + return 0, tui.ErrTimeout + } + b := f.in[0] + f.in = f.in[1:] + return b, nil +} + +func (f *keyLine) WriteRaw(s string) error { f.out.WriteString(s); return nil } + +// TestSetup: values changed through the screens and saved, the file's +// comments and order kept; a section added, an item renamed in [menu] +// items too; a file that does not check is not saved without asking +func TestSetup(t *testing.T) { + dir := t.TempDir() + users := filepath.Join(dir, "users") + os.WriteFile(users, []byte("alice:{PLAIN}secret\n"), 0o600) + conf := filepath.Join(dir, "modemmenu.conf") + orig := fmt.Sprintf(`; my line +[general] +log = none +auth = file +; how many tries +attempts = 3 ; three +[file] +path = %s +[menu] +items = bbs +[item bbs] +key = B +label = Board +mode = exec +command = true +`, users) + os.WriteFile(conf, []byte(orig), 0o640) + + // an Esc and what follows must not be taken as Alt: they come apart + const esc = "\x1b\x00" + const ( + up, enter, f9, f10 = "\x1b[A", "\r", "\x1b[20~", "\x1b[21~" + ) + keys := strings.Join([]string{ + enter, // Login and messages + "a", "a", enter, // auth, attempts: typed + "\x15", "5", enter, // + esc, // back to the first window + "g", enter, // Guest account + enter, up, enter, // enabled: from the list, no + esc, // + "m", "m", enter, enter, // Menu items, bbs + "\x1b[F", up, enter, "\x15board", enter, // End, Rename + esc, esc, // + f10, f9, + }, "") + f := &keyLine{in: []byte(keys)} + d := &tui.Desk{Line: f, Rows: 25, Cols: 80, Charset: tui.UTF8} + u, err := newSetup(d, conf) + if err != nil { + t.Fatal(err) + } + u.size = func() (int, int) { return 25, 80 } + if err := u.run(); err != nil { + if scr := d.Screen(); scr != nil { + t.Log("\n" + scr.Text()) + } + t.Fatal(err) + } + b, _ := os.ReadFile(conf) + want := fmt.Sprintf(`; my line +[general] +log = none +auth = file +; how many tries +attempts = 5 ; three +[file] +path = %s +[menu] +items = board +[item board] +key = B +label = Board +mode = exec +command = true + +[guest] +enabled = no +`, users) + if string(b) != want { + t.Errorf("saved:\n%s\nwant:\n%s", b, want) + } + if st, _ := os.Stat(conf); st.Mode().Perm() != 0o640 { + t.Errorf("mode %v", st.Mode()) + } + if _, err := loadSettings(conf); err != nil { + t.Errorf("does not load: %v", err) + } + + // a mistake: the check says, Back keeps the file as it is + keys = strings.Join([]string{enter, "a", "a", enter, "\x15", "many", enter, f10, enter, f9, "d"}, "") + f.in = []byte(keys) + f.out.Reset() + u, _ = newSetup(d, conf) + if err := u.run(); err != nil { + t.Fatal(err) + } + if !strings.Contains(f.out.String(), "not a number") { + t.Errorf("the check's error not shown") + } + if b2, _ := os.ReadFile(conf); string(b2) != want { + t.Errorf("saved after a failed check:\n%s", b2) + } +} diff --git a/cmd/modemmenu/handoff_test.go b/cmd/modemmenu/handoff_test.go index 7af5cde..f2c726a 100644 --- a/cmd/modemmenu/handoff_test.go +++ b/cmd/modemmenu/handoff_test.go @@ -342,7 +342,13 @@ command = echo "pppd up"; exec sleep 1000 if d := time.Since(start); d > 20*time.Second { t.Errorf("hung up %v after PPP started", d) } - log, _ := os.ReadFile(filepath.Join(dir, "server.log")) + // the server logs the session's end just after the line closes + var log []byte + for end := time.Now().Add(5 * time.Second); time.Now().Before(end); time.Sleep(50 * time.Millisecond) { + if log, _ = os.ReadFile(filepath.Join(dir, "server.log")); strings.Contains(string(log), "ended") { + break + } + } if !strings.Contains(string(log), "on the line handed over") || !strings.Contains(string(log), "ended") { t.Errorf("log:\n%s", log) } diff --git a/cmd/modemmenu/launch.go b/cmd/modemmenu/launch.go index cd06d93..936ddcd 100644 --- a/cmd/modemmenu/launch.go +++ b/cmd/modemmenu/launch.go @@ -48,6 +48,8 @@ func launch(t *term.Term, cfg *settings, it *menuItem, s *session, deadline time return changePasswordItem(t, cfg, it, s, deadline) case "callers": return callersItem(t, cfg, it) + case "setup": + return setupItem(t, cfg, it, s, deadline) case "exec": return runExec(t, cfg, it, s, deadline, logf) default: diff --git a/cmd/modemmenu/main.go b/cmd/modemmenu/main.go index a418bfd..d25285e 100644 --- a/cmd/modemmenu/main.go +++ b/cmd/modemmenu/main.go @@ -51,11 +51,20 @@ func main() { listenAt := flag.String("listen", "", "with -serve: listen on this unix socket `path` (or tcp:host:port) instead of [server] listen") upstreamFlag := flag.Bool("upstream", false, "a session of -serve with trust_user (internal)") authdFlag := flag.Bool("authd", false, "answer the [authd] auth socket alone (with -serve it is answered too)") + setupFlag := flag.Bool("setup", false, "change the configuration on a full screen (the sysop's setup)") flag.Parse() if *hash { os.Exit(printHash()) } + if *setupFlag { + // before the file is loaded: the setup is how one that does not + // load is put right + if s, err := loadSettings(*config); err == nil { + openLog(s.log) + } + os.Exit(runSetupLocal(*config)) + } s, err := loadSettings(*config) if err != nil { fmt.Fprintln(os.Stderr, "modemmenu:", err) @@ -248,8 +257,21 @@ func run(t *term.Term, s *settings, sess *session) int { } lim.mark(sess, time.Now()) - t.Write(config.Expand(s.welcome, sess.vars(false), nil)) - t.Write(motd(s, sess)) + desk := deskFor(t, s, sess) + m := motd(s, sess) + if desk == nil || strings.Contains(m, "\x1b") { + // the full screen has the welcome in its window, and a motd of + // plain text in a box; one with ANSI's colors is shown as it is + t.Write(config.Expand(s.welcome, sess.vars(false), nil)) + t.Write(m) + if desk != nil && m != "" { + if err := pause(t, desk, s, "Press any key to continue"); err != nil { + logf("logout user=%s: %v", sess.User, describe(err)) + return 0 + } + } + m = "" + } var first *menuItem if rule != nil && rule.item != "" { first = s.selects(strings.ToUpper(rule.item)) @@ -257,7 +279,11 @@ func run(t *term.Term, s *settings, sess *session) int { first = nil } } - err = menu(t, s, sess, lim, first) + if desk != nil { + err = fullMenu(t, s, sess, lim, first, desk, m) + } else { + err = menu(t, s, sess, lim, first) + } switch { case err == nil: t.Write(config.Expand(s.goodbye, sess.vars(false), nil)) diff --git a/cmd/modemmenu/serve_many_test.go b/cmd/modemmenu/serve_many_test.go index f2bbf63..776e766 100644 --- a/cmd/modemmenu/serve_many_test.go +++ b/cmd/modemmenu/serve_many_test.go @@ -29,7 +29,7 @@ func startServe(t *testing.T, dir, conf, sock string) { if _, _, err := openPty(); err != nil { t.Skip("no pty:", err) } - srv := exec.Command(bin, "-config", conf, "-serve") + srv := exec.Command(bin, "-config", lineMenu(conf), "-serve") out, err := os.Create(filepath.Join(dir, "serve.out")) if err != nil { t.Fatal(err) diff --git a/cmd/modemmenu/server_test.go b/cmd/modemmenu/server_test.go index 671e7a7..8da48ad 100644 --- a/cmd/modemmenu/server_test.go +++ b/cmd/modemmenu/server_test.go @@ -82,7 +82,7 @@ label = Info command = echo "on $(tty | cut -c1-8) dialed=$MODEM_DIALED rate=$MODEM_RATE date=$MODEM_DATE auth=$MODEM_AUTH user=$MODEM_USER pw=$MODEM_PASSWORD up=$MODEMMENU_UPSTREAM_USER" `, filepath.Join(dir, "server.log"), users, sock)), 0o600) - srv := exec.Command(bin, "-config", conf, "-serve") + srv := exec.Command(bin, "-config", lineMenu(conf), "-serve") if err := srv.Start(); err != nil { t.Fatal(err) } @@ -144,7 +144,7 @@ header = yes `, users, sock)), 0o600) m, s := testPty(t) defer m.Close() - cmd := exec.Command(bin, "-config", front) + cmd := exec.Command(bin, "-config", lineMenu(front)) cmd.Stdin, cmd.Stdout, cmd.Stderr = s, s, s cmd.Env = append(os.Environ(), "DMODEM_RATE=26400/26400", "DMODEM_DIALED=5552000") cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true} diff --git a/cmd/modemmenu/session.go b/cmd/modemmenu/session.go index 325b7a7..f079620 100644 --- a/cmd/modemmenu/session.go +++ b/cmd/modemmenu/session.go @@ -39,6 +39,11 @@ type session struct { TermName string `json:"term_name"` // what it said it is: xterm(390), VT220, SyncTERM (CTerm 1.316) Rows string `json:"rows"` Cols string `json:"cols"` + // the characters it shows: utf8, cp437 (SyncTERM), 8bit (not UTF-8), + // "" not asked + charset string + // a DEC terminal itself (by its DA2), not an emulator: no colors + hardware bool // an upstream menu's deadline, from its session header upstreamDeadline time.Time diff --git a/cmd/modemmenu/settings.go b/cmd/modemmenu/settings.go index 2bf660e..0e89f92 100644 --- a/cmd/modemmenu/settings.go +++ b/cmd/modemmenu/settings.go @@ -41,6 +41,7 @@ type menuItem struct { key string // shown in the menu choices []string // what selects it, upper case: key, name, aliases label string + help string // a line of what it is, in the full-screen menu mode string // exec, tcp, rlogin, telnet command string host string @@ -99,6 +100,8 @@ type settings struct { menuPrompt string hangup []string // what hangs up, upper case; the first one shown hotkeys bool // one key selects, without a line end + fullscreen bool // the menu full screen on terminals that can have it + path string // the configuration file's items []*menuItem backends []auth.Authenticator } @@ -110,7 +113,7 @@ func loadSettings(path string) (*settings, error) { if err != nil { return nil, err } - s := &settings{} + s := &settings{path: path} g := c.Section("general") s.log = g.Get("log", "syslog") s.banner = config.Unescape(g.Get("banner", `\r\nD-Modem\r\n`)) @@ -220,6 +223,13 @@ func loadSettings(path string) (*settings, error) { if s.hotkeys, err = m.Bool("hotkeys", false); err != nil { return nil, err } + switch fs := strings.ToLower(m.Get("fullscreen", "auto")); fs { + case "auto", "yes": + s.fullscreen = true + case "no": + default: + return nil, fmt.Errorf("[menu] fullscreen: %q (auto or no)", fs) + } keys := map[string]string{} for _, h := range s.hangup { keys[h] = "[menu] hangup" @@ -286,6 +296,7 @@ func loadItem(name string, sec *config.Section) (*menuItem, error) { } } it.label = sec.Get("label", name) + it.help = sec.Get("help", "") it.mode = strings.ToLower(sec.Get("mode", "exec")) it.command = sec.Get("command", "") it.host = sec.Get("host", "127.0.0.1") @@ -307,7 +318,7 @@ func loadItem(name string, sec *config.Section) (*menuItem, error) { it.runAs = sec.Get("run_as", "") it.tty = strings.ToLower(sec.Get("tty", "raw")) defAfter := "hangup" - if m := strings.ToLower(sec.Get("mode", "")); m == "passwd" || m == "callers" { + if m := strings.ToLower(sec.Get("mode", "")); m == "passwd" || m == "callers" || m == "setup" { // a password changed is no reason to hang up defAfter = "menu" } @@ -366,6 +377,14 @@ func loadItem(name string, sec *config.Section) (*menuItem, error) { switch it.mode { case "passwd", "callers": // built in: no command, no server + case "setup": + // the configuration changed from the line: for some only + if len(it.access.users) == 0 && len(it.access.groups) == 0 { + return nil, fmt.Errorf("[item %s] mode = setup needs users or groups (the sysops')", name) + } + if it.access.guest { + return nil, fmt.Errorf("[item %s] mode = setup is not for guests", name) + } case "exec": if it.command == "" { return nil, fmt.Errorf("[item %s] mode = exec needs a command", name) @@ -381,7 +400,7 @@ func loadItem(name string, sec *config.Section) (*menuItem, error) { return nil, fmt.Errorf("[item %s] path: %q (an absolute path)", name, it.path) } default: - return nil, fmt.Errorf("[item %s] mode: %q (exec, tcp, unix, telnet, rlogin, passwd or callers)", name, it.mode) + return nil, fmt.Errorf("[item %s] mode: %q (exec, tcp, unix, telnet, rlogin, passwd, callers or setup)", name, it.mode) } if it.tty != "raw" && it.tty != "sane" { return nil, fmt.Errorf("[item %s] tty: %q (raw or sane)", name, it.tty) diff --git a/cmd/modemmenu/setup.go b/cmd/modemmenu/setup.go new file mode 100644 index 0000000..1d71b96 --- /dev/null +++ b/cmd/modemmenu/setup.go @@ -0,0 +1,1001 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +package main + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "syscall" + "time" + + "github.com/D-Modem/menu/internal/confdoc" + "github.com/D-Modem/menu/internal/confedit" + "github.com/D-Modem/menu/internal/term" + "github.com/D-Modem/menu/internal/tui" + "github.com/D-Modem/menu/samples" + "golang.org/x/sys/unix" +) + +// The sysop's setup: modemmenu.conf on a full screen, as the BBS setup +// programs of the 1990s had theirs. Each section is a window of its +// keys, with the sample's help for the key under the bar; values are typed +// in a box or picked from a list. Saving checks the file as -check does +// first, and keeps its comments and order: only the lines of the keys +// changed are. modemmenu -setup runs it on the console (or over SSH); +// an [item] with mode = setup on the line of a sysop who calls in. + +// kindGroups: the windows of the first one, under their headings (and +// the menus of the bar); a section the sample has that is in none goes +// under Other +var kindGroups = []struct { + label string + kinds []string +}{ + {"Login", []string{"general", "terminal", "callerid", "number", "guest", "signup"}}, + {"Users", []string{"file", "ldap", "sql", "system", "groups"}}, + {"Menu", []string{"menu", "item", "dropfile"}}, + {"Dial-up", []string{"ppp_pool", "autoppp", "aol"}}, + {"Server", []string{"server", "authd"}}, +} + +// kindTitles: a section's window's title +var kindTitles = map[string]string{ + "general": "Login and messages", "terminal": "Terminal detection", "callerid": "Caller ID", + "number": "Numbers by caller ID", "guest": "Guest account", "signup": "New accounts (sign-up)", + "file": "Users file", "ldap": "LDAP", "sql": "SQL database", "system": "System accounts (PAM)", + "groups": "Groups", "menu": "The menu", "item": "Menu items", "dropfile": "Door drop files", + "ppp_pool": "PPP addresses and DNS", "autoppp": "AutoPPP (dialers)", "aol": "AOL networks", + "server": "Server (-serve)", "authd": "Auth socket (authd)", +} + +// setupUI is a setup session: the file, what it was, the windows up. +type setupUI struct { + d *tui.Desk + path string + doc *confedit.Doc + orig *confedit.Doc // as it was read or last saved + isNew bool // the file is not there yet: the sample to begin with + kinds []*confdoc.Kind + byKind map[string]*confdoc.Kind + stack []*page + flash string // a word for the help box, once + warn bool + size func() (rows, cols int) // the terminal's, if it can change (nil: no) + who string // for the log +} + +// page is a window: the first one, a section's, or a list of the named +// sections of a kind ([item NAME]). +type page struct { + what string // "main", "section", "named" + sec string // the section ("item bbs"), or the kind of the named + kind *confdoc.Kind + l *tui.List +} + +// newSetup: the setup of the file at path (the sample when there is none) +func newSetup(d *tui.Desk, path string) (*setupUI, error) { + u := &setupUI{d: d, path: path, byKind: map[string]*confdoc.Kind{}} + b, err := os.ReadFile(path) + switch { + case errors.Is(err, os.ErrNotExist): + b, u.isNew = []byte(samples.Conf), true + u.flash = path + " is not there yet: this is the sample, saved there with F10." + case err != nil: + return nil, err + } + u.doc, u.orig = confedit.Parse(string(b)), confedit.Parse(string(b)) + u.kinds = confdoc.Parse(samples.Conf) + for _, k := range u.kinds { + u.byKind[k.Name] = k + } + d.Name = "modemmenu Setup" + d.Menus = u.menus() + u.stack = []*page{{what: "main", l: &tui.List{Title: "modemmenu Setup", MinW: 50}}} + return u, nil +} + +// groups: kindGroups, and Other for the sample's sections in none +func (u *setupUI) groups() []struct { + label string + kinds []string +} { + out := append(kindGroups[:0:0], kindGroups...) + in := map[string]bool{} + for _, g := range kindGroups { + for _, k := range g.kinds { + in[k] = true + } + } + var other []string + for _, k := range u.kinds { + if !in[k.Name] { + other = append(other, k.Name) + } + } + if len(other) > 0 { + out = append(out, struct { + label string + kinds []string + }{"Other", other}) + } + return out +} + +// menus: the bar's pull-downs +func (u *setupUI) menus() []tui.Menu { + ms := []tui.Menu{{Label: "File", Items: []tui.MenuItem{ + {Label: "Save F10", ID: "save"}, {Label: "Check", ID: "check"}, + {Label: "Undo all changes", ID: "revert"}, {Heading: true}, {Label: "Quit F9", ID: "quit"}}}} + for _, g := range u.groups() { + m := tui.Menu{Label: g.label} + for _, k := range g.kinds { + if u.byKind[k] != nil { + m.Items = append(m.Items, tui.MenuItem{Label: u.title(k), ID: "kind:" + k}) + } + } + ms = append(ms, m) + } + return append(ms, tui.Menu{Label: "Help", Items: []tui.MenuItem{{Label: "Keys", ID: "keys"}, {Label: "About", ID: "about"}}}) +} + +func (u *setupUI) title(kind string) string { + if t, ok := kindTitles[kind]; ok { + return t + } + return "[" + kind + "]" +} + +// named: the sections of a named kind in the file ("item bbs"...) +func (u *setupUI) named(kind string) []string { + var out []string + for _, s := range u.doc.Sections() { + if strings.HasPrefix(s, kind+" ") { + out = append(out, s) + } + } + return out +} + +// fill: a page's entries from the file as it is now +func (u *setupUI) fill(p *page) { + var items []tui.Entry + switch p.what { + case "main": + for _, g := range u.groups() { + items = append(items, tui.Entry{Heading: true, Label: g.label}) + for _, k := range g.kinds { + kd := u.byKind[k] + if kd == nil { + continue + } + e := tui.Entry{ID: "kind:" + k, Label: u.title(k), Help: kd.Help, Mark: "►"} + switch { + case kd.Named: + n := len(u.named(k)) + e.Value = fmt.Sprintf("%d set", n) + case !u.doc.Has(k): + e.Value, e.Unset = "not set", true + case u.doc.Has(k) && hasKey(kd, "enabled"): + v, ok := u.doc.Get(k, "enabled") + if !ok { + v = kd.ByKey["enabled"].Value + } + e.Value = map[bool]string{true: "on", false: "off"}[yes(v)] + } + e.Changed = u.sectionChanged(k, kd.Named) + items = append(items, e) + } + } + case "named": + for _, s := range u.named(p.kind.Name) { + name := strings.TrimPrefix(s, p.kind.Name+" ") + v, _ := u.doc.Get(s, "label") + if p.kind.Name == "number" { + v, _ = u.doc.Get(s, "user") + } + items = append(items, tui.Entry{ID: "sec:" + s, Label: name, Value: v, Mark: "►", + Changed: u.changed(s, ""), Help: "Enter opens it; Del deletes it."}) + } + items = append(items, tui.Entry{Heading: true}, tui.Entry{ID: "add", Label: "Add a new one…", + Help: "A new [" + p.kind.Name + " NAME]: its name first."}) + case "section": + kd := p.kind + if kd.Free != "" { + for _, k := range u.doc.Keys(p.sec) { + v, _ := u.doc.Get(p.sec, k) + items = append(items, tui.Entry{ID: "key:" + k, Label: k, Value: v, Changed: u.changed(p.sec, k), + Help: "Each key is " + kd.Free + ". Del takes it out."}) + } + items = append(items, tui.Entry{Heading: true}, tui.Entry{ID: "addkey", Label: "Add a new one…", Help: "Each key is " + kd.Free + "."}) + break + } + known := map[string]bool{} + for _, k := range kd.Keys { + known[k.Name] = true + e := u.keyEntry(p.sec, k) + if strings.TrimSpace(k.Help) == "" && kd.Help != "" { + // the section's comment is the key's + e.Help = capFirst(strings.TrimSpace(kd.Help)) + } + items = append(items, e) + } + for _, k := range u.doc.Keys(p.sec) { + if !known[k] { + v, _ := u.doc.Get(p.sec, k) + items = append(items, tui.Entry{ID: "key:" + k, Label: k, Value: v, Changed: u.changed(p.sec, k), + Help: "Not a key of [" + kd.Name + "] that modemmenu knows: a mistake, or for a newer version. Del takes it out."}) + } + } + if kd.Named { + items = append(items, tui.Entry{Heading: true}, + tui.Entry{ID: "rename", Label: "Rename…", Help: "A new name for this [" + p.sec + "]."}, + tui.Entry{ID: "delete", Label: "Delete…", Help: "Take [" + p.sec + "] out of the file."}) + } + } + p.l.Items = items + p.l.Fix() +} + +// keyEntry: a key's line in its section's window +func (u *setupUI) keyEntry(sec string, k *confdoc.Key) tui.Entry { + e := tui.Entry{ID: "key:" + k.Name, Label: k.Name, Changed: u.changed(sec, k.Name)} + if v, ok := u.doc.Get(sec, k.Name); ok { + e.Value = v + if v == "" { + e.Value = `""` + } + } else { + e.Value, e.Unset = "(not set)", true + if k.Value != "" { + e.Note = "sample: " + k.Value + } + } + e.Help = keyHelp(k) + if len(choices(k)) > 1 { + e.Mark = "↕" + } + return e +} + +// keyHelp: a key's help as the sample has it +func keyHelp(k *confdoc.Key) string { + h := strings.TrimSpace(k.Help) + if h == "" { + h = "See the comments in the sample." + } + h = strings.ToUpper(h[:1]) + h[1:] + if !strings.HasSuffix(h, ".") && !strings.HasSuffix(h, ")") { + h += "." + } + if k.Value != "" { + h += " Sample: " + k.Value + } + return h +} + +// choiceRE: a value the help names (confdoc writes them "; `name`: what") +var choiceRE = regexp.MustCompile("`([a-z0-9_]+)`:") + +// choices: the values a key takes, when the sample says (yes and no for +// a yes-or-no; the names its help gives); nil when it is anything typed +func choices(k *confdoc.Key) []string { + if isBool(k.Value) { + return []string{"yes", "no"} + } + var out []string + seen := map[string]bool{} + add := func(v string) { + if v != "" && !seen[v] && !strings.ContainsAny(v, " {}/\\\"") { + seen[v] = true + out = append(out, v) + } + } + for _, m := range choiceRE.FindAllStringSubmatch(k.Help, -1) { + add(m[1]) + } + if len(out) == 0 { + return nil + } + add(k.Value) + for _, e := range k.Examples { + add(e) + } + return out +} + +func isBool(v string) bool { + switch strings.ToLower(v) { + case "yes", "no", "true", "false", "on", "off": + return true + } + return false +} + +func yes(v string) bool { + switch strings.ToLower(v) { + case "yes", "true", "on", "1": + return true + } + return false +} + +func hasKey(k *confdoc.Kind, name string) bool { return k.ByKey[name] != nil } + +// changed: whether a key (or, key "", any of a section) is not as it was +func (u *setupUI) changed(sec, key string) bool { + if key == "" { + if u.doc.Has(sec) != u.orig.Has(sec) { + return true + } + keys := append(u.doc.Keys(sec), u.orig.Keys(sec)...) + for _, k := range keys { + if u.changed(sec, k) { + return true + } + } + return false + } + a, aok := u.doc.Get(sec, key) + b, bok := u.orig.Get(sec, key) + return a != b || aok != bok +} + +// sectionChanged: a section's, or with named any of the kind's +func (u *setupUI) sectionChanged(kind string, named bool) bool { + if !named { + return u.changed(kind, "") + } + all := map[string]bool{} + for _, s := range append(u.doc.Sections(), u.orig.Sections()...) { + if strings.HasPrefix(s, kind+" ") { + all[s] = true + } + } + for s := range all { + if u.changed(s, "") { + return true + } + } + return false +} + +// changes: how many keys are not as they were +func (u *setupUI) changes() int { + n := 0 + secs := map[string]bool{} + for _, s := range append(u.doc.Sections(), u.orig.Sections()...) { + secs[s] = true + } + for s := range secs { + keys := map[string]bool{} + for _, k := range append(u.doc.Keys(s), u.orig.Keys(s)...) { + keys[k] = true + } + for k := range keys { + if u.changed(s, k) { + n++ + } + } + if len(keys) == 0 && u.doc.Has(s) != u.orig.Has(s) { + n++ + } + } + return n +} + +// frame: the desk with the windows up (open: a bar menu pulled down) +func (u *setupUI) frame(open int) *tui.Screen { + d := u.d + scr := d.Blank(open) + for i, p := range u.stack { + p.l.Depth = i + p.l.Draw(d, scr, i == len(u.stack)-1) + } + top := u.stack[len(u.stack)-1] + switch e := top.l.Selected(); { + case u.flash != "": + d.HelpBox(scr, "Note", u.flash, u.warn) + case e != nil: + d.HelpBox(scr, e.Label, e.Help, false) + } + status := "" + if n := u.changes(); n > 0 || u.isNew { + status = fmt.Sprintf("%d unsaved", n) + if u.isNew { + status = "not saved yet" + } + } + keys := tui.Hints("F1", "Help", "Enter", "Change", "Esc", "Back", "/", "Menu") + if top.what == "section" { + keys = append(keys, tui.Hint{Key: "Del", Label: "Default"}) + } + keys = append(keys, tui.Hints("F9", "Quit", "F10", "Save")...) + d.KeyBar(scr, keys, status) + return scr +} + +// run: the setup until the sysop quits (or hangs up: the error) +func (u *setupUI) run() error { + d := u.d + defer d.Close() + for { + if u.size != nil { + if r, c := u.size(); (r != d.Rows || c != d.Cols) && tui.OK(r, c) { + d.Rows, d.Cols = r, c + d.Redraw() + } + } + top := u.stack[len(u.stack)-1] + u.fill(top) + scr := u.frame(-1) + u.flash, u.warn = "", false + if err := d.Show(scr); err != nil { + return err + } + k, alt, err := d.Key(-1) + if errors.Is(err, term.ErrTimeout) && u.size != nil { + continue // at the console: no one to hang up on + } + if err != nil { + return err + } + if alt || k == '/' || k == tui.F(12) { + i := 0 + if alt { + if i = d.MenuKey(k); i < 0 { + continue + } + } + id, err := d.PullDown(u.frame, i) + if err != nil { + return err + } + done, err := u.command(id) + if done || err != nil { + return err + } + continue + } + switch k { + case tui.F(10): + if _, err := u.save(); err != nil { + return err + } + case tui.F(9): + if done, err := u.quit(); done || err != nil { + return err + } + case tui.KeyEsc, tui.KeyLeft: + if len(u.stack) > 1 { + u.stack = u.stack[:len(u.stack)-1] + continue + } + if done, err := u.quit(); done || err != nil { + return err + } + case tui.F(1), '?': + if err := u.help(top); err != nil { + return err + } + case tui.KeyDel, tui.KeyBackspace: + if err := u.remove(top); err != nil { + return err + } + case tui.KeyEnter, tui.KeyRight, ' ': + if err := u.pick(top, k == ' '); err != nil { + return err + } + default: + top.l.Move(d, k) + } + } +} + +// command: a pull-down's item; done: the setup is over +func (u *setupUI) command(id string) (bool, error) { + switch { + case id == "": + case id == "save": + _, err := u.save() + return false, err + case id == "check": + err := checkConf(u.path, u.doc.String()) + if err != nil { + _, err = u.d.Message(u.frame(-1), tui.Alert, "Check", err.Error()) + return false, err + } + u.flash = "The configuration checks: " + u.summary() + case id == "revert": + if u.changes() == 0 { + return false, nil + } + ok, err := u.d.Confirm(u.frame(-1), tui.Alert, "Undo", "Undo every change since the file was read or saved?") + if ok { + u.doc = confedit.Parse(u.orig.String()) + u.stack = u.stack[:1] + } + return false, err + case id == "quit": + return u.quit() + case id == "keys": + _, err := u.d.Message(u.frame(-1), tui.Info, "Keys", setupKeys) + return false, err + case id == "about": + _, err := u.d.Message(u.frame(-1), tui.Info, "About", "modemmenu Setup\n\n"+u.path+ + "\n\nThe help of each key is the sample configuration's. Saving keeps the file's comments and order.") + return false, err + case strings.HasPrefix(id, "kind:"): + u.stack = u.stack[:1] + u.open(strings.TrimPrefix(id, "kind:")) + } + return false, nil +} + +// setupKeys: Help > Keys +const setupKeys = "↑ ↓ PgUp PgDn Home End, or a letter: move the bar.\n" + + "Enter: open a window, change a value (typed, or picked from a list).\n" + + "Space: the next of a value's choices. Del: back to the default.\n" + + "Esc: back a window. / or F12 or Alt and a letter: the menus.\n" + + "F1: the help of the line. F10: save. F9: quit. ^L: draw again." + +// open: the window of a kind (its section's, or its named ones') +func (u *setupUI) open(kind string) { + kd := u.byKind[kind] + if kd == nil { + return + } + if kd.Named { + u.stack = append(u.stack, &page{what: "named", sec: kind, kind: kd, + l: &tui.List{Title: u.title(kind), MinW: 44}}) + return + } + u.stack = append(u.stack, &page{what: "section", sec: kind, kind: kd, + l: &tui.List{Title: "[" + kind + "] " + u.title(kind), MinW: 56}}) +} + +// pick: Enter (or Space: the next choice) on the bar's entry +func (u *setupUI) pick(p *page, space bool) error { + e := p.l.Selected() + if e == nil { + return nil + } + d := u.d + switch { + case strings.HasPrefix(e.ID, "kind:"): + if !space { + u.open(strings.TrimPrefix(e.ID, "kind:")) + } + case strings.HasPrefix(e.ID, "sec:"): + if !space { + s := strings.TrimPrefix(e.ID, "sec:") + u.stack = append(u.stack, &page{what: "section", sec: s, kind: p.kind, + l: &tui.List{Title: "[" + s + "]", MinW: 56}}) + } + case e.ID == "add": + name, ok, err := d.Input(u.frame(-1), "New ["+p.kind.Name+" NAME]", "Its name (one word: letters, digits, - and _"+ + map[bool]string{true: "; for a number, the caller's number", false: ""}[p.kind.Name == "number"]+"):", "", 32, false) + if err != nil || !ok || name == "" { + return err + } + if !nameRE.MatchString(name) || u.doc.Has(p.kind.Name+" "+name) { + u.flash, u.warn = fmt.Sprintf("%q is not a name that can be had: one word, not one there already.", name), true + return nil + } + sec := p.kind.Name + " " + name + u.doc.AddSection(sec) + if p.kind.Name == "item" { + u.doc.Set(sec, "label", name) + if v, ok := u.doc.Get(menuSection, "items"); ok { + u.doc.Set(menuSection, "items", strings.TrimSpace(v+" "+name)) + } + } + u.stack = append(u.stack, &page{what: "section", sec: sec, kind: p.kind, l: &tui.List{Title: "[" + sec + "]", MinW: 56}}) + case e.ID == "addkey": + name, ok, err := d.Input(u.frame(-1), "New key", "Its name (one word):", "", 32, false) + if err != nil || !ok || name == "" { + return err + } + name = strings.ToLower(name) + if !nameRE.MatchString(name) { + u.flash, u.warn = fmt.Sprintf("%q is not one word.", name), true + return nil + } + v, ok, err := d.Input(u.frame(-1), name, p.kind.Free, "", 255, false) + if err != nil || !ok { + return err + } + u.doc.Set(p.sec, name, v) + case e.ID == "rename": + kind := p.kind.Name + old := strings.TrimPrefix(p.sec, kind+" ") + name, ok, err := d.Input(u.frame(-1), "Rename", "A new name for ["+p.sec+"]:", old, 32, false) + if err != nil || !ok || name == "" || name == old { + return err + } + if !nameRE.MatchString(name) || u.doc.Has(kind+" "+name) { + u.flash, u.warn = fmt.Sprintf("%q is not a name that can be had: one word, not one there already.", name), true + return nil + } + u.doc.RenameSection(p.sec, kind+" "+name) + if kind == "item" { + u.renameInItems(old, name) + } + p.sec = kind + " " + name + p.l.Title = "[" + p.sec + "]" + case e.ID == "delete": + ok, err := d.Confirm(u.frame(-1), tui.Alert, "Delete", "Take ["+p.sec+"] out of the file?") + if err != nil || !ok { + return err + } + u.doc.RemoveSection(p.sec) + if p.kind.Name == "item" { + u.renameInItems(strings.TrimPrefix(p.sec, "item "), "") + } + u.stack = u.stack[:len(u.stack)-1] + case strings.HasPrefix(e.ID, "key:"): + return u.edit(p, strings.TrimPrefix(e.ID, "key:"), space) + } + return nil +} + +// nameRE: a section's NAME, a key the sysop names +var nameRE = regexp.MustCompile(`^[A-Za-z0-9_+*#.-]{1,32}$`) + +// renameInItems: an item's name in [menu] items changed (to "": out) +func (u *setupUI) renameInItems(old, name string) { + v, ok := u.doc.Get(menuSection, "items") + if !ok { + return + } + var out []string + for _, f := range strings.Fields(strings.ReplaceAll(v, ",", " ")) { + if f == old { + if name == "" { + continue + } + f = name + } + out = append(out, f) + } + u.doc.Set(menuSection, "items", strings.Join(out, " ")) +} + +// edit: a key's value: picked from its choices, or typed +func (u *setupUI) edit(p *page, key string, space bool) error { + d := u.d + var k *confdoc.Key + if p.kind != nil { + k = p.kind.ByKey[key] + } + if k == nil { + k = &confdoc.Key{Name: key} + } + cur, set := u.doc.Get(p.sec, key) + cs := choices(k) + if len(cs) > 1 { + at := -1 + for i, c := range cs { + if strings.EqualFold(c, cur) { + at = i + } + } + if space { + u.doc.Set(p.sec, key, cs[(at+1)%len(cs)]) + return nil + } + def := "(not set: the default)" + list := append(append([]string{}, cs...), def, "Type a value…") + start := at + if !set { + start = len(cs) + } + i, ok, err := d.Choose(u.frame(-1), key, list, nil, max(start, 0)) + if err != nil || !ok { + return err + } + switch { + case i < len(cs): + u.doc.Set(p.sec, key, cs[i]) + return nil + case i == len(cs): + u.doc.Unset(p.sec, key) + return nil + } + } else if space { + return nil + } + prompt := strings.TrimSpace(k.Help) + if prompt != "" { + prompt = strings.ToUpper(prompt[:1]) + prompt[1:] + } + if len(prompt) > 230 { + prompt = prompt[:229] + "…" + } + if !strings.HasSuffix(prompt, ".") && prompt != "" { + prompt += "." + } + if !set && k.Value != "" { + prompt += " Sample: " + k.Value + } + v, ok, err := d.Input(u.frame(-1), "["+p.sec+"] "+key, prompt+" (Empty: not set, the default; \"\" for nothing.)", cur, 1024, false) + if err != nil || !ok { + return err + } + switch v { + case "": + u.doc.Unset(p.sec, key) + case `""`: + u.doc.Set(p.sec, key, "") + default: + u.doc.Set(p.sec, key, v) + } + return nil +} + +// remove: Del on a key (its default again) or a named section (out) +func (u *setupUI) remove(p *page) error { + e := p.l.Selected() + if e == nil { + return nil + } + switch { + case strings.HasPrefix(e.ID, "key:"): + u.doc.Unset(p.sec, strings.TrimPrefix(e.ID, "key:")) + case strings.HasPrefix(e.ID, "sec:"): + s := strings.TrimPrefix(e.ID, "sec:") + ok, err := u.d.Confirm(u.frame(-1), tui.Alert, "Delete", "Take ["+s+"] out of the file?") + if err != nil || !ok { + return err + } + u.doc.RemoveSection(s) + if p.kind.Name == "item" { + u.renameInItems(strings.TrimPrefix(s, "item "), "") + } + } + return nil +} + +// help: F1, the whole help of the bar's line +func (u *setupUI) help(p *page) error { + e := p.l.Selected() + if e == nil { + return nil + } + text := e.Help + if strings.HasPrefix(e.ID, "key:") && p.kind != nil { + if k := p.kind.ByKey[strings.TrimPrefix(e.ID, "key:")]; k != nil { + text = keyHelp(k) + if len(k.Examples) > 0 { + text += "\n\nAlso: " + strings.Join(k.Examples, ", ") + } + } + } + _, err := u.d.Message(u.frame(-1), tui.Info, e.Label, text) + return err +} + +// summary: what the file sets up, for a check that passed +func (u *setupUI) summary() string { + items := len(u.named("item")) + return fmt.Sprintf("%d menu items.", items) +} + +// save: the file checked and written; whether it was +func (u *setupUI) save() (bool, error) { + text := u.doc.String() + if err := checkConf(u.path, text); err != nil { + i, err := u.d.Message(u.frame(-1), tui.Alert, "Check", "The configuration does not check:\n\n"+err.Error()+ + "\n\nmodemmenu would not start with it.", "Back", "Save anyway") + if err != nil || i != 1 { + return false, err + } + } + if err := writeConf(u.path, text); err != nil { + _, err = u.d.Message(u.frame(-1), tui.Alert, "Save", "Not saved: "+err.Error()) + return false, err + } + logf("setup: %s saved (%d changes) %s", u.path, u.changes(), u.who) + serverChanged := u.changed("server", "") + u.orig, u.isNew = confedit.Parse(text), false + u.flash = "Saved to " + u.path + ". The next call reads it." + if serverChanged { + u.flash += " [server] is read when modemmenu -serve starts: restart it." + } + return true, nil +} + +// quit: whether the setup ends (after asking to save what changed) +func (u *setupUI) quit() (bool, error) { + if u.changes() == 0 && !u.isNew { + return true, nil + } + i, err := u.d.Message(u.frame(-1), tui.Info, "Quit", "Save the changes first?", "Save", "Don't save", "Cancel") + if err != nil { + return true, err + } + switch i { + case 0: + saved, err := u.save() + return saved, err + case 1: + return true, nil + } + return false, nil +} + +// checkConf: whether text loads as a configuration at path would (in a +// file beside it, for its relative paths) +func checkConf(path, text string) error { + f, err := os.CreateTemp(filepath.Dir(path), ".modemmenu-check-*.conf") + if err != nil { + f, err = os.CreateTemp("", "modemmenu-check-*.conf") + if err != nil { + return err + } + } + defer os.Remove(f.Name()) + _, err = f.WriteString(text) + f.Close() + if err != nil { + return err + } + _, err = loadSettings(f.Name()) + if err != nil { + return errors.New(strings.ReplaceAll(err.Error(), f.Name(), path)) + } + return nil +} + +// writeConf: text as the file at path: a new file renamed over it (its +// mode and owner kept), or written in place where its directory takes no +// new file +func writeConf(path, text string) error { + mode := os.FileMode(0o640) + uid, gid := -1, -1 + if st, err := os.Stat(path); err == nil { + mode = st.Mode().Perm() + if sys, ok := st.Sys().(*syscall.Stat_t); ok { + uid, gid = int(sys.Uid), int(sys.Gid) + } + } + f, err := os.CreateTemp(filepath.Dir(path), ".modemmenu-*.conf") + if err != nil { + return os.WriteFile(path, []byte(text), mode) + } + tmp := f.Name() + _, err = f.WriteString(text) + if cerr := f.Close(); err == nil { + err = cerr + } + if err == nil { + err = os.Chmod(tmp, mode) + } + if err != nil { + os.Remove(tmp) + return err + } + if uid >= 0 { + os.Chown(tmp, uid, gid) // as it was, where we may + } + if err := os.Rename(tmp, path); err != nil { + os.Remove(tmp) + return err + } + return nil +} + +// runSetupLocal: modemmenu -setup on the terminal it was started on +func runSetupLocal(path string) int { + t := term.New(os.Stdin, os.Stdout) + defer t.Close() + size := func() (int, int) { + ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) + if err != nil || ws.Row == 0 || ws.Col == 0 { + return 24, 80 + } + return int(ws.Row), int(ws.Col) + } + rows, cols := size() + if !tui.OK(rows, cols) { + fmt.Fprintf(os.Stderr, "modemmenu: the setup needs 40x14 at least (this is %dx%d)\n", cols, rows) + return 1 + } + d := &tui.Desk{Line: t, Rows: rows, Cols: cols, Charset: localCharset(path), Mono: localMono(), Shade: true} + u, err := newSetup(d, path) + if err != nil { + fmt.Fprintln(os.Stderr, "modemmenu:", err) + return 1 + } + u.size = size + d.Idle = time.Minute // the size looked at again + u.who = "on the console" + if err := u.run(); err != nil { + fmt.Fprintln(os.Stderr, "modemmenu:", err) + return 1 + } + return 0 +} + +// localCharset: [terminal] charset if it says, else UTF-8 where the +// locale is, a VT's lines on a VT, ASCII otherwise +func localCharset(path string) tui.Charset { + if b, err := os.ReadFile(path); err == nil { + if v, ok := confedit.Parse(string(b)).Get("terminal", "charset"); ok { + if cs, ok := tui.ParseCharset(v); ok { + return cs + } + } + } + for _, k := range []string{"LC_ALL", "LC_CTYPE", "LANG"} { + if v := os.Getenv(k); v != "" { + v = strings.ToLower(strings.ReplaceAll(v, "-", "")) + if strings.Contains(v, "utf8") { + return tui.UTF8 + } + break + } + } + switch t := os.Getenv("TERM"); { + case strings.HasPrefix(t, "vt"), strings.HasPrefix(t, "xterm"), strings.HasPrefix(t, "screen"), strings.HasPrefix(t, "tmux"): + return tui.DEC + case t == "linux", t == "ansi", t == "pcansi", strings.HasPrefix(t, "syncterm"): + return tui.CP437 + } + return tui.ASCII +} + +// localMono: a terminal without colors (a VT's TERM) +func localMono() bool { + t := os.Getenv("TERM") + return strings.HasPrefix(t, "vt1") || strings.HasPrefix(t, "vt2") || strings.HasPrefix(t, "vt3") || t == "dumb" +} + +// setupItem: an [item] of mode = setup: the setup on the caller's line +func setupItem(t *term.Term, cfg *settings, it *menuItem, s *session, deadline time.Time) error { + d := deskForSetup(t, cfg, s) + if d == nil { + return t.Write("\r\nThe setup needs an ANSI terminal of 80x24 (40x14 at least).\r\n") + } + u, err := newSetup(d, cfg.path) + if err != nil { + return t.Write("\r\nThe setup cannot read " + cfg.path + ": " + err.Error() + "\r\n") + } + 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) + } + 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 err +} + +// deskForSetup: the full screen for the setup, whether or not the menu +// is full screen +func deskForSetup(t *term.Term, cfg *settings, s *session) *tui.Desk { + c := *cfg + c.fullscreen = true + return deskFor(t, &c, s) +} + +// capFirst: s with its first letter a capital +func capFirst(s string) string { + if s == "" { + return s + } + return strings.ToUpper(s[:1]) + s[1:] +} + +// menuSection: [menu], whose items = the setup keeps up with the items' +const menuSection = "menu" diff --git a/cmd/modemmenu/terminal.go b/cmd/modemmenu/terminal.go index c3f377a..7392bda 100644 --- a/cmd/modemmenu/terminal.go +++ b/cmd/modemmenu/terminal.go @@ -12,6 +12,7 @@ import ( "github.com/D-Modem/menu/internal/config" "github.com/D-Modem/menu/internal/term" + "github.com/D-Modem/menu/internal/tui" "golang.org/x/sys/unix" ) @@ -20,6 +21,7 @@ type terminalSettings struct { detect bool // ask: ANSI, VT52, an ANSI terminal's identity petscii string // auto (a terminal that is not ANSI may be one), yes (every caller), no timeout time.Duration + charset string // the full screen's lines: auto, utf8, cp437, dec, ascii } // loadTerminal: [terminal], checked @@ -27,6 +29,10 @@ func loadTerminal(sec *config.Section) (terminalSettings, error) { var err error ts := terminalSettings{ petscii: strings.ToLower(sec.Get("petscii", "auto")), + charset: strings.ToLower(sec.Get("charset", "auto")), + } + if _, ok := tui.ParseCharset(ts.charset); !ok && ts.charset != "auto" { + return ts, fmt.Errorf("[terminal] charset: %q (auto, utf8, cp437, dec or ascii)", ts.charset) } if ts.detect, err = sec.Bool("detect", true); err != nil { return ts, err @@ -102,9 +108,26 @@ func detectTerminal(t *term.Term, s *settings, sess *session) error { sess.TermName = n } sess.Term = in.Term() + sess.hardware = in.Hardware() if in.Level() > 0 || in.Emulator() { sess.Terminal = "vt" } + // what its characters are, for the full screen's lines + switch { + case ts.charset != "auto" || !s.fullscreen: + case in.CTerm(): + sess.charset = "cp437" + default: + utf8, ok, err := p.UTF8(ts.timeout) + if err != nil { + return err + } + if ok && utf8 { + sess.charset = "utf8" + } else if ok { + sess.charset = "8bit" + } + } return nil } diff --git a/cmd/modemmenu/terminal_test.go b/cmd/modemmenu/terminal_test.go index c2ce994..5c3ae46 100644 --- a/cmd/modemmenu/terminal_test.go +++ b/cmd/modemmenu/terminal_test.go @@ -224,7 +224,7 @@ func TestDetectPETSCII(t *testing.T) { // startOn: modemmenu on the slave of a pty func startOn(t *testing.T, bin, conf string, s *os.File, env ...string) *exec.Cmd { - cmd := exec.Command(bin, "-config", conf) + cmd := exec.Command(bin, "-config", lineMenu(conf)) cmd.Stdin, cmd.Stdout, cmd.Stderr = s, s, s cmd.Env = append(os.Environ(), env...) cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true} diff --git a/docs/README.md b/docs/README.md index 8dd3eb6..90e5b8f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,8 +2,9 @@ - [Building](building.md): Go, PAM, `make install` - [Running it from app_dmodem](running.md): a program per call, one server on a unix socket, FreePBX -- [Configuration](configuration.md): `modemmenu.conf`, the connect rate, terminals, the message of the day, the menu and its items, guests and sign-up, caller ID, time limits, drop files, last callers, handing callers on +- [Configuration](configuration.md): `modemmenu.conf`, the connect rate, terminals, the message of the day, the menu (full screen or lines) and its items, guests and sign-up, caller ID, time limits, drop files, last callers, handing callers on - [Configuration reference](reference.md): every section and key, from the sample +- [The sysop's setup](setup.md): `modemmenu -setup`, the configuration on a full screen, and on the line for sysops - [Authentication](authentication.md): users file, LDAP, SQL, system accounts (PAM), changing passwords, the auth socket - [PPP dialers](ppp.md): AutoPPP and PAP, the pppd plugin - [AOL callers](aol.md): the classic AOL client dialing in, its network played diff --git a/docs/configuration.md b/docs/configuration.md index c29897c..507467c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -65,7 +65,8 @@ mode = passwd Comments start with `;` or `#`, on a line of their own or after a value (following a space or tab, outside `"quotes"`); `\;` and `\#` are the characters themselves. `modemmenu -check -config FILE` checks a -configuration. +configuration, and `modemmenu -setup -config FILE` changes it on a full +screen ([the sysop's setup](setup.md)). ## The connect rate @@ -171,6 +172,44 @@ menu shows the key, its labels lined up. - `[menu] title`, `prompt` and `items` (the order) set the rest; see the sample. +## The full-screen menu + +A caller whose terminal can have it (ANSI, or a VT that said what it +is, of 40x14 at least) gets the menu full screen: the items in a window +on a blue desk, the welcome at its top, the bar moved with the cursor +keys (or an item's first letter) and Enter, or an item's key in `[ ]` +starting it at once. The motd comes up in a box over it (one with +ANSI's escapes is shown as it is first, then "Press any key"). An item's +`help` is shown under the menu; the bar at the top has the user, the +rate and the time, the one at the foot the minutes left. Esc asks +before hanging up; F1 shows the keys. After a program (`mode = exec`, +`passwd`, `callers`) that comes back to the menu, "Press any key for the +menu" keeps what it wrote on the screen until the caller has read it. + +The login stays lines, as before: PPP dialers (AutoPPP) and AOL +clients read and answer it, and a terminal's answers are not known +until then. Others (VT52, a Commodore, a terminal that answers +nothing) get the menu of lines. + +```ini +[menu] +fullscreen = auto ; no: lines for every caller + +[terminal] +charset = auto ; utf8, cp437, dec or ascii + +[item bbs] +help = Messages, files and doors on the board +``` + +For its lines, modemmenu asks an ANSI terminal whether it shows UTF-8 +(an `é` written and the cursor's place asked); SyncTERM is code page +437. Without UTF-8 a VT or xterm gets its line-drawing set, others code +page 437. Only the cells that change are sent, and below 9600 bit/s the +desk is plain blue (cleared in a few bytes) instead of shaded: the +first screen is about 1 KB at 2400 bit/s, moving the bar a few hundred +bytes. + ## Who sees what diff --git a/docs/reference.md b/docs/reference.md index 20f9a93..f72b0f2 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -38,15 +38,15 @@ The login: the log, the backends, what the caller sees, the time limits. |---|---|---| | `log` | `syslog` | syslog (auth facility), a file, or none. Never the line: stderr is the caller's screen. | | `auth` | `file` | the backends, asked in this order: file, ldap, sql, system (PAM). A backend that does not know the user passes to the next; a wrong password stops there. | -| `banner` | `\r\nWelcome to D-Modem.\r\n` | the backends, asked in this order: file, ldap, sql, system (PAM). A backend that does not know the user passes to the next; a wrong password stops there. | -| `banner_file` | `/etc/asterisk/modemmenu.banner` | the backends, asked in this order: file, ldap, sql, system (PAM). A backend that does not know the user passes to the next; a wrong password stops there. | -| `login_prompt` | `\r\nlogin: ` | the backends, asked in this order: file, ldap, sql, system (PAM). A backend that does not know the user passes to the next; a wrong password stops there. | -| `password_prompt` | `Password: ` | the backends, asked in this order: file, ldap, sql, system (PAM). A backend that does not know the user passes to the next; a wrong password stops there. | +| `banner` | `\r\nWelcome to D-Modem.\r\n` | what the caller sees first (or a file of it, which may have ANSI's colors), then the prompts of the login. | +| `banner_file` | `/etc/asterisk/modemmenu.banner` | what the caller sees first (or a file of it, which may have ANSI's colors), then the prompts of the login. | +| `login_prompt` | `\r\nlogin: ` | what the caller sees first (or a file of it, which may have ANSI's colors), then the prompts of the login. | +| `password_prompt` | `Password: ` | what the caller sees first (or a file of it, which may have ANSI's colors), then the prompts of the login. | | `connect_message` | `\r\nConnected at {speed}.\r\n` | after the banner, before the login prompt: the connect rate ({speed}: "33600 bit/s", or "50666 bit/s down, 31200 bit/s up"); left out when it is not known; "" for none. | -| `welcome` | `\r\nHello {user}, on {date} at {time}.\r\n` | after the banner, before the login prompt: the connect rate ({speed}: "33600 bit/s", or "50666 bit/s down, 31200 bit/s up"); left out when it is not known; "" for none. | +| `welcome` | `\r\nHello {user}, on {date} at {time}.\r\n` | after a login, before the motd ({placeholders}; on the full-screen menu, at the top of its window) | | `motd` | `\r\nThe line is down for maintenance on Sunday.\r\n` | the message of the day, after the welcome and before the menu: motd, then the file (read at each login), {placeholders} in both. | | `motd_file` | `/etc/motd` | the message of the day, after the welcome and before the menu: motd, then the file (read at each login), {placeholders} in both. | -| `goodbye` | `\r\nGoodbye.\r\n` | the message of the day, after the welcome and before the menu: motd, then the file (read at each login), {placeholders} in both. | +| `goodbye` | `\r\nGoodbye.\r\n` | at the end of the call, before the line is hung up. | | `password_echo` | `*` | the echo of each password character, or none. | | `attempts` | `3` | logins before the call is hung up, seconds after a wrong one. | | `fail_delay` | `3` | logins before the call is hung up, seconds after a wrong one. | @@ -56,7 +56,7 @@ The login: the log, the backends, what the caller sees, the time limits. | `max_input` | `64` | the longest line the caller may type at a prompt: a user name or a menu choice (a password twice as long) | | `time_limit` | `0` | time limits (0: none), from the login: a call, and a user's calls in a day (kept in state_dir/usage). A selection is hung up at the limit. {time_left} (whole minutes) shows what is left, e.g. in [menu] prompt. | | `daily_limit` | `0` | time limits (0: none), from the login: a call, and a user's calls in a day (kept in state_dir/usage). A selection is hung up at the limit. {time_left} (whole minutes) shows what is left, e.g. in [menu] prompt. | -| `state_dir` | `/var/lib/modemmenu` | time limits (0: none), from the login: a call, and a user's calls in a day (kept in state_dir/usage). A selection is hung up at the limit. {time_left} (whole minutes) shows what is left, e.g. in [menu] prompt. | +| `state_dir` | `/var/lib/modemmenu` | where the usage, the last callers and the nodes are kept. | | `time_warning` | `5m 1m` | these long before the limit, the message is written on the line in the middle of a running selection (not one with time_warning = no), once for each: 5m 1m warns at 5 minutes and at 1 minute left; 0: none. {time_left} is in minutes, {seconds_left} in seconds; \a beeps. | | `time_warning_message` | `\r\n\r\n*** {time_left} minute(s) left in this call ***\r\n` | these long before the limit, the message is written on the line in the middle of a running selection (not one with time_warning = no), once for each: 5m 1m warns at 5 minutes and at 1 minute left; 0: none. {time_left} is in minutes, {seconds_left} in seconds; \a beeps. | @@ -69,6 +69,7 @@ the caller's terminal, asked before the banner: ANSI (and its size), VT52, what | `detect` | `yes` | See the section above. | | `petscii` | `auto` | auto: watch the login for a Commodore's keys; yes: every caller is one. | | `timeout` | `2s` | for each question a terminal may not answer. | +| `charset` | `auto` | the full-screen menu's lines and shading: auto (UTF-8 when the terminal shows it, code page 437 for SyncTERM and other BBS terminals, a VT's line-drawing set for a DEC terminal), utf8, cp437, dec or ascii. | ## `[groups]` @@ -263,6 +264,7 @@ The menu: its title, prompt, items and the words that hang up. | `items` | `bbs ppp shell passwd` | the items in order (default: every [item ...] in file order) | | `hangup` | `G goodbye bye logoff` | the caller types a choice and Enter: an item's key, its name or one of its aliases, in any case (P, p, ppp). ? shows the menu again. What hangs up (the first one is shown in the menu): | | `hotkeys` | `no` | yes: one key selects at once, without Enter (one-character keys only) | +| `fullscreen` | `auto` | auto: the menu full screen for a caller whose terminal can have it (ANSI, or a VT that said what it is, 40x14 at least): the items in a window on a blue desk, chosen with the cursor keys or an item's key, the welcome at its top and the motd in a box; others get the menu of lines. no: lines for every caller. The login is always lines (dialers and AOL clients read and answer it) | ## `[item NAME]` @@ -272,10 +274,11 @@ There may be any number of these, each with its own NAME. | key | sample | what it does | |---|---|---| -| `key` | `B` | its key in the menu, one or more characters (default: its name's first letter) Also: `P`, `PW`, `S`, `M`, `L`, `LC`, `U`, `K`, `W`, `N`, `C`, `GO`. | +| `key` | `B` | its key in the menu, one or more characters (default: its name's first letter) Also: `P`, `PW`, `S`, `M`, `L`, `LC`, `Z`, `U`, `K`, `W`, `N`, `C`, `GO`. | | `aliases` | `...` | more words that select it (its name always does) Also: `dialup net`. | -| `label` | `...` | its line in the menu. Also: `Bulletin board`, `PPP (dial-up networking)`, `Change your password`, `Shell`, `More services`, `The BBS`, `Legend of the Red Dragon`, `Last callers`, `Upload a file (ZMODEM)`, `Kermit file server`, `The web (lynx)`, `Mail`, `Usenet news`, `Chat (IRC)`, `Gopherspace`. | -| `mode` | `exec` | a program on the line: command (by /bin/sh -c); `passwd`: built in: the caller changes their password (min_password = 8 characters); `callers`: built in: the last callers (count = 10), the time, user and rate of each login (state_dir/callers, the latest 200; kept only with such an item); `tcp`: a TCP server, raw: host, port; send is sent first; `unix`: a server on a unix socket, raw: path; send too (tcp, telnet and rlogin take a path as well, for their protocol on a unix socket); `telnet`: a telnet server: host, port (23), terminal (ANSI); `rlogin`: an rlogin server: host, port (513), and rlogin_client_user ({password}), rlogin_server_user ({user}), rlogin_terminal (ansi/{baud}) Also: `rlogin`, `tcp`, `passwd`, `unix`, `callers`. | +| `label` | `...` | its line in the menu. Also: `Bulletin board`, `PPP (dial-up networking)`, `Change your password`, `Shell`, `More services`, `The BBS`, `Legend of the Red Dragon`, `Last callers`, `Setup`, `Upload a file (ZMODEM)`, `Kermit file server`, `The web (lynx)`, `Mail`, `Usenet news`, `Chat (IRC)`, `Gopherspace`. | +| `help` | `...` | a line of what it is, under the full-screen menu. Also: `Messages, files and doors on the board`. | +| `mode` | `exec` | a program on the line: command (by /bin/sh -c); `passwd`: built in: the caller changes their password (min_password = 8 characters); `callers`: built in: the last callers (count = 10), the time, user and rate of each login (state_dir/callers, the latest 200; kept only with such an item); `setup`: built in: the sysop's setup, modemmenu.conf on full screens (as modemmenu -setup); needs users or groups; `tcp`: a TCP server, raw: host, port; send is sent first; `unix`: a server on a unix socket, raw: path; send too (tcp, telnet and rlogin take a path as well, for their protocol on a unix socket); `telnet`: a telnet server: host, port (23), terminal (ANSI); `rlogin`: an rlogin server: host, port (513), and rlogin_client_user ({password}), rlogin_server_user ({user}), rlogin_terminal (ansi/{baud}) Also: `rlogin`, `tcp`, `passwd`, `unix`, `callers`, `setup`. | | `run_as` | `{user}` | exec as this system user (modemmenu must be root) | | `tty` | `raw` | the line as app_dmodem has it; sane: cooked, echo, CR to LF, for programs that want a terminal. Also: `sane`. | | `pass_password` | `yes` | the password in {password} and MODEM_PASSWORD. Also: `no`. | diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000..0512900 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,74 @@ +# The sysop's setup + +`modemmenu -setup` changes `modemmenu.conf` on a full screen, as the BBS +setup programs of the 1990s did: a menu bar of pull-down menus, a blue +desk, a window for each section cascading over the one it was opened +from, the help of the line under the bar in a box below, and the keys on +a bar at the foot. + +![The full-screen menu and the setup](https://raw.githubusercontent.com/D-Modem/menu/main/.github/screens.png) + +``` +modemmenu -setup -config /etc/asterisk/modemmenu.conf +``` + +- The first window has every section, under Login, Users, Menu, + Dial-up and Server (the same as the bar's menus). Enter opens one: its + keys and their values, `(not set)` and the sample's value for those the + file leaves out. +- **Enter** changes the value under the bar: typed in a box (cursor keys, + Home, End, Del, ^U), or picked from a list for a key the sample gives + the choices of (`yes`/`no`, `petscii = auto|yes|no`...). **Space** steps + through the choices; **Del** takes the key out (its default again). An + empty box is "not set"; type `""` for an empty value. +- `[item NAME]` and `[number NAME]` are lists: add one, open it, rename + it or delete it. An item renamed or deleted is renamed or taken out of + `[menu] items` too. `[groups]` takes groups of the sysop's own names. +- **F1** shows the whole help of the line, **/** (or F12, or Alt and a + letter) the menus, **Esc** goes back a window, **F10** saves, **F9** + quits (asking to save what changed). `^L` draws the screen again. +- The help of every key is the sample's ([reference](reference.md)), built + into the program. + +Saving checks the file first, as `modemmenu -check` does; a file that +does not check is saved only when asked again ("Save anyway"). The file +keeps its comments, blank lines and order: a changed key is changed on +its own line (a comment after it kept), a new key goes after the last of +its section, a new section at the end. The file is written beside the +old one and renamed over it, its mode and owner kept. Each call reads +the file anew, so the next call has the changes; `[server]` is read when +`modemmenu -serve` starts. + +With no file yet, the setup starts from the sample and saves it there. + +## On the line + +A sysop who calls in can have the setup on the menu, as an item of +`mode = setup`: + +```ini +[item setup] +key = Z +label = Setup +mode = setup +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 +written as the user modemmenu runs as, which must be able to write it. + +## Terminals + +The screens are drawn in UTF-8 where the terminal shows it, in code page +437 for SyncTERM and the ANSI terminals of BBSs, in a VT's line-drawing +set (`ESC ( 0`) for a VT or xterm without UTF-8, and in ASCII when told +(`[terminal] charset`). A DEC terminal itself (no colors) gets bold and +reverse for the colors. Only the cells that change are sent: moving +the bar costs a hundred bytes or two. At the console, `-setup` takes +UTF-8 from the locale (`LANG`), else the VT set for `TERM=vt*` or +`xterm*`, code page 437 for `TERM=linux` or `ansi`. + +**See also:** [Configuration](configuration.md#the-full-screen-menu) diff --git a/internal/confdoc/confdoc.go b/internal/confdoc/confdoc.go new file mode 100644 index 0000000..98a67a9 --- /dev/null +++ b/internal/confdoc/confdoc.go @@ -0,0 +1,199 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +// Package confdoc reads what samples/modemmenu.conf.sample says of each +// section and key in its comments: the help, the sample's value and the +// other values it shows. tools/confref makes the configuration reference +// of it, and modemmenu -setup the help of its screens. +package confdoc + +import ( + "regexp" + "strings" +) + +// Kind is a kind of section: [general], [item NAME] ... +type Kind struct { + Name string + Named bool + Help string + Free string // what its keys are, when they are the sysop's own names + Keys []*Key + ByKey map[string]*Key +} + +// Key is one key of a kind of section. +type Key struct { + Name string + Help string + Value string // the sample's (or its first commented example's) + Examples []string // the other values the sample shows +} + +var ( + headerRE = regexp.MustCompile(`^;?\[([a-z_]+)(?:\s+(\S+))?\]$`) + keyRE = regexp.MustCompile(`^(;?)([a-z][a-z0-9_]*)\s*=\s*(.*)$`) + docKeyRE = regexp.MustCompile(`^;\s{2,4}([a-z][a-z0-9_]*) = (\S+)\s*(.*)$`) + docMoreRE = regexp.MustCompile(`^;\s{5,}(.*)$`) + // a value of a described key and what it does ("passwd built in") + docValueRE = regexp.MustCompile(`^([a-z0-9_]+)\s{2,}(\S.*)$`) +) + +// Unquote is a sample value as the sysop writes it: without its quotes. +func Unquote(v string) string { + if i := strings.Index(v, " ; "); i >= 0 && !strings.HasPrefix(v, `"`) { + v = v[:i] + } + v = strings.TrimSpace(v) + if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' { + return v[1 : len(v)-1] + } + return v +} + +// kinds' help where the sample has none before them, or has the first +// example's +var kindHelp = map[string]string{ + "general": "The login: the log, the backends, what the caller sees, the time limits.", + "menu": "The menu: its title, prompt, items and the words that hang up.", + "item": "A menu item: a program on the line, a server to connect the caller to, or a built-in (see the item keys below).", +} + +// kinds whose keys are the sysop's own names, and what such a key is +var freeKinds = map[string]string{ + "groups": "a group: its name, then its members (`sysops = alice bob`)", +} + +func Parse(sample string) []*Kind { + var kinds []*Kind + byName := map[string]*Kind{} + get := func(name string) *Kind { + if k := byName[name]; k != nil { + return k + } + k := &Kind{Name: name, ByKey: map[string]*Key{}} + byName[name] = k + kinds = append(kinds, k) + return k + } + var cur *Kind + var prose []string + keyOf := func(k *Kind, name string) *Key { + if x := k.ByKey[name]; x != nil { + return x + } + x := &Key{Name: name} + k.ByKey[name] = x + k.Keys = append(k.Keys, x) + return x + } + var doc *Key // the key an indented doc line describes + var pending []*Key // described keys not yet given to a kind + adopt := func(k *Kind) { + for _, d := range pending { + x := keyOf(k, d.Name) + if x.Value == "" { + x.Value = d.Value + } + if x.Help == "" { + x.Help = d.Help + } + } + pending = nil + } + docBlock := false + inHeader := true // the file's own comment before the first section + afterKey := false // a comment now starts the help of the keys after it + for _, raw := range strings.Split(sample, "\n") { + line := strings.TrimRight(raw, " \t\r") + if line == "" { + prose, doc, docBlock, afterKey = nil, nil, false, false + continue + } + if m := headerRE.FindStringSubmatch(line); m != nil { + inHeader = false + cur = get(m[1]) + adopt(cur) + if m[2] != "" { + cur.Named = true + } + if cur.Help == "" && len(prose) > 0 { + cur.Help = strings.Join(prose, " ") + } + prose, doc, afterKey = nil, nil, false + continue + } + if inHeader { + continue + } + // an indented description of keys ("; key = B its key"): of + // the kind of section whose keys or header come next + if m := docKeyRE.FindStringSubmatch(line); m != nil && (docBlock || len(prose) > 0) { + docBlock = true + doc = &Key{Name: m[1], Value: Unquote(m[2]), Help: m[3]} + pending = append(pending, doc) + continue + } + if m := docMoreRE.FindStringSubmatch(line); m != nil && doc != nil { + more := m[1] + if v := docValueRE.FindStringSubmatch(more); v != nil { + more = "; `" + v[1] + "`: " + v[2] + } + sep := " " + if strings.HasPrefix(more, ";") { + sep = "" + } + doc.Help = strings.TrimSpace(doc.Help + sep + more) + continue + } + if m := keyRE.FindStringSubmatch(line); m != nil && cur != nil { + adopt(cur) + x := keyOf(cur, m[2]) + v := Unquote(m[3]) + if x.Help == "" && len(prose) > 0 { + x.Help = strings.Join(prose, " ") + } + if x.Value == "" && m[1] == "" { + x.Value = v + } else if v != "" && v != x.Value { + seen := false + for _, e := range x.Examples { + seen = seen || e == v + } + if !seen { + x.Examples = append(x.Examples, v) + } + } + afterKey = true + continue + } + if strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") { + t := strings.TrimSpace(strings.TrimLeft(line, ";#")) + if doc != nil || docBlock || afterKey { + // prose after a block of key descriptions starts afresh + prose, doc, docBlock, afterKey = nil, nil, false, false + } + prose = append(prose, t) + } + } + if cur != nil { + adopt(cur) + } + for _, k := range kinds { + if h, ok := kindHelp[k.Name]; ok { + k.Help = h + } + if f, ok := freeKinds[k.Name]; ok { + k.Free, k.Keys = f, nil + } + } + // a key with no value of its own: its first example is it + for _, k := range kinds { + for _, x := range k.Keys { + if x.Value == "" && len(x.Examples) > 0 { + x.Value, x.Examples = x.Examples[0], x.Examples[1:] + } + } + } + return kinds +} diff --git a/internal/confedit/confedit.go b/internal/confedit/confedit.go new file mode 100644 index 0000000..c2206e6 --- /dev/null +++ b/internal/confedit/confedit.go @@ -0,0 +1,301 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +// Package confedit changes modemmenu.conf as a sysop would in an editor: +// a key's value on its own line (its comment after it kept), a new key +// after the last of its section, a new section at the end. The comments, +// the blank lines and the order of everything else stay as they are. +package confedit + +import ( + "strings" + + "github.com/D-Modem/menu/internal/config" +) + +// Doc is a configuration file's lines. +type Doc struct { + lines []string +} + +// Parse is a Doc of a file's text. +func Parse(text string) *Doc { + text = strings.ReplaceAll(text, "\r\n", "\n") + text = strings.TrimSuffix(text, "\n") + d := &Doc{} + if text != "" { + d.lines = strings.Split(text, "\n") + } + return d +} + +// String is the file's text. +func (d *Doc) String() string { + if len(d.lines) == 0 { + return "" + } + return strings.Join(d.lines, "\n") + "\n" +} + +// header is the section a line opens ("" and false: none). +func header(line string) (string, bool) { + t := strings.TrimSpace(line) + if t == "" || t[0] != '[' { + return "", false + } + end := strings.IndexByte(t, ']') + if end < 0 { + return "", false + } + return strings.Join(strings.Fields(t[1:end]), " "), true +} + +// keyLine is a line's key and its raw value (after =), ok false for a +// comment, a blank line or a header. +func keyLine(line string) (key, raw string, ok bool) { + t := strings.TrimSpace(line) + if t == "" || t[0] == ';' || t[0] == '#' || t[0] == '[' { + return "", "", false + } + eq := strings.IndexByte(line, '=') + if eq < 0 { + return "", "", false + } + return strings.ToLower(strings.TrimSpace(line[:eq])), line[eq+1:], true +} + +// value is a raw value as the configuration reads it. +func value(raw string) string { + v := strings.TrimSpace(config.StripComment(raw)) + if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' { + v = v[1 : len(v)-1] + } + return v +} + +// span is where a section is: its header's line, and the line after its +// last key (or after the header). +type span struct{ header, lastKey int } + +// spans are each [name] of the file, in order (a name may come more than +// once: the configuration takes them as one). +func (d *Doc) spans(name string) []span { + var out []span + cur := "" + in := false + for i, l := range d.lines { + if h, ok := header(l); ok { + cur, in = h, h == name + if in { + out = append(out, span{i, i + 1}) + } + continue + } + if _, _, ok := keyLine(l); ok && in && cur == name { + out[len(out)-1].lastKey = i + 1 + } + } + return out +} + +// Sections are the sections' names, in the order they first come. +func (d *Doc) Sections() []string { + var out []string + seen := map[string]bool{} + for _, l := range d.lines { + if h, ok := header(l); ok && !seen[h] { + seen[h] = true + out = append(out, h) + } + } + return out +} + +// Has is whether the file has [name]. +func (d *Doc) Has(name string) bool { return len(d.spans(name)) > 0 } + +// find is the line of a section's key (its last, which is the one that +// counts), -1 none. +func (d *Doc) find(section, key string) int { + at := -1 + for _, sp := range d.spans(section) { + for i := sp.header + 1; i < len(d.lines); i++ { + if _, ok := header(d.lines[i]); ok { + break + } + if k, _, ok := keyLine(d.lines[i]); ok && k == key { + at = i + } + } + } + return at +} + +// Keys are a section's keys, in file order, each once. +func (d *Doc) Keys(section string) []string { + var out []string + seen := map[string]bool{} + for _, sp := range d.spans(section) { + for i := sp.header + 1; i < len(d.lines); i++ { + if _, ok := header(d.lines[i]); ok { + break + } + if k, _, ok := keyLine(d.lines[i]); ok && !seen[k] { + seen[k] = true + out = append(out, k) + } + } + } + return out +} + +// Get is a key's value, and whether it is set. +func (d *Doc) Get(section, key string) (string, bool) { + i := d.find(section, key) + if i < 0 { + return "", false + } + _, raw, _ := keyLine(d.lines[i]) + return value(raw), true +} + +// Quote is v as it is written after "key = ": in quotes when it would +// not read back the same without. +func Quote(v string) string { + if v == "" { + return `""` + } + if strings.TrimSpace(v) != v || value(v) != v { + return `"` + v + `"` + } + return v +} + +// commentAt is where a raw value's comment begins (len(raw): none). +func commentAt(raw string) int { + quoted := false + for i := 0; i < len(raw); i++ { + c := raw[i] + switch { + case c == '\\' && i+1 < len(raw) && (raw[i+1] == ';' || raw[i+1] == '#'): + i++ + case c == '"': + quoted = !quoted + case (c == ';' || c == '#') && !quoted && (i == 0 || raw[i-1] == ' ' || raw[i-1] == '\t'): + // the spaces before the comment go with it + j := i + for j > 0 && (raw[j-1] == ' ' || raw[j-1] == '\t') { + j-- + } + return j + } + } + return len(raw) +} + +// Set gives a key a value: on its line if it has one (its comment kept, +// and its quotes if it had them), else on a new line after the section's +// last key (the section added at the end if there is none). +func (d *Doc) Set(section, key, v string) { + q := Quote(v) + if i := d.find(section, key); i >= 0 { + l := d.lines[i] + eq := strings.IndexByte(l, '=') + raw := l[eq+1:] + c := commentAt(raw) + old := strings.TrimSpace(raw[:c]) + if strings.HasPrefix(old, `"`) && !strings.HasPrefix(q, `"`) { + q = `"` + v + `"` + } + lead := " " + if len(raw) > 0 && raw[0] != ' ' && raw[0] != '\t' { + lead = "" + } + d.lines[i] = l[:eq+1] + lead + q + raw[c:] + return + } + sps := d.spans(section) + if len(sps) == 0 { + d.AddSection(section) + sps = d.spans(section) + } + at := sps[len(sps)-1].lastKey + d.insert(at, key+" = "+q) +} + +// Unset takes a key out (every line of it): its default again. +func (d *Doc) Unset(section, key string) { + for { + i := d.find(section, key) + if i < 0 { + return + } + d.lines = append(d.lines[:i], d.lines[i+1:]...) + } +} + +// AddSection puts [name] at the end, after a blank line. +func (d *Doc) AddSection(name string) { + if d.Has(name) { + return + } + if n := len(d.lines); n > 0 && strings.TrimSpace(d.lines[n-1]) != "" { + d.lines = append(d.lines, "") + } + d.lines = append(d.lines, "["+name+"]") +} + +// RemoveSection takes out [name] and its keys, and the comments among +// them; the comment just before the next section stays with that one. +func (d *Doc) RemoveSection(name string) { + for { + sps := d.spans(name) + if len(sps) == 0 { + return + } + sp := sps[len(sps)-1] + end := len(d.lines) + for i := sp.header + 1; i < len(d.lines); i++ { + if _, ok := header(d.lines[i]); ok { + end = i + break + } + } + // the next section's own comment and the blank line before it + if end < len(d.lines) { + for end > sp.header+1 && isComment(d.lines[end-1]) { + end-- + } + } + for end > sp.header+1 && strings.TrimSpace(d.lines[end-1]) == "" { + end-- + } + start := sp.header + // its own comment, just above it + for start > 0 && isComment(d.lines[start-1]) { + start-- + } + if start > 0 && strings.TrimSpace(d.lines[start-1]) == "" && (end >= len(d.lines) || strings.TrimSpace(d.lines[end]) == "") { + start-- + } + d.lines = append(d.lines[:start], d.lines[end:]...) + } +} + +// RenameSection gives [old] (each of its headers) a new name. +func (d *Doc) RenameSection(old, name string) { + for _, sp := range d.spans(old) { + d.lines[sp.header] = "[" + name + "]" + } +} + +func isComment(l string) bool { + t := strings.TrimSpace(l) + return t != "" && (t[0] == ';' || t[0] == '#') +} + +func (d *Doc) insert(at int, line string) { + d.lines = append(d.lines, "") + copy(d.lines[at+1:], d.lines[at:]) + d.lines[at] = line +} diff --git a/internal/confedit/confedit_test.go b/internal/confedit/confedit_test.go new file mode 100644 index 0000000..dde68ce --- /dev/null +++ b/internal/confedit/confedit_test.go @@ -0,0 +1,134 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +package confedit + +import ( + "os" + "path/filepath" + "testing" + + "github.com/D-Modem/menu/internal/config" +) + +const sample = `; the file's comment + +[general] +; the log +log = syslog ; or a file +banner = "\r\nHi\r\n" +attempts=3 + +; the menu +[menu] +title = Main + +; a BBS +[item bbs] +key = B +label = Board + +; the shell +[item shell] +key = S +` + +// TestSet: values changed in place with their comments and quotes, new +// keys after their section's last, new sections at the end +func TestSet(t *testing.T) { + d := Parse(sample) + d.Set("general", "log", "/var/log/modemmenu.log") + d.Set("general", "banner", "Hello") + d.Set("general", "attempts", "5") + d.Set("general", "motd", " spaced ") + d.Set("menu", "hotkeys", "yes") + d.Set("aol", "enabled", "yes") + d.Set("item bbs", "help", "a; b") + want := `; the file's comment + +[general] +; the log +log = /var/log/modemmenu.log ; or a file +banner = "Hello" +attempts=5 +motd = " spaced " + +; the menu +[menu] +title = Main +hotkeys = yes + +; a BBS +[item bbs] +key = B +label = Board +help = a; b + +; the shell +[item shell] +key = S + +[aol] +enabled = yes +` + if got := d.String(); got != want { + t.Errorf("got\n%s\nwant\n%s", got, want) + } + // and the configuration reads what was set + p := filepath.Join(t.TempDir(), "c.conf") + os.WriteFile(p, []byte(d.String()), 0o600) + c, err := config.Parse(p) + if err != nil { + t.Fatal(err) + } + for _, kv := range [][3]string{{"general", "log", "/var/log/modemmenu.log"}, {"general", "motd", " spaced "}, + {"general", "banner", "Hello"}, {"item bbs", "help", "a; b"}, {"aol", "enabled", "yes"}} { + if v := c.Section(kv[0]).Get(kv[1], "?"); v != kv[2] { + t.Errorf("[%s] %s = %q, want %q", kv[0], kv[1], v, kv[2]) + } + if v, _ := d.Get(kv[0], kv[1]); v != kv[2] { + t.Errorf("Get [%s] %s = %q, want %q", kv[0], kv[1], v, kv[2]) + } + } + for _, v := range []string{"a ; b", "x # y", `"q"`, ""} { + d.Set("general", "motd", v) + os.WriteFile(p, []byte(d.String()), 0o600) + c, _ := config.Parse(p) + if got := c.Section("general").Get("motd", "?"); got != v { + t.Errorf("motd %q read back as %q", v, got) + } + } +} + +// TestRemove: a key and a section out, the next one's comment kept +func TestRemove(t *testing.T) { + d := Parse(sample) + d.Unset("general", "attempts") + d.RemoveSection("item bbs") + d.RenameSection("item shell", "item sh") + want := `; the file's comment + +[general] +; the log +log = syslog ; or a file +banner = "\r\nHi\r\n" + +; the menu +[menu] +title = Main + +; the shell +[item sh] +key = S +` + if got := d.String(); got != want { + t.Errorf("got\n%s\nwant\n%s", got, want) + } + d.RemoveSection("item sh") + if got := d.String(); got != "; the file's comment\n\n[general]\n; the log\nlog = syslog ; or a file\nbanner = \"\\r\\nHi\\r\\n\"\n\n; the menu\n[menu]\ntitle = Main\n" { + t.Errorf("got\n%s", got) + } + if s := d.Sections(); len(s) != 2 || s[1] != "menu" { + t.Errorf("sections %q", s) + } +} diff --git a/internal/term/detect.go b/internal/term/detect.go index b246ed2..c7b559d 100644 --- a/internal/term/detect.go +++ b/internal/term/detect.go @@ -365,3 +365,30 @@ func (p *Prober) Identify(timeout time.Duration) (VTInfo, error) { p.keep(got) return in, nil } + +// UTF8: whether the terminal shows UTF-8: é written as its two bytes, +// and where the cursor went (one cell on, or two for a terminal of 8-bit +// characters), wiped after. ok is false when it did not answer. +func (p *Prober) UTF8(timeout time.Duration) (utf8, ok bool, err error) { + if err := p.T.WriteRaw("\r\xc3\xa9\x1b[6n"); err != nil { + return false, false, err + } + got, err := p.collect(timeout, 64, cprPattern.Match) + if err != nil { + return false, false, err + } + m := cprPattern.FindSubmatch(got) + p.keep(got) + if err := p.T.WriteRaw("\r \r"); err != nil { + return false, false, err + } + if m == nil { + return false, false, nil + } + col, _ := strconv.Atoi(string(m[2])) + return col == 2, true, nil +} + +// CTerm: whether it said it is SyncTERM (whose characters are code page +// 437's) +func (in VTInfo) CTerm() bool { return in.cterm != "" } diff --git a/internal/tui/charset.go b/internal/tui/charset.go new file mode 100644 index 0000000..422ab30 --- /dev/null +++ b/internal/tui/charset.go @@ -0,0 +1,149 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +package tui + +import ( + "strings" + "unicode/utf8" +) + +// Charset is what the terminal shows the screen's lines and shading in. +type Charset int + +const ( + UTF8 Charset = iota // as they are + CP437 // the IBM PC's code page: what BBS terminals have + DEC // a VT's line-drawing set (ESC ( 0), the rest ASCII + ASCII // + - | and letters for the arrows +) + +// ParseCharset is a charset by its name: utf8, cp437, dec, ascii. +func ParseCharset(s string) (Charset, bool) { + switch strings.ToLower(strings.ReplaceAll(s, "-", "")) { + case "utf8": + return UTF8, true + case "cp437", "ibm", "pc", "437": + return CP437, true + case "dec", "vt", "vt100": + return DEC, true + case "ascii": + return ASCII, true + } + return UTF8, false +} + +func (c Charset) String() string { + return [...]string{"utf8", "cp437", "dec", "ascii"}[c] +} + +// cp437High is code page 437 from 0x80 to 0xFF. +const cp437High = "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»" + + "░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀" + + "αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ " + +var cp437 = func() map[rune]byte { + m := map[rune]byte{} + i := 0x80 + for _, r := range cp437High { + m[r] = byte(i) + i++ + } + // its glyphs in the control range (► ▲ ▼...) are not sent: some + // terminals take them as controls and the columns after slip + m['►'], m['↕'], m['…'] = 0xaf, 0xf0, 0xaf + return m +}() + +// what the PC's code page has no place for that the screens draw +var plain = map[rune]string{ + '◄': "<", '↑': "^", '↓': "v", '→': ">", '←': "<", '…': "~", '►': ">", + '↕': "|", '▲': "^", '▼': "v", '•': "*", '✓': "*", +} + +// the VT's line-drawing characters (in ESC ( 0), double lines as single +var decGraphics = map[rune]byte{ + '┘': 'j', '┐': 'k', '┌': 'l', '└': 'm', '┼': 'n', '─': 'q', '├': 't', '┤': 'u', '┴': 'v', '┬': 'w', '│': 'x', + '╝': 'j', '╗': 'k', '╔': 'l', '╚': 'm', '═': 'q', '║': 'x', '╟': 't', '╢': 'u', + '░': 'a', '▒': 'a', '▓': 'a', '·': '~', '°': 'f', '±': 'g', '≤': 'y', '≥': 'z', '£': '}', +} + +// and as ASCII +var asciiLines = map[rune]byte{ + '┘': '+', '┐': '+', '┌': '+', '└': '+', '┼': '+', '─': '-', '├': '+', '┤': '+', '┴': '+', '┬': '+', '│': '|', + '╝': '+', '╗': '+', '╔': '+', '╚': '+', '═': '=', '║': '|', '╟': '+', '╢': '+', + '░': ' ', '▒': ' ', '▓': '#', '█': '#', '·': '.', '■': '#', +} + +// Encode is s (text and escape sequences) as a terminal of cs takes it. +func Encode(s string, cs Charset) []byte { + if cs == UTF8 { + return []byte(s) + } + out := make([]byte, 0, len(s)) + graphics := false // DEC: ESC ( 0 is in + for i := 0; i < len(s); { + if s[i] == 0x1b { + // an escape sequence, as it is: ESC [ ... final, or ESC and one + j := i + 1 + if j < len(s) && s[j] == '[' { + j++ + for j < len(s) && (s[j] < 0x40 || s[j] > 0x7e) { + j++ + } + } + j = min(j+1, len(s)) + out = append(out, s[i:j]...) + i = j + continue + } + r, n := utf8.DecodeRuneInString(s[i:]) + i += n + if r < 0x80 { + if graphics { + out, graphics = append(out, "\x1b(B"...), false + } + out = append(out, byte(r)) + continue + } + switch cs { + case CP437: + if b, ok := cp437[r]; ok { + out = append(out, b) + } else if p, ok := plain[r]; ok { + out = append(out, p...) + } else { + out = append(out, '?') + } + case DEC: + if b, ok := decGraphics[r]; ok { + if !graphics { + out, graphics = append(out, "\x1b(0"...), true + } + out = append(out, b) + continue + } + if graphics { + out, graphics = append(out, "\x1b(B"...), false + } + out = append(out, ascii(r)...) + default: + out = append(out, ascii(r)...) + } + } + if graphics { + out = append(out, "\x1b(B"...) + } + return out +} + +// ascii is r in ASCII, as near as it goes. +func ascii(r rune) string { + if b, ok := asciiLines[r]; ok { + return string(b) + } + if p, ok := plain[r]; ok { + return p + } + return "?" +} diff --git a/internal/tui/desk.go b/internal/tui/desk.go new file mode 100644 index 0000000..f78edbf --- /dev/null +++ b/internal/tui/desk.go @@ -0,0 +1,370 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +package tui + +import ( + "strings" + "time" + "unicode" +) + +// Line is the terminal: one byte at a time, within a time, and bytes out. +type Line interface { + ReadOne(time.Duration) (byte, error) + WriteRaw(string) error +} + +// Desk is the full screen on a terminal: what it shows now, and how. +type Desk struct { + Line Line + Rows int + Cols int + Charset Charset + Mono bool // no colors: bright as bold, light bars reversed (a VT) + Name string // at the right of the menu bar + // the desk shaded (░), not plain blue: plain is cleared in a few + // bytes, for a slow line + 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 + Idle time.Duration + + on *Screen // what the terminal shows (nil: not known) + last *Screen // what was shown last + pending []byte + lastCR bool +} + +// Menu is a pull-down menu of the menu bar. +type Menu struct { + Label string + Items []MenuItem +} + +// MenuItem is a menu's line: an ID it gives back, or a heading line. +type MenuItem struct { + Label string + ID string + Heading bool +} + +// Hint is a key on the bar at the foot, and what it does. +type Hint struct{ Key, Label string } + +// OK is whether a terminal of rows and cols has room for the desk. +func OK(rows, cols int) bool { return rows >= 14 && cols >= 40 } + +// Show sends what changed between the terminal's screen and scr. +func (d *Desk) Show(scr *Screen) error { + old, out := d.on, "" + if old == nil || old.Cols != scr.Cols || old.Rows != scr.Rows { + // the screen cleared in the desk's colors: its blank cells need + // nothing more + out = "\x1b[0m" + sgr(Attr{LightGray, Blue}, d.Mono) + "\x1b[2J" + old = NewScreen(scr.Cols, scr.Rows) + old.Fill(0, 0, scr.Rows, scr.Cols, ' ', Attr{LightGray, Blue}) + } + out += scr.Render(old, d.Mono) + "\x1b[?25l" + d.on = scr.Clone() + d.last = d.on + return d.Line.WriteRaw(string(Encode(out, d.Charset))) +} + +// Redraw: the next Show sends the whole screen (the terminal's is not +// known: a program ran on it, or the caller asked with ^L). +func (d *Desk) Redraw() { d.on = nil } + +// Close takes the desk down: colors reset, the screen cleared, the cursor +// shown at the top. +func (d *Desk) Close() error { + d.on = nil + return d.Line.WriteRaw("\x1b[0m\x1b[2J\x1b[H\x1b[?25h") +} + +// Key is the next key within wait (< 0: Idle; ErrTimeout without one); alt: a letter +// or digit typed with Alt. ^L redraws the screen at the next Show and is +// given back too. +func (d *Desk) Key(wait time.Duration) (rune, bool, error) { + k, alt, err := d.readKey(wait) + if err == nil && k == 0x0c { + d.Redraw() + } + return k, alt, err +} + +// Blank is the bare desk: the shading, the menu bar (open: the menu shown +// pulled down, -1 none), the key bar's row empty. +func (d *Desk) Blank(open int) *Screen { + scr := NewScreen(d.Cols, d.Rows) + if d.Shade { + scr.Fill(1, 0, d.Rows-2, d.Cols, '░', Attr{LightGray, Blue}) + } else { + scr.Fill(1, 0, d.Rows-2, d.Cols, ' ', Attr{LightGray, Blue}) + } + bar := Attr{Black, LightGray} + scr.Fill(0, 0, 1, d.Cols, ' ', bar) + scr.Fill(d.Rows-1, 0, 1, d.Cols, ' ', bar) + bm := barMenus(d.Menus) + for i, m := range bm { + a, hot := bar, Attr{Red, LightGray} + if i == open { + a, hot = Attr{Black, Green}, Attr{Red, Green} + } + scr.Put(0, m.col-1, " "+m.label+" ", a) + if m.hot >= 0 { + scr.Put(0, m.col+m.hot, string([]rune(m.label)[m.hot]), hot) + } + } + end := 0 + if len(bm) > 0 { + l := bm[len(bm)-1] + end = l.col + Width(l.label) + 1 + } + if name := " " + d.Name + " "; d.Name != "" && end+Width(name)+2 < d.Cols { + if len(bm) == 0 { + scr.Put(0, 1, name, bar) + } else { + scr.Put(0, d.Cols-Width(name)-1, name, bar) + } + } + return scr +} + +// Status puts s at the right of the menu bar (over Name). +func (d *Desk) Status(scr *Screen, s string) { + if s == "" { + return + } + s = " " + Trunc(s, d.Cols/2) + " " + scr.Put(0, d.Cols-Width(s)-1, s, Attr{Black, LightGray}) +} + +// KeyBar draws the bar of keys at the foot, and status at its right. +func (d *Desk) KeyBar(scr *Screen, keys []Hint, status string) { + cols, row := d.Cols, d.Rows-1 + bar, hot := Attr{Black, LightGray}, Attr{Red, LightGray} + scr.Fill(row, 0, 1, cols, ' ', bar) + room := cols - 1 + if status != "" { + room -= min(Width(status)+3, cols/2) + } + x := 1 + for _, k := range keys { + n := Width(k.Key) + 1 + Width(k.Label) + if x+n > room { + break + } + scr.Put(row, x, k.Key, hot) + scr.Put(row, x+Width(k.Key)+1, k.Label, bar) + x += n + 2 + } + if status != "" { + st := Trunc(status, cols/2-2) + scr.Put(row, cols-Width(st)-2, st, Attr{Blue, LightGray}) + } +} + +// HasHelp is whether the screen has rows for the help box. +func (d *Desk) HasHelp() bool { return d.Rows >= 20 } + +// Bottom is the last row a window may use (above the help box, if there +// is one, and the window's shadow). +func (d *Desk) Bottom() int { + if d.HasHelp() { + return d.Rows - 7 + } + return d.Rows - 3 +} + +// HelpBox draws the box of help above the key bar: a title and text (two +// lines of it; more is cut). tone: 0 plain, 1 a warning. +func (d *Desk) HelpBox(scr *Screen, title, text string, warn bool) { + if !d.HasHelp() { + return + } + row, cols := d.Rows-5, d.Cols + scr.Box(row, 0, 4, cols, Single, Attr{LightCyan, Blue}, Attr{White, Blue}) + if title != "" { + scr.Title(row, 0, cols, title, Attr{Yellow, Blue}) + } + a := Attr{White, Blue} + if warn { + a = Attr{LightRed, Blue} + } + lines := Wrap(text, cols-4) + if len(lines) > 2 { + lines[1] = Trunc(lines[1], cols-14) + " (F1: more)" + lines = lines[:2] + } + for i, l := range lines { + scr.Put(row+1+i, 2, l, a) + } +} + +// barMenu is a menu's place on the bar. +type barMenu struct { + label string + col int // where its label begins + hot int // its hot key's rune in label (-1: none) + key rune +} + +// barMenus are the bar's menus placed, each with a hot key no other has. +func barMenus(menus []Menu) []barMenu { + var out []barMenu + used := map[rune]bool{} + col := 3 + for _, m := range menus { + b := barMenu{label: m.Label, col: col, hot: -1} + for i, r := range []rune(m.Label) { + u := unicode.ToUpper(r) + if unicode.IsLetter(r) && !used[u] { + b.hot, b.key, used[u] = i, u, true + break + } + } + out = append(out, b) + col += Width(m.Label) + 2 + } + return out +} + +// MenuKey is the menu whose hot key (with Alt) k is; -1 none. +func (d *Desk) MenuKey(k rune) int { + for i, m := range barMenus(d.Menus) { + if m.key == unicode.ToUpper(k) { + return i + } + } + return -1 +} + +// PullDown shows the menu bar's menu i pulled down over base (the screen +// with that menu open: base(i)), until an item is picked (its ID) or the +// menus are left (""). +func (d *Desk) PullDown(base func(open int) *Screen, i int) (string, error) { + bm := barMenus(d.Menus) + if i < 0 || i >= len(bm) { + return "", nil + } + first := func(m Menu) int { + for j, it := range m.Items { + if !it.Heading { + return j + } + } + return -1 + } + cur, top := first(d.Menus[i]), 0 + for { + scr := base(i) + m := d.Menus[i] + width := 14 + for _, it := range m.Items { + width = max(width, Width(it.Label)+4) + } + width = min(width, d.Cols-2) + col := max(min(bm[i].col-2, d.Cols-width-2), 0) + n := min(len(m.Items), d.Rows-6) + if cur >= 0 { + top = min(top, cur) + if cur >= top+n { + top = cur - n + 1 + } + } + box := Attr{Black, LightGray} + scr.Box(1, col, n+2, width, Single, box, box) + scr.Shadow(1, col, n+2, width) + for r := 0; r < n; r++ { + j := top + r + it := m.Items[j] + row := 2 + r + if it.Heading { + scr.Rule(row, col, width, Single, box) + if it.Label != "" { + scr.Put(row, col+2, " "+Trunc(it.Label, width-6)+" ", Attr{DarkGray, LightGray}) + } + continue + } + a := box + if j == cur { + a = Attr{Black, Green} + } + scr.PutW(row, col+1, width-2, " "+Trunc(it.Label, width-3), a) + } + if top > 0 { + scr.Put(2, col+width-1, "▲", box) + } + if top+n < len(m.Items) { + scr.Put(1+n, col+width-1, "▼", box) + } + d.KeyBar(scr, Hints("Enter", "Select", "Esc", "Close", "←→", "Menus", "↑↓", "Items"), "") + if err := d.Show(scr); err != nil { + return "", err + } + k, alt, err := d.Key(-1) + if err != nil { + return "", err + } + move := func(dir int) { + for j := cur + dir; j >= 0 && j < len(m.Items); j += dir { + if !m.Items[j].Heading { + cur = j + return + } + } + } + switch { + case alt: + if j := d.MenuKey(k); j >= 0 { + i, cur, top = j, first(d.Menus[j]), 0 + } + case k == KeyEsc || k == '/' || k == F(12) || k == F(10): + return "", nil + case k == KeyLeft: + i = (i + len(d.Menus) - 1) % len(d.Menus) + cur, top = first(d.Menus[i]), 0 + case k == KeyRight: + i = (i + 1) % len(d.Menus) + cur, top = first(d.Menus[i]), 0 + case k == KeyUp: + move(-1) + case k == KeyDown || k == KeyTab: + move(1) + case k == KeyHome: + cur = -1 + move(1) + case k == KeyEnd: + cur = len(m.Items) + move(-1) + case k == KeyEnter || k == ' ': + if cur >= 0 { + return m.Items[cur].ID, nil + } + default: + // a letter: the next item beginning with it + for o := 1; o <= len(m.Items); o++ { + j := (cur + o + len(m.Items)) % len(m.Items) + it := m.Items[j] + if !it.Heading && strings.HasPrefix(strings.ToUpper(it.Label), string(unicode.ToUpper(k))) { + cur = j + break + } + } + } + } +} + +// Hints is keys and what they do, in pairs: Hints("Enter", "Select", ...). +func Hints(kv ...string) []Hint { + var out []Hint + for i := 0; i+1 < len(kv); i += 2 { + out = append(out, Hint{Key: kv[i], Label: kv[i+1]}) + } + return out +} + +// Screen is the screen last shown (nil: none yet). +func (d *Desk) Screen() *Screen { return d.last } diff --git a/internal/tui/dialog.go b/internal/tui/dialog.go new file mode 100644 index 0000000..61ee6ee --- /dev/null +++ b/internal/tui/dialog.go @@ -0,0 +1,245 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +package tui + +import ( + "strings" + "unicode" +) + +// Dialog kinds: the colors of a dialog. +const ( + Info = iota // black on gray + Alert // white on red: an error, a question that loses something +) + +// Message shows a dialog over base: a title, text (wrapped) and buttons +// (none: "OK"). The button chosen, -1 for Esc. A button's first letter +// chooses it too. +func (d *Desk) Message(base *Screen, kind int, title, text string, buttons ...string) (int, error) { + if len(buttons) == 0 { + buttons = []string{"OK"} + } + frame, inside, btn, btnOn := Attr{White, LightGray}, Attr{Black, LightGray}, Attr{Black, Cyan}, Attr{White, Green} + if kind == Alert { + frame, inside, btn, btnOn = Attr{White, Red}, Attr{White, Red}, Attr{Black, LightGray}, Attr{White, Green} + } + bw := 0 + for _, b := range buttons { + bw += Width(b) + 6 + } + w := max(bw+4, Width(title)+10, 36) + if Width(text) > 60 { + w = max(w, 64) + } + w = min(w, d.Cols-4) + lines := []string{} + for _, para := range strings.Split(text, "\n") { + lines = append(lines, Wrap(para, w-6)...) + } + for _, l := range lines { + w = max(w, min(Width(l)+6, d.Cols-4)) + } + maxLines := d.Rows - 8 + if len(lines) > maxLines { + lines = append(lines[:maxLines-1], "…") + } + h := len(lines) + 5 + row := max(1, (d.Rows-h)/2) + col := (d.Cols - w) / 2 + cur := 0 + for { + scr := base.Clone() + scr.Box(row, col, h, w, Double, frame, inside) + scr.Shadow(row, col, h, w) + scr.Title(row, col, w, title, frame) + for i, l := range lines { + scr.Put(row+2+i, col+3, l, inside) + } + x := col + (w-bw)/2 + 1 + for i, b := range buttons { + a := btn + if i == cur { + a = btnOn + } + label := " " + b + " " + scr.Put(row+h-2, x, label, a) + if i == cur { + scr.Put(row+h-2, x, "►", a) + } + x += Width(label) + 2 + } + if err := d.Show(scr); err != nil { + return -1, err + } + k, _, err := d.Key(-1) + if err != nil { + return -1, err + } + switch k { + case KeyEsc: + return -1, nil + case KeyEnter, ' ': + return cur, nil + case KeyLeft, KeyUp: + cur = (cur + len(buttons) - 1) % len(buttons) + case KeyRight, KeyDown, KeyTab: + cur = (cur + 1) % len(buttons) + default: + for i, b := range buttons { + if k < KeyUp && strings.HasPrefix(strings.ToUpper(b), string(unicode.ToUpper(k))) { + return i, nil + } + } + } + } +} + +// Confirm asks a yes-or-no question: whether the answer is yes. +func (d *Desk) Confirm(base *Screen, kind int, title, text string) (bool, error) { + i, err := d.Message(base, kind, title, text, "Yes", "No") + return i == 0, err +} + +// Input is a box over base to type a line in: prompt above it, value to +// begin with. The line, and whether it was taken (Enter) or not (Esc). +// Mask hides what is typed. Cursor keys, Home, End, Del, BS and ^U edit. +func (d *Desk) Input(base *Screen, title, prompt, value string, limit int, mask bool) (string, bool, error) { + if limit <= 0 { + limit = 255 + } + w := min(d.Cols-4, 72) + fw := w - 6 // the field's width + plines := Wrap(prompt, w-6) + if prompt == "" { + plines = nil + } + plines = plines[:min(len(plines), 4)] + h := 5 + len(plines) + row := max(1, (d.Rows-h)/2-1) + col := (d.Cols - w) / 2 + line := []rune(value) + pos, off := len(line), 0 + frame, inside := Attr{White, Blue}, Attr{White, Blue} + field := Attr{Black, Cyan} + for { + if pos < off { + off = pos + } + if pos >= off+fw { + off = pos - fw + 1 + } + scr := base.Clone() + scr.Box(row, col, h, w, Double, frame, inside) + scr.Shadow(row, col, h, w) + scr.Title(row, col, w, title, Attr{Yellow, Blue}) + for i, l := range plines { + scr.Put(row+1+i, col+3, l, inside) + } + fr := row + 2 + len(plines) + shown := line[off:min(len(line), off+fw)] + s := string(shown) + if mask { + s = strings.Repeat("*", len(shown)) + } + scr.PutW(fr, col+3, fw, s, field) + if off > 0 { + scr.Put(fr, col+2, "◄", Attr{Yellow, Blue}) + } + if off+fw < len(line) { + scr.Put(fr, col+3+fw, "►", Attr{Yellow, Blue}) + } + d.KeyBar(scr, Hints("Enter", "Done", "Esc", "Cancel", "^U", "Clear", "←→", "Move"), "") + if err := d.Show(scr); err != nil { + return value, false, err + } + // the cursor where the next character goes, and shown + cx := col + 3 + pos - off + 1 + if err := d.Line.WriteRaw("\x1b[" + itoa(fr+1) + ";" + itoa(cx) + "H\x1b[?25h"); err != nil { + return value, false, err + } + k, _, err := d.Key(-1) + if err != nil { + return value, false, err + } + switch { + case k == KeyEnter: + return string(line), true, nil + case k == KeyEsc: + return value, false, nil + case k == KeyLeft: + pos = max(pos-1, 0) + case k == KeyRight: + pos = min(pos+1, len(line)) + case k == KeyHome || k == 0x01: + pos = 0 + case k == KeyEnd || k == 0x05: + pos = len(line) + case k == KeyBackspace: + if pos > 0 { + line = append(line[:pos-1], line[pos:]...) + pos-- + } + case k == KeyDel || k == 0x04: + if pos < len(line) { + line = append(line[:pos], line[pos+1:]...) + } + case k == 0x15: + line, pos, off = nil, 0, 0 + case k == 0x0b: // ^K: to the end + line = line[:pos] + case k >= ' ' && k < KeyUp && k != 0x7f && len(line) < limit: + line = append(line[:pos], append([]rune{k}, line[pos:]...)...) + pos++ + } + } +} + +// Choose is a list over base to pick one of choices from (cur: the one +// the bar starts on): its index, and whether one was picked. notes are +// shown beside the choices (may be nil). +func (d *Desk) Choose(base *Screen, title string, choices, notes []string, cur int) (int, bool, error) { + l := &List{Title: title, Center: true, Cur: cur} + for i, c := range choices { + e := Entry{Label: c} + if i < len(notes) && notes[i] != "" { + e.Value = notes[i] + e.Unset = true + } + l.Items = append(l.Items, e) + } + l.Fix() + for { + scr := base.Clone() + l.Draw(d, scr, true) + d.KeyBar(scr, Hints("Enter", "Pick", "Esc", "Cancel", "↑↓", "Move"), "") + if err := d.Show(scr); err != nil { + return cur, false, err + } + k, _, err := d.Key(-1) + if err != nil { + return cur, false, err + } + switch k { + case KeyEnter, ' ': + return l.Cur, true, nil + case KeyEsc, KeyLeft: + return cur, false, nil + default: + l.Move(d, k) + } + } +} + +func itoa(n int) string { + if n == 0 { + return "0" + } + var b []byte + for n > 0 { + b = append([]byte{byte('0' + n%10)}, b...) + n /= 10 + } + return string(b) +} diff --git a/internal/tui/keys.go b/internal/tui/keys.go new file mode 100644 index 0000000..d86b2ef --- /dev/null +++ b/internal/tui/keys.go @@ -0,0 +1,197 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +package tui + +import ( + "errors" + "time" + "unicode/utf8" + + "github.com/D-Modem/menu/internal/term" +) + +// Keys that are no character, as runes past Unicode's private area. +const ( + KeyUp rune = 0x10ff00 + iota + KeyDown + KeyLeft + KeyRight + KeyHome + KeyEnd + KeyPgUp + KeyPgDn + KeyIns + KeyDel + KeyF1 // F1 to F12: KeyF1+0 to KeyF1+11 +) + +const ( + KeyEnter rune = '\r' + KeyEsc rune = 0x1b + KeyTab rune = '\t' + KeyBackspace rune = 0x08 +) + +// F is function key n (1-12). +func F(n int) rune { return KeyF1 + rune(n-1) } + +// ErrTimeout: no key came in time (the line's own). +var ErrTimeout = term.ErrTimeout + +// escWait is how long the rest of an escape sequence may take after its +// ESC: a byte at 300 bit/s takes 33 ms, and modems send in bursts. +const escWait = 400 * time.Millisecond + +// a VT's ESC [ n ~ keys +var tildeKeys = map[int]rune{1: KeyHome, 2: KeyIns, 3: KeyDel, 4: KeyEnd, 5: KeyPgUp, 6: KeyPgDn, + 7: KeyHome, 8: KeyEnd, 11: F(1), 12: F(2), 13: F(3), 14: F(4), 15: F(5), 17: F(6), 18: F(7), + 19: F(8), 20: F(9), 21: F(10), 23: F(11), 24: F(12)} + +// readKey is a key from read (which returns ErrTimeout itself, or any +// error); alt: a letter typed with Alt (ESC and it at once). +func (d *Desk) readKey(wait time.Duration) (k rune, alt bool, err error) { + for { + b, err := d.next(wait) + if err != nil { + return 0, false, err + } + switch { + case b == '\n' && d.lastCR: + d.lastCR = false + continue + case b == '\r' || b == '\n': + d.lastCR = b == '\r' + return KeyEnter, false, nil + case b == 0x7f || b == 0x08: + d.lastCR = false + return KeyBackspace, false, nil + case b == 0x1b: + d.lastCR = false + return d.escape() + case b >= 0xc0 && d.Charset == UTF8: + d.lastCR = false + buf := []byte{b} + for len(buf) < 4 && !utf8.FullRune(buf) { + c, err := d.next(escWait) + if err != nil { + break + } + buf = append(buf, c) + } + r, _ := utf8.DecodeRune(buf) + return r, false, nil + case b >= 0x80: + continue // not a key the screens take + } + d.lastCR = false + return rune(b), false, nil + } +} + +// escape is the key whose ESC came. +func (d *Desk) escape() (rune, bool, error) { + b, err := d.next(escWait) + if errors.Is(err, ErrTimeout) { + return KeyEsc, false, nil + } + if err != nil { + return 0, false, err + } + switch b { + case '[': + case 'O': + c, err := d.next(escWait) + if err != nil { + return KeyEsc, false, nil + } + switch c { + case 'A', 'B', 'C', 'D', 'H', 'F': + return cursorKey(c), false, nil + case 'P', 'Q', 'R', 'S': + return F(int(c-'P') + 1), false, nil + } + return KeyEsc, false, nil + case 0x1b: + d.unread(b) + return KeyEsc, false, nil + default: + if b >= 'a' && b <= 'z' { + b -= 'a' - 'A' + } + if b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' { + return rune(b), true, nil + } + d.unread(b) // a key of its own, after Esc + return KeyEsc, false, nil + } + // CSI: parameters, then the final byte + n, params := 0, 0 + for i := 0; i < 16; i++ { + c, err := d.next(escWait) + if err != nil { + return KeyEsc, false, nil + } + switch { + case c >= '0' && c <= '9': + if params == 0 { + n = n*10 + int(c-'0') + } + case c == ';': + params++ + case c == '~': + if k, ok := tildeKeys[n]; ok { + return k, false, nil + } + return KeyEsc, false, nil + case c >= 0x40 && c <= 0x7e: + if k := cursorKey(c); k != 0 { + return k, false, nil + } + return KeyEsc, false, nil + } + } + return KeyEsc, false, nil +} + +// cursorKey is ESC [ c's key (0: none). +func cursorKey(c byte) rune { + switch c { + case 'A': + return KeyUp + case 'B': + return KeyDown + case 'C': + return KeyRight + case 'D': + return KeyLeft + case 'H': + return KeyHome + case 'F': + return KeyEnd + case 'P', 'Q', 'R', 'S': + return F(int(c-'P') + 1) + } + return 0 +} + +// next is the next byte (what was put back first). +func (d *Desk) next(wait time.Duration) (byte, error) { + if len(d.pending) > 0 { + b := d.pending[0] + d.pending = d.pending[1:] + return b, nil + } + if wait < 0 { + wait = d.Idle + if wait <= 0 { + wait = 24 * time.Hour + } + } + if wait == 0 { + return 0, ErrTimeout + } + return d.Line.ReadOne(wait) +} + +func (d *Desk) unread(b byte) { d.pending = append([]byte{b}, d.pending...) } diff --git a/internal/tui/list.go b/internal/tui/list.go new file mode 100644 index 0000000..9ba5369 --- /dev/null +++ b/internal/tui/list.go @@ -0,0 +1,320 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +package tui + +import ( + "strings" + "unicode" +) + +// Entry is a line of a List: a label and its value, or a heading. +type Entry struct { + ID string + Key string // shown before the label, as [B]; typing it picks the entry + Label string + Value string // at the right of the labels + Unset bool // Value is a default, not set: shown dim + Note string // after the value, dim + Help string // in the help box while the bar is on it + Mark string // at the window's right edge: ► a window, ↕ a choice + Heading bool // a line across the window, Label in it + Off bool // shown, not chosen + Changed bool // a * before it +} + +// choosable is whether the bar may rest on e. +func (e *Entry) choosable() bool { return !e.Heading && !e.Off } + +// List is a window of entries with a bar on one of them. +type List struct { + Title string + Notes []string // lines above the entries (three at most) + Items []Entry + Cur int + top int + // where it goes: Depth cascades it down and right of the first window + // (0); Center puts it in the middle of the desk + Depth int + Center bool + MinW int +} + +// colors of a window, as it is up or under another +type winColors struct { + frame, title, label, value, unset, note, mark, off, head, key Attr + bar, barValue, barKey Attr + box BoxStyle +} + +var ( + activeWin = winColors{ + frame: Attr{White, Blue}, title: Attr{Yellow, Blue}, label: Attr{White, Blue}, + value: Attr{Yellow, Blue}, unset: Attr{LightGray, Blue}, note: Attr{Cyan, Blue}, + mark: Attr{LightRed, Blue}, off: Attr{DarkGray, Blue}, head: Attr{LightCyan, Blue}, + key: Attr{LightCyan, Blue}, bar: Attr{White, Cyan}, barValue: Attr{Yellow, Cyan}, + barKey: Attr{Yellow, Cyan}, box: Double, + } + inactiveWin = winColors{ + frame: Attr{LightGray, Blue}, title: Attr{LightGray, Blue}, label: Attr{LightGray, Blue}, + value: Attr{LightGray, Blue}, unset: Attr{LightGray, Blue}, note: Attr{LightGray, Blue}, + mark: Attr{LightGray, Blue}, off: Attr{DarkGray, Blue}, head: Attr{LightGray, Blue}, + key: Attr{LightGray, Blue}, bar: Attr{Black, LightGray}, barValue: Attr{Black, LightGray}, + barKey: Attr{Black, LightGray}, box: Single, + } +) + +// geometry is a window's place: top row and left column, height and width +// (frame and all), and the rows of notes and of entries. +type geometry struct{ row, col, h, w, notes, list int } + +// widths: the labels' column (with keys) and the values'. +func (l *List) widths() (kw, lw, vw int) { + for _, e := range l.Items { + if e.Heading { + lw = max(lw, Width(e.Label)+4-12) + continue + } + if e.Key != "" { + kw = max(kw, Width(e.Key)+3) + } + lw = max(lw, Width(e.Label)) + v := e.Value + if e.Note != "" { + v += " " + e.Note + } + vw = max(vw, Width(v)) + } + return +} + +func (l *List) geom(d *Desk) geometry { + first, last := 2, d.Bottom() + kw, lw, vw := l.widths() + for _, n := range l.Notes { + vw = max(vw, Width(n)-lw-kw-4) + } + maxW := d.Cols - 4 + width := 2 + 2 + kw + lw + 3 + if vw > 0 { + width += 2 + min(max(vw, 12), 40) + } + width = max(width, Width(l.Title)+10, min(l.MinW, maxW), 30) + width = min(width, maxW) + off := min(l.Depth, 5) + g := geometry{w: width, col: 2 + 3*off, row: first + off} + g.notes = min(len(l.Notes), 3) + if g.notes > 0 { + g.notes++ // the rule under them + } + g.list = max(len(l.Items), 1) + g.h = 2 + g.notes + g.list + if l.Center { + g.col = (d.Cols - g.w) / 2 + g.row = max(first, first+(last-first+1-g.h)/2) + } + if g.col+g.w+2 > d.Cols { + g.col = max(d.Cols-2-g.w, 0) + } + room := last - g.row + 1 + if g.h > room { + if room < 2+g.notes+5 { // too far down: up as far as needed + g.row = max(first, last+1-(2+g.notes+5)) + room = last - g.row + 1 + } + g.h = min(g.h, room) + g.list = max(g.h-2-g.notes, 1) + } + return g +} + +// Fix puts the bar on an entry it may rest on (the first after Cur). +func (l *List) Fix() { + if l.Cur >= 0 && l.Cur < len(l.Items) && l.Items[l.Cur].choosable() { + return + } + if j := l.step(l.Cur, 1); j >= 0 { + l.Cur = j + } else if j := l.step(l.Cur, -1); j >= 0 { + l.Cur = j + } +} + +// step is the next entry the bar may rest on from i, by dir (-1 if none). +func (l *List) step(i, dir int) int { + for j := i + dir; j >= 0 && j < len(l.Items); j += dir { + if l.Items[j].choosable() { + return j + } + } + return -1 +} + +// Selected is the entry under the bar (nil: none). +func (l *List) Selected() *Entry { + if l.Cur < 0 || l.Cur >= len(l.Items) || !l.Items[l.Cur].choosable() { + return nil + } + return &l.Items[l.Cur] +} + +// Move handles the keys that move the bar: whether k was one. A letter +// moves to the next entry it begins (or whose Key it is, picked: true). +func (l *List) Move(d *Desk, k rune) (moved, picked bool) { + g := l.geom(d) + to := func(j int) { + if j >= 0 { + l.Cur = j + } + } + switch k { + case KeyUp: + to(l.step(l.Cur, -1)) + case KeyDown, KeyTab: + to(l.step(l.Cur, 1)) + case KeyHome: + to(l.step(-1, 1)) + case KeyEnd: + to(l.step(len(l.Items), -1)) + case KeyPgUp, KeyPgDn: + dir := 1 + if k == KeyPgUp { + dir = -1 + } + j := min(max(l.Cur+dir*(g.list-1), 0), len(l.Items)-1) + if n := l.step(j-dir, dir); n >= 0 { + to(n) + } else { + to(l.step(j, -dir)) + } + default: + if k < ' ' || k >= KeyUp || k == ' ' { + return false, false + } + u := strings.ToUpper(string(k)) + for i, e := range l.Items { + if e.choosable() && e.Key != "" && strings.ToUpper(e.Key) == u { + l.Cur = i + return true, true + } + } + n := len(l.Items) + for o := 1; o <= n; o++ { + j := (l.Cur + o + n) % n + e := l.Items[j] + if e.choosable() && strings.HasPrefix(strings.ToUpper(e.Label), u) { + l.Cur = j + return true, false + } + } + return false, false + } + return true, false +} + +// Draw draws the window on scr, active (on top: a double frame, the bar +// bright) or under another. +func (l *List) Draw(d *Desk, scr *Screen, active bool) { + c := inactiveWin + if active { + c = activeWin + } + g := l.geom(d) + scr.Box(g.row, g.col, g.h, g.w, c.box, c.frame, c.label) + scr.Shadow(g.row, g.col, g.h, g.w) + if l.Title != "" { + scr.Title(g.row, g.col, g.w, l.Title, c.title) + } + inner := g.w - 2 + for i := 0; i < min(len(l.Notes), 3); i++ { + scr.PutW(g.row+1+i, g.col+1, inner, " "+Trunc(l.Notes[i], inner-2), c.note) + } + if g.notes > 0 { + scr.Rule(g.row+g.notes, g.col, g.w, c.box, c.frame) + } + cur := l.Cur + if cur >= 0 && cur < l.top { + l.top = cur + if l.top > 0 && l.Items[l.top-1].Heading { + l.top-- // a heading just above comes too + } + } + if cur >= l.top+g.list { + l.top = cur - g.list + 1 + } + l.top = max(min(l.top, len(l.Items)-g.list), 0) + kw, lw, vmax := l.widths() + _ = vmax + if vmax > 0 { + lw = min(lw, max(inner*11/20-kw, inner-6-kw-max(vmax, 8))) + } else { + lw = inner - 4 - kw + } + vw := inner - 2 - kw - lw - 2 - 2 + first := g.row + 1 + g.notes + for r := 0; r < g.list; r++ { + i := l.top + r + row := first + r + if i >= len(l.Items) { + break + } + e := l.Items[i] + if e.Heading { + scr.Put(row, g.col+1, strings.Repeat("─", inner), c.off) + if e.Label != "" { + scr.Put(row, g.col+2, " "+Trunc(e.Label, inner-4)+" ", c.head) + } + continue + } + label, value, unset, note, mark, key := c.label, c.value, c.unset, c.note, c.mark, c.key + if e.Off { + label, value, unset, note, key = c.off, c.off, c.off, c.off, c.off + } + if i == cur && active || i == cur && !e.Off { + scr.Fill(row, g.col+1, 1, inner, ' ', c.bar) + label, unset, note, mark, value, key = c.bar, c.bar, c.bar, c.barValue, c.barValue, c.barKey + } + if e.Changed { + scr.Put(row, g.col+1, "*", mark) + } + x := g.col + 2 + if kw > 0 { + if e.Key != "" { + scr.Put(row, x, "[", label) + scr.Put(row, x+1, e.Key, key) + scr.Put(row, x+1+Width(e.Key), "]", label) + } + x += kw + } + scr.PutW(row, x, lw, Trunc(e.Label, lw), label) + if vw > 0 { + va := value + if e.Unset { + va = unset + } + x += lw + 2 + n := scr.Put(row, x, Trunc(e.Value, vw), va) + if e.Note != "" && n+3 < vw && vw >= 20 { + scr.Put(row, x+n+2, Trunc(e.Note, vw-n-2), note) + } + } + if e.Mark != "" { + scr.Put(row, g.col+g.w-3, e.Mark, value) + } + } + if l.top > 0 { + scr.Put(first, g.col+g.w-1, "▲", c.frame) + } + if l.top+g.list < len(l.Items) { + scr.Put(first+g.list-1, g.col+g.w-1, "▼", c.frame) + } +} + +// capFirst is s with its first letter a capital. +func capFirst(s string) string { + r := []rune(s) + if len(r) > 0 { + r[0] = unicode.ToUpper(r[0]) + } + return string(r) +} diff --git a/internal/tui/screen.go b/internal/tui/screen.go new file mode 100644 index 0000000..a3765ab --- /dev/null +++ b/internal/tui/screen.go @@ -0,0 +1,345 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +// Package tui is the full screen of an ANSI terminal, drawn in character +// cells as the setup programs and BBSs of the 1990s drew theirs: a shaded +// blue desk, windows with shadows over it, a menu bar of pull-down menus +// at the top and a bar of keys at the foot. What is drawn is kept, and +// only the cells that change are sent, so that a caller at 2400 bit/s is +// not sent the whole screen for a moved bar. A terminal without UTF-8 +// gets code page 437, a VT the DEC line-drawing set, others ASCII. +// +// The screen buffer and its rendering come from the bbs's setup screens +// (D-Modem/bbs internal/textui/desk.go). +package tui + +import ( + "strconv" + "strings" + "unicode/utf8" +) + +// The sixteen colors of a PC's text mode: 0-7 as backgrounds too. +const ( + Black = iota + Blue + Green + Cyan + Red + Magenta + Brown + LightGray + DarkGray + LightBlue + LightGreen + LightCyan + LightRed + LightMagenta + Yellow + White +) + +// Attr is a cell's colors: foreground (0-15) and background (0-7). +type Attr struct{ FG, BG uint8 } + +// A is an Attr. +func A(fg, bg uint8) Attr { return Attr{fg, bg} } + +// cell is a character cell. +type cell struct { + r rune + a Attr +} + +// Screen is a screen's cells. +type Screen struct { + Cols, Rows int + c []cell +} + +// NewScreen is a blank screen. +func NewScreen(cols, rows int) *Screen { + s := &Screen{Cols: cols, Rows: rows, c: make([]cell, cols*rows)} + for i := range s.c { + s.c[i] = cell{' ', Attr{LightGray, Black}} + } + return s +} + +// Clone is a copy of s. +func (s *Screen) Clone() *Screen { + n := &Screen{Cols: s.Cols, Rows: s.Rows, c: make([]cell, len(s.c))} + copy(n.c, s.c) + return n +} + +// at is the cell at row and column (from 0), nil off the screen. +func (s *Screen) at(row, col int) *cell { + if row < 0 || col < 0 || row >= s.Rows || col >= s.Cols { + return nil + } + return &s.c[row*s.Cols+col] +} + +// Rune is the character at row and column (0 off the screen). +func (s *Screen) Rune(row, col int) rune { + if c := s.at(row, col); c != nil { + return c.r + } + return 0 +} + +// AttrAt is the colors at row and column. +func (s *Screen) AttrAt(row, col int) Attr { + if c := s.at(row, col); c != nil { + return c.a + } + return Attr{} +} + +// Line is a row's characters, for tests and logs. +func (s *Screen) Line(row int) string { + var b strings.Builder + for x := 0; x < s.Cols; x++ { + b.WriteRune(s.Rune(row, x)) + } + return b.String() +} + +// Text is the whole screen's characters, a line to a row. +func (s *Screen) Text() string { + var b strings.Builder + for y := 0; y < s.Rows; y++ { + b.WriteString(strings.TrimRight(s.Line(y), " ") + "\n") + } + return b.String() +} + +// Put writes str from row, col in a, cut at the screen's edge; how many +// cells it took. +func (s *Screen) Put(row, col int, str string, a Attr) int { + n := 0 + for _, r := range str { + if c := s.at(row, col+n); c != nil { + *c = cell{r, a} + } + n++ + } + return n +} + +// PutW writes str in exactly w cells: cut, or padded with spaces. +func (s *Screen) PutW(row, col, w int, str string, a Attr) { + s.Put(row, col, Fit(str, w), a) +} + +// Fill fills a rectangle with r in a. +func (s *Screen) Fill(row, col, h, w int, r rune, a Attr) { + for y := row; y < row+h; y++ { + for x := col; x < col+w; x++ { + if c := s.at(y, x); c != nil { + *c = cell{r, a} + } + } + } +} + +// Paint sets the colors of a rectangle, keeping its characters. +func (s *Screen) Paint(row, col, h, w int, a Attr) { + for y := row; y < row+h; y++ { + for x := col; x < col+w; x++ { + if c := s.at(y, x); c != nil { + c.a = a + } + } + } +} + +// BoxStyle is a box's lines: corners, sides and the joins of a line +// across it. +type BoxStyle struct{ tl, tr, bl, br, h, v, lj, rj rune } + +var ( + Double = BoxStyle{'╔', '╗', '╚', '╝', '═', '║', '╟', '╢'} + Single = BoxStyle{'┌', '┐', '└', '┘', '─', '│', '├', '┤'} +) + +// Box draws a box's frame and fills its inside. +func (s *Screen) Box(row, col, h, w int, st BoxStyle, frame, inside Attr) { + s.Fill(row+1, col+1, h-2, w-2, ' ', inside) + for x := col + 1; x < col+w-1; x++ { + s.Put(row, x, string(st.h), frame) + s.Put(row+h-1, x, string(st.h), frame) + } + for y := row + 1; y < row+h-1; y++ { + s.Put(y, col, string(st.v), frame) + s.Put(y, col+w-1, string(st.v), frame) + } + s.Put(row, col, string(st.tl), frame) + s.Put(row, col+w-1, string(st.tr), frame) + s.Put(row+h-1, col, string(st.bl), frame) + s.Put(row+h-1, col+w-1, string(st.br), frame) +} + +// Rule draws a line across a box at row, joined to its sides. +func (s *Screen) Rule(row, col, w int, st BoxStyle, a Attr) { + s.Put(row, col, string(st.lj), a) + for x := col + 1; x < col+w-1; x++ { + s.Put(row, x, "─", a) + } + s.Put(row, col+w-1, string(st.rj), a) +} + +// Title puts a title in a box's top line, centered: " Title ". +func (s *Screen) Title(row, col, w int, t string, a Attr) { + t = " " + Trunc(t, w-6) + " " + s.Put(row, col+(w-len([]rune(t)))/2, t, a) +} + +// Shadow darkens the cells to the right of and below a rectangle, as a +// window's shadow on the desk. +func (s *Screen) Shadow(row, col, h, w int) { + dark := Attr{DarkGray, Black} + s.Paint(row+1, col+w, h, 2, dark) + s.Paint(row+h, col+2, 1, w, dark) +} + +// Render is what turns the terminal's screen, old (nil: unknown), into s: +// each changed run of cells, with its colors. The last cell of the last +// line is left alone (writing it scrolls some terminals). +func (s *Screen) Render(old *Screen, mono bool) string { + var b strings.Builder + if old == nil || old.Cols != s.Cols || old.Rows != s.Rows { + b.WriteString("\x1b[0m\x1b[2J") + old = nil + } + cur := Attr{255, 255} + for y := 0; y < s.Rows; y++ { + last := s.Cols + if y == s.Rows-1 { + last-- + } + at := -1 // where the cursor is after the last cell written (-1: unknown) + for x := 0; x < last; x++ { + c := s.c[y*s.Cols+x] + if old != nil && old.c[y*s.Cols+x] == c { + // unchanged: skipped, unless a change follows so near + // that writing the cells costs less than moving + if at != x || !s.changedWithin(old, y, x, last, 8) { + continue + } + } + if at != x { + b.WriteString("\x1b[" + strconv.Itoa(y+1) + ";" + strconv.Itoa(x+1) + "H") + } + if c.a != cur { + b.WriteString(sgr(c.a, mono)) + cur = c.a + } + r := c.r + if r < ' ' || r == 0x7f || !utf8.ValidRune(r) { + r = ' ' + } + b.WriteRune(r) + at = x + 1 + } + } + return b.String() +} + +// changedWithin is whether a cell of row y from x, before last, within n +// of x, differs from old's. +func (s *Screen) changedWithin(old *Screen, y, x, last, n int) bool { + for i := x; i < min(x+n, last); i++ { + if old.c[y*s.Cols+i] != s.c[y*s.Cols+i] { + return true + } + } + return false +} + +// sgr is the escape that gives a's colors (in mono: bright as bold, a light +// background as reverse). +func sgr(a Attr, mono bool) string { + if mono { + s := "\x1b[0" + if a.FG >= 8 && a.FG != DarkGray { + s += ";1" + } + if a.BG == LightGray || a.BG == Cyan || a.BG == Green { + s += ";7" + } + return s + "m" + } + ansi := []int{0, 4, 2, 6, 1, 5, 3, 7} // the PC's order to ANSI's + s := "\x1b[0" + if a.FG >= 8 { + s += ";1" + } + return s + ";" + strconv.Itoa(30+ansi[a.FG%8]) + ";" + strconv.Itoa(40+ansi[a.BG%8]) + "m" +} + +// Fit is s cut or padded with spaces to exactly w runes. +func Fit(s string, w int) string { + if w <= 0 { + return "" + } + r := []rune(s) + if len(r) > w { + return string(r[:w]) + } + return s + strings.Repeat(" ", w-len(r)) +} + +// Trunc is s cut to w runes, with "…" where it was cut. +func Trunc(s string, w int) string { + r := []rune(s) + if w <= 0 { + return "" + } + if len(r) <= w { + return s + } + if w == 1 { + return string(r[:1]) + } + return string(r[:w-1]) + "…" +} + +// Wrap breaks s into lines of at most w runes, at spaces where it can. +func Wrap(s string, w int) []string { + var out []string + for _, para := range strings.Split(s, "\n") { + words := strings.Fields(para) + if len(words) == 0 { + out = append(out, "") + continue + } + line := "" + for _, wd := range words { + for len([]rune(wd)) > w { + if line != "" { + out = append(out, line) + line = "" + } + r := []rune(wd) + out = append(out, string(r[:w])) + wd = string(r[w:]) + } + switch { + case line == "": + line = wd + case len([]rune(line))+1+len([]rune(wd)) <= w: + line += " " + wd + default: + out = append(out, line) + line = wd + } + } + out = append(out, line) + } + return out +} + +// Width is s's width in cells. +func Width(s string) int { return utf8.RuneCountInString(s) } diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go new file mode 100644 index 0000000..2b5b1a8 --- /dev/null +++ b/internal/tui/tui_test.go @@ -0,0 +1,170 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +package tui + +import ( + "bytes" + "strings" + "testing" + "time" +) + +// fakeLine: keys queued, what is written kept +type fakeLine struct { + in []byte + out bytes.Buffer +} + +func (f *fakeLine) ReadOne(time.Duration) (byte, error) { + if len(f.in) == 0 { + return 0, ErrTimeout + } + b := f.in[0] + f.in = f.in[1:] + return b, nil +} + +func (f *fakeLine) WriteRaw(s string) error { f.out.WriteString(s); return nil } + +// TestRender: the whole screen at first, then only what changed +func TestRender(t *testing.T) { + a := NewScreen(20, 5) + a.Put(1, 2, "hello", Attr{White, Blue}) + full := a.Render(nil, false) + if !strings.HasPrefix(full, "\x1b[0m\x1b[2J") || !strings.Contains(full, "hello") { + t.Fatalf("full: %q", full) + } + b := a.Clone() + b.Put(1, 3, "a", Attr{White, Blue}) + if d := b.Render(a, false); d != "\x1b[2;4H\x1b[0;1;37;44ma" { + t.Errorf("one cell: %q", d) + } + if d := b.Render(b, false); d != "" { + t.Errorf("nothing changed: %q", d) + } + // two changes close together: the cells between written, not a move + c := b.Clone() + c.Put(1, 2, "J", Attr{White, Blue}) + c.Put(1, 5, "y", Attr{White, Blue}) + if d := c.Render(b, false); d != "\x1b[2;3H\x1b[0;1;37;44mJaly" { + t.Errorf("a run: %q", d) + } +} + +// TestDeskPlain: a plain desk is cleared in its colors, its blank cells +// not sent +func TestDeskPlain(t *testing.T) { + f := &fakeLine{} + d := &Desk{Line: f, Rows: 24, Cols: 80, Charset: CP437} + scr := d.Blank(-1) + d.Show(scr) + plain := f.out.Len() + f.out.Reset() + d.Shade = true + d.Redraw() + d.Show(d.Blank(-1)) + if shaded := f.out.Len(); plain > 600 || shaded < 1800 { + t.Errorf("a plain desk took %d bytes, a shaded one %d", plain, shaded) + } + // a moved bar: a few bytes + l := &List{Title: "Menu", Items: []Entry{{Key: "A", Label: "One"}, {Key: "B", Label: "Two"}}} + s1 := d.Blank(-1) + l.Draw(d, s1, true) + d.Show(s1) + f.out.Reset() + l.Move(d, KeyDown) + s2 := d.Blank(-1) + l.Draw(d, s2, true) + d.Show(s2) + if n := f.out.Len(); n > 200 { + t.Errorf("moving the bar took %d bytes: %q", n, f.out.String()) + } +} + +// TestEncode: the lines in each charset +func TestEncode(t *testing.T) { + s := "\x1b[1;1H╔═╗║░► x" + for _, c := range []struct { + cs Charset + want string + }{ + {UTF8, s}, + {CP437, "\x1b[1;1H\xc9\xcd\xbb\xba\xb0\xaf x"}, + {DEC, "\x1b[1;1H\x1b(0lqkxa\x1b(B> x"}, + {ASCII, "\x1b[1;1H+=+| > x"}, + } { + if got := string(Encode(s, c.cs)); got != c.want { + t.Errorf("%v: %q, want %q", c.cs, got, c.want) + } + } + // nothing of the control range: some terminals take it as controls + for r := range cp437 { + if b := cp437[r]; b < 0x20 { + t.Errorf("%c is sent as control %#x", r, b) + } + } +} + +// TestKeys: the keys of VTs, xterm and ANSI terminals, and Alt +func TestKeys(t *testing.T) { + f := &fakeLine{in: []byte("\x1b[A\x1bOB\x1b[5~\x1b[11~\x1bOP\x1b[21~\r\nx\x1bf\x1b\x7f\xc3\xa9")} + d := &Desk{Line: f, Charset: UTF8} + want := []struct { + k rune + alt bool + }{{KeyUp, false}, {KeyDown, false}, {KeyPgUp, false}, {F(1), false}, {F(1), false}, {F(10), false}, + {KeyEnter, false}, {'x', false}, {'F', true}, {KeyEsc, false}, {KeyBackspace, false}, {'é', false}} + for i, w := range want { + k, alt, err := d.Key(time.Second) + if err != nil || k != w.k || alt != w.alt { + t.Errorf("key %d: %q %v %v, want %q %v", i, k, alt, err, w.k, w.alt) + } + } + if _, _, err := d.Key(time.Second); err != ErrTimeout { + t.Errorf("no more keys: %v", err) + } +} + +// TestDialogs: a question answered, a line typed and edited, a choice +func TestDialogs(t *testing.T) { + f := &fakeLine{} + d := &Desk{Line: f, Rows: 24, Cols: 80} + base := d.Blank(-1) + f.in = []byte("\x1b[C\r") + if yes, err := d.Confirm(base, Info, "Q", "Sure?"); yes || err != nil { + t.Errorf("right, Enter: %v %v", yes, err) + } + f.in = []byte("n") + if i, _ := d.Message(base, Info, "Q", "Save?", "Save", "No", "Cancel"); i != 1 { + t.Errorf("n: %d", i) + } + f.in = []byte("\x1b[Dxy\x7f\x1b[H>\r") + if v, ok, _ := d.Input(base, "T", "type", "abc", 0, false); !ok || v != ">abxc" { + t.Errorf("edited: %q %v", v, ok) + } + f.in = []byte("\x1b") + if v, ok, _ := d.Input(base, "T", "type", "abc", 0, false); ok || v != "abc" { + t.Errorf("Esc: %q %v", v, ok) + } + f.in = []byte("\x1b[B\x1b[B\r") + if i, ok, _ := d.Choose(base, "Pick", []string{"a", "b", "c"}, nil, 0); !ok || i != 2 { + t.Errorf("choice %d %v", i, ok) + } +} + +// TestPullDown: a menu pulled down, moved across, an item picked +func TestPullDown(t *testing.T) { + f := &fakeLine{in: []byte("\x1b[C\x1b[B\r")} + d := &Desk{Line: f, Rows: 24, Cols: 80, Menus: []Menu{ + {Label: "File", Items: []MenuItem{{Label: "Save", ID: "save"}}}, + {Label: "Setup", Items: []MenuItem{{Label: "One", ID: "one"}, {Heading: true}, {Label: "Two", ID: "two"}}}, + }} + id, err := d.PullDown(d.Blank, 0) + if err != nil || id != "two" { + t.Errorf("picked %q %v", id, err) + } + if d.MenuKey('s') != 1 || d.MenuKey('f') != 0 || d.MenuKey('q') != -1 { + t.Errorf("menu keys") + } +} diff --git a/mkdocs.yml b/mkdocs.yml index 153caf4..3fef771 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,6 +16,7 @@ nav: - Building: building.md - Configuration: configuration.md - Configuration reference: reference.md + - Setup screens: setup.md - Authentication: authentication.md - PPP: ppp.md - Dial-up network: dialup-network.md diff --git a/samples/modemmenu.conf.sample b/samples/modemmenu.conf.sample index fafb264..4075e11 100644 --- a/samples/modemmenu.conf.sample +++ b/samples/modemmenu.conf.sample @@ -53,6 +53,8 @@ log = syslog ; backend that does not know the user passes to the next; a wrong password ; stops there auth = file +; what the caller sees first (or a file of it, which may have ANSI's +; colors), then the prompts of the login banner = "\r\nWelcome to D-Modem.\r\n" ;banner_file = /etc/asterisk/modemmenu.banner login_prompt = "\r\nlogin: " @@ -61,11 +63,14 @@ password_prompt = "Password: " ; "33600 bit/s", or "50666 bit/s down, 31200 bit/s up"); left out when it ; is not known; "" for none connect_message = "\r\nConnected at {speed}.\r\n" +; after a login, before the motd ({placeholders}; on the full-screen menu, +; at the top of its window) welcome = "\r\nHello {user}, on {date} at {time}.\r\n" ; the message of the day, after the welcome and before the menu: motd, ; then the file (read at each login), {placeholders} in both ;motd = "\r\nThe line is down for maintenance on Sunday.\r\n" ;motd_file = /etc/motd +; at the end of the call, before the line is hung up goodbye = "\r\nGoodbye.\r\n" ; the echo of each password character, or none password_echo = * @@ -85,6 +90,7 @@ user_pattern = ^[A-Za-z0-9][A-Za-z0-9._@-]{0,63}$ ; {time_left} (whole minutes) shows what is left, e.g. in [menu] prompt time_limit = 0 daily_limit = 0 +; where the usage, the last callers and the nodes are kept state_dir = /var/lib/modemmenu ; these long before the limit, the message is written on the line in the ; middle of a running selection (not one with time_warning = no), once @@ -103,6 +109,10 @@ detect = yes petscii = auto ; for each question a terminal may not answer timeout = 2s +; the full-screen menu's lines and shading: auto (UTF-8 when the terminal +; shows it, code page 437 for SyncTERM and other BBS terminals, a VT's +; line-drawing set for a DEC terminal), utf8, cp437, dec or ascii +charset = auto ; groups of users for the items' groups = (members of system groups count ; too, for logins by auth = system) @@ -344,18 +354,29 @@ items = bbs ppp shell passwd hangup = G goodbye bye logoff ; yes: one key selects at once, without Enter (one-character keys only) hotkeys = no +; auto: the menu full screen for a caller whose terminal can have it (ANSI, +; or a VT that said what it is, 40x14 at least): the items in a window on a +; blue desk, chosen with the cursor keys or an item's key, the welcome at +; its top and the motd in a box; others get the menu of lines. no: lines +; for every caller. The login is always lines (dialers and AOL clients +; read and answer it) +fullscreen = auto ; An item: ; key = B its key in the menu, one or more characters ; (default: its name's first letter) ; aliases = ... more words that select it (its name always does) ; label = ... its line in the menu +; help = ... a line of what it is, under the full-screen menu ; mode = exec a program on the line: command (by /bin/sh -c) ; passwd built in: the caller changes their password ; (min_password = 8 characters) ; callers built in: the last callers (count = 10), the time, ; user and rate of each login (state_dir/callers, ; the latest 200; kept only with such an item) +; setup built in: the sysop's setup, modemmenu.conf on +; full screens (as modemmenu -setup); needs users +; or groups ; tcp a TCP server, raw: host, port; send is sent first ; unix a server on a unix socket, raw: path; send too ; (tcp, telnet and rlogin take a path as well, for @@ -414,6 +435,7 @@ hotkeys = no [item bbs] key = B label = Bulletin board +help = Messages, files and doors on the board mode = rlogin host = 127.0.0.1 port = 513 @@ -502,6 +524,13 @@ after = menu ;mode = callers ;count = 10 +; the sysop's setup, on the line: the configuration on full screens +;[item setup] +;key = Z +;label = Setup +;mode = setup +;groups = sysops + ; files by ZMODEM (lrzsz): rz receives what the caller's terminal sends, ; binary, restricted to the directory, renaming rather than overwriting ;[item upload] diff --git a/samples/samples.go b/samples/samples.go new file mode 100644 index 0000000..67d821f --- /dev/null +++ b/samples/samples.go @@ -0,0 +1,13 @@ +// Copyright (C) 2026 D-Modem Authors +// SPDX-License-Identifier: AGPL-3.0-only + +// Package samples has the sample configuration in the program, for the +// help of modemmenu -setup's screens. +package samples + +import _ "embed" + +// Conf is modemmenu.conf.sample. +// +//go:embed modemmenu.conf.sample +var Conf string diff --git a/tools/confref/main.go b/tools/confref/main.go index 74db8fc..b950802 100644 --- a/tools/confref/main.go +++ b/tools/confref/main.go @@ -16,201 +16,18 @@ import ( "fmt" "log" "os" - "regexp" "strings" -) - -// kind is a kind of section: [general], [item NAME] ... -type kind struct { - name string - named bool - help string - free string // what its keys are, when they are the sysop's own names - keys []*key - byKey map[string]*key -} -// key is one key of a kind of section. -type key struct { - name string - help string - value string // the sample's (or its first commented example's) - examples []string // the other values the sample shows -} - -var ( - headerRE = regexp.MustCompile(`^;?\[([a-z_]+)(?:\s+(\S+))?\]$`) - keyRE = regexp.MustCompile(`^(;?)([a-z][a-z0-9_]*)\s*=\s*(.*)$`) - docKeyRE = regexp.MustCompile(`^;\s{2,4}([a-z][a-z0-9_]*) = (\S+)\s*(.*)$`) - docMoreRE = regexp.MustCompile(`^;\s{5,}(.*)$`) - // a value of a described key and what it does ("passwd built in") - docValueRE = regexp.MustCompile(`^([a-z0-9_]+)\s{2,}(\S.*)$`) + "github.com/D-Modem/menu/internal/confdoc" ) -// unquote is a sample value as the sysop writes it: without its quotes. -func unquote(v string) string { - if i := strings.Index(v, " ; "); i >= 0 && !strings.HasPrefix(v, `"`) { - v = v[:i] - } - v = strings.TrimSpace(v) - if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' { - return v[1 : len(v)-1] - } - return v -} - -// kinds' help where the sample has none before them, or has the first -// example's -var kindHelp = map[string]string{ - "general": "The login: the log, the backends, what the caller sees, the time limits.", - "menu": "The menu: its title, prompt, items and the words that hang up.", - "item": "A menu item: a program on the line, a server to connect the caller to, or a built-in (see the item keys below).", -} - -// kinds whose keys are the sysop's own names, and what such a key is -var freeKinds = map[string]string{ - "groups": "a group: its name, then its members (`sysops = alice bob`)", -} - -func parse(sample string) []*kind { - var kinds []*kind - byName := map[string]*kind{} - get := func(name string) *kind { - if k := byName[name]; k != nil { - return k - } - k := &kind{name: name, byKey: map[string]*key{}} - byName[name] = k - kinds = append(kinds, k) - return k - } - var cur *kind - var prose []string - keyOf := func(k *kind, name string) *key { - if x := k.byKey[name]; x != nil { - return x - } - x := &key{name: name} - k.byKey[name] = x - k.keys = append(k.keys, x) - return x - } - var doc *key // the key an indented doc line describes - var pending []*key // described keys not yet given to a kind - adopt := func(k *kind) { - for _, d := range pending { - x := keyOf(k, d.name) - if x.value == "" { - x.value = d.value - } - if x.help == "" { - x.help = d.help - } - } - pending = nil - } - docBlock := false - inHeader := true // the file's own comment before the first section - afterKey := false // a comment now starts the help of the keys after it - for _, raw := range strings.Split(sample, "\n") { - line := strings.TrimRight(raw, " \t\r") - if line == "" { - prose, doc, docBlock, afterKey = nil, nil, false, false - continue - } - if m := headerRE.FindStringSubmatch(line); m != nil { - inHeader = false - cur = get(m[1]) - adopt(cur) - if m[2] != "" { - cur.named = true - } - if cur.help == "" && len(prose) > 0 { - cur.help = strings.Join(prose, " ") - } - prose, doc, afterKey = nil, nil, false - continue - } - if inHeader { - continue - } - // an indented description of keys ("; key = B its key"): of - // the kind of section whose keys or header come next - if m := docKeyRE.FindStringSubmatch(line); m != nil && (docBlock || len(prose) > 0) { - docBlock = true - doc = &key{name: m[1], value: unquote(m[2]), help: m[3]} - pending = append(pending, doc) - continue - } - if m := docMoreRE.FindStringSubmatch(line); m != nil && doc != nil { - more := m[1] - if v := docValueRE.FindStringSubmatch(more); v != nil { - more = "; `" + v[1] + "`: " + v[2] - } - sep := " " - if strings.HasPrefix(more, ";") { - sep = "" - } - doc.help = strings.TrimSpace(doc.help + sep + more) - continue - } - if m := keyRE.FindStringSubmatch(line); m != nil && cur != nil { - adopt(cur) - x := keyOf(cur, m[2]) - v := unquote(m[3]) - if x.help == "" && len(prose) > 0 { - x.help = strings.Join(prose, " ") - } - if x.value == "" && m[1] == "" { - x.value = v - } else if v != "" && v != x.value { - seen := false - for _, e := range x.examples { - seen = seen || e == v - } - if !seen { - x.examples = append(x.examples, v) - } - } - afterKey = true - continue - } - if strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") { - t := strings.TrimSpace(strings.TrimLeft(line, ";#")) - if doc != nil || docBlock || afterKey { - // prose after a block of key descriptions starts afresh - prose, doc, docBlock, afterKey = nil, nil, false, false - } - prose = append(prose, t) - } - } - if cur != nil { - adopt(cur) - } - for _, k := range kinds { - if h, ok := kindHelp[k.name]; ok { - k.help = h - } - if f, ok := freeKinds[k.name]; ok { - k.free, k.keys = f, nil - } - } - // a key with no value of its own: its first example is it - for _, k := range kinds { - for _, x := range k.keys { - if x.value == "" && len(x.examples) > 0 { - x.value, x.examples = x.examples[0], x.examples[1:] - } - } - } - return kinds -} +type kind = confdoc.Kind func header(k *kind) string { - if k.named { - return "[" + k.name + " NAME]" + if k.Named { + return "[" + k.Name + " NAME]" } - return "[" + k.name + "]" + return "[" + k.Name + "]" } func anchor(k *kind) string { @@ -255,7 +72,7 @@ full. |---|---| `) for _, k := range kinds { - help := k.help + help := k.Help if i := strings.Index(help, ". "); i >= 0 { help = help[:i+1] } @@ -263,36 +80,36 @@ full. } for _, k := range kinds { fmt.Fprintf(&b, "\n## `%s`\n\n", header(k)) - if k.help != "" { - b.WriteString(prose(k.help) + "\n\n") + if k.Help != "" { + b.WriteString(prose(k.Help) + "\n\n") } - if k.named { + if k.Named { b.WriteString("There may be any number of these, each with its own NAME.\n\n") } - if k.free != "" { - fmt.Fprintf(&b, "Each key is %s.\n", k.free) + if k.Free != "" { + fmt.Fprintf(&b, "Each key is %s.\n", k.Free) } - if len(k.keys) == 0 { + if len(k.Keys) == 0 { continue } b.WriteString("| key | sample | what it does |\n|---|---|---|\n") - for _, x := range k.keys { - help := prose(x.help) + for _, x := range k.Keys { + help := prose(x.Help) if help == "" { help = "See the section above." } - if len(x.examples) > 0 { + if len(x.Examples) > 0 { var ex []string - for _, e := range x.examples { + for _, e := range x.Examples { ex = append(ex, code(e)) } help += " Also: " + strings.Join(ex, ", ") + "." } v := "" - if x.value != "" { - v = code(x.value) + if x.Value != "" { + v = code(x.Value) } - fmt.Fprintf(&b, "| `%s` | %s | %s |\n", x.name, v, cell(help)) + fmt.Fprintf(&b, "| `%s` | %s | %s |\n", x.Name, v, cell(help)) } } b.WriteString("\n**See also:** [Configuration](configuration.md)\n") @@ -308,7 +125,7 @@ func main() { if len(os.Args) > 1 { out = os.Args[1] } - if err := os.WriteFile(out, []byte(reference(parse(string(sample)))), 0o644); err != nil { + if err := os.WriteFile(out, []byte(reference(confdoc.Parse(string(sample)))), 0o644); err != nil { log.Fatal(err) } }