@@ -46,38 +46,217 @@ func runCommand(cmd *cobra.Command, args []string, fn func(ctx *RunContext) erro
4646 return fn (ctx )
4747}
4848
49- // PrintList prints items as a table and appends a "Showing N results (page P, total T)." footer.
50- // In structured mode the footer is suppressed to keep stdout byte-pure for
51- // jq/toon pipelines, so a page with more rows beyond it is announced on
52- // stderr instead — without it a consumer sees a partial page
53- // (e.g. the default --limit 20 of a far larger total) as the whole set. The
54- // judgment accounts for the page offset: on the last page
55- // ((page-1)*limit+count reaches total) there is no rest, so no note.
56- func (ctx * RunContext ) PrintList (items any , cols []output.Column , count , page , limit , total int ) error {
57- if err := ctx .Printer .Print (items , cols ); err != nil {
58- return err
49+ // moreState is what a list response says about whether rows exist beyond the
50+ // ones returned.
51+ type moreState int8
52+
53+ const (
54+ moreUnknown moreState = iota // the API gave no signal
55+ moreNo // has_next_page=false / empty next cursor
56+ moreYes // has_next_page=true / non-empty next cursor
57+ )
58+
59+ // moreOf maps a response's has_next_page flag to a moreState.
60+ func moreOf (hasNext bool ) moreState {
61+ if hasNext {
62+ return moreYes
5963 }
60- if ctx .Structured () {
61- if (page - 1 )* limit + count < total {
62- _ , _ = fmt .Fprintf (ctx .Cmd .ErrOrStderr (), "note: showing %d of %d total results (page %d); raise --limit or use --page for the rest\n " , count , total , page )
64+ return moreNo
65+ }
66+
67+ // pageBase says how a command numbers its pages. The zero value means the
68+ // command has no page notion at all.
69+ type pageBase int8
70+
71+ const (
72+ pageNone pageBase = iota // no page number (top-N lists, one-shot lists)
73+ pageOne // pages start at 1
74+ pageZero // pages start at 0
75+ pageUnknown // pages by number, base not declared by the API schema
76+ )
77+
78+ // listDefaults is what an API's request schema declares for a list: the page
79+ // size served when the request sets none (0 when undeclared) and how pages are
80+ // numbered.
81+ type listDefaults struct {
82+ Limit int
83+ PageBase pageBase
84+ }
85+
86+ // listDefaultsFor returns the schema-declared defaults of an SDK Service.Method.
87+ func listDefaultsFor (service , method string ) listDefaults {
88+ return listDefaultsBySDKMethod [service + "." + method ]
89+ }
90+
91+ // listPage is everything a list response tells us about its own completeness.
92+ // It is the single input to the completeness statement every list output makes,
93+ // so a partial page is never mistaken for the whole set.
94+ type listPage struct {
95+ Count int // rows shown
96+ Page int // page number as sent to the API (0 when the request set none)
97+ Base pageBase // how Page is numbered
98+ Limit int // effective page size: requested, else the schema default; 0 when unknown
99+ Total int // total matching rows; negative when the API returned none
100+ More moreState // has_next_page / next cursor, when the API returned one
101+ }
102+
103+ // totalKnown reports whether Total is a usable server total. A total below the
104+ // shown count is an absent field, not a real total.
105+ func (p listPage ) totalKnown () bool { return p .Total >= 0 && p .Total >= p .Count }
106+
107+ // offset returns how many rows precede this page, when that is knowable: always
108+ // for the first page, and for later pages only with a known page size and base.
109+ func (p listPage ) offset () (int , bool ) {
110+ switch {
111+ case p .Base == pageNone , p .Page == 0 && p .Base != pageOne , p .Base == pageOne && p .Page <= 1 :
112+ return 0 , true
113+ case p .Limit <= 0 :
114+ return 0 , false
115+ case p .Base == pageOne :
116+ return max (p .Page - 1 , 0 ) * p .Limit , true
117+ case p .Base == pageZero :
118+ return p .Page * p .Limit , true
119+ }
120+ return 0 , false
121+ }
122+
123+ // nextPage returns the page number after this one, when the base is known.
124+ func (p listPage ) nextPage () (int , bool ) {
125+ if p .Base != pageOne && p .Base != pageZero {
126+ return 0 , false
127+ }
128+ if p .Base == pageOne {
129+ return max (p .Page , 1 ) + 1 , true
130+ }
131+ return p .Page + 1 , true
132+ }
133+
134+ // results renders a row count with its noun.
135+ func results (n int ) string {
136+ if n == 1 {
137+ return "1 result"
138+ }
139+ return fmt .Sprintf ("%d results" , n )
140+ }
141+
142+ // statement applies the completeness rules in order and returns the table
143+ // footer and the structured-mode stderr note ("" when no warning applies):
144+ //
145+ // a. total known: "showing N of T", plus a next-page hint when rows remain;
146+ // b. else has_next_page/cursor known: true warns, false is complete;
147+ // c. else a full page (N == limit) cannot tell: warn that more may exist;
148+ // d. else only the plain count.
149+ //
150+ // A count is never labelled a total, and "rows remain" is claimed from a page
151+ // offset only when that offset is known.
152+ func (p listPage ) statement (cmd * cobra.Command ) (footer , note string ) {
153+ pageText := ""
154+ if p .Base != pageNone && (p .Page > 0 || p .Base == pageOne || p .Base == pageZero ) {
155+ pageText = fmt .Sprintf ("page %d" , p .Page )
156+ }
157+ paren := func (extra string ) string {
158+ switch {
159+ case pageText != "" && extra != "" :
160+ return " (" + pageText + ", " + extra + ")"
161+ case pageText != "" :
162+ return " (" + pageText + ")"
163+ case extra != "" :
164+ return " (" + extra + ")"
63165 }
64- return nil
166+ return ""
65167 }
66- _ , _ = fmt .Fprintf (ctx .Writer , "Showing %d results (page %d, total %d).\n " , count , page , total )
67- return nil
168+ hint := moreHint (cmd , p )
169+ // warn builds the footer and note for a partial list: "<shown>, <why>: <how>".
170+ warn := func (shown , why string ) (string , string ) {
171+ body := "showing " + shown
172+ if why != "" {
173+ body += ", " + why
174+ }
175+ if hint != "" {
176+ body += ": " + hint
177+ }
178+ return "S" + body [1 :] + "." , "note: " + body
179+ }
180+ switch {
181+ case p .totalKnown ():
182+ footer = fmt .Sprintf ("Showing %s%s." , results (p .Count ), paren (fmt .Sprintf ("total %d" , p .Total )))
183+ if off , ok := p .offset (); ok && off + p .Count < p .Total {
184+ body := fmt .Sprintf ("showing %d of %d total results%s" , p .Count , p .Total , paren ("" ))
185+ if hint != "" {
186+ body += ": " + hint
187+ }
188+ note = "note: " + body
189+ }
190+ return footer , note
191+ case p .More == moreYes :
192+ return warn (results (p .Count )+ paren ("" ), "more exist" )
193+ case p .More == moreNo :
194+ return fmt .Sprintf ("Showing %s%s." , results (p .Count ), paren ("" )), ""
195+ case p .Limit > 0 && p .Count == p .Limit :
196+ return warn (results (p .Count )+ paren (fmt .Sprintf ("= limit %d" , p .Limit )), "more may exist" )
197+ }
198+ return fmt .Sprintf ("Showing %s%s." , results (p .Count ), paren ("" )), ""
68199}
69200
70- // PrintTotal prints items as a table and appends a "Total: N" footer.
71- func (ctx * RunContext ) PrintTotal (items any , cols []output.Column , total int ) error {
201+ // moreHint names the flags this command actually has for reaching further rows.
202+ // A page number is offered only when the page base is known.
203+ func moreHint (cmd * cobra.Command , p listPage ) string {
204+ has := func (name string ) bool { return cmd .Flags ().Lookup (name ) != nil }
205+ paged := p .Base != pageNone && has ("page" )
206+ next , nextKnown := p .nextPage ()
207+ pageFlag := "--page"
208+ if nextKnown {
209+ pageFlag = fmt .Sprintf ("--page %d" , next )
210+ }
211+ switch {
212+ case paged && has ("limit" ):
213+ return "raise --limit or use " + pageFlag
214+ case paged :
215+ return "use " + pageFlag
216+ case has ("search-after-ctx" ):
217+ return "pass the response's search_after_ctx as --search-after-ctx"
218+ case has ("cursor" ):
219+ return "pass the response's next_cursor as --cursor"
220+ case has ("limit" ):
221+ return "raise --limit"
222+ }
223+ return ""
224+ }
225+
226+ // announce states how complete the list just printed is: a footer on stdout in
227+ // table mode, a stderr note (only when rows may be missing) in structured mode
228+ // so stdout stays byte-pure for jq/toon pipelines.
229+ func (ctx * RunContext ) announce (pg listPage ) {
230+ if ctx .Structured () {
231+ ctx .noteIncomplete (pg )
232+ return
233+ }
234+ footer , _ := pg .statement (ctx .Cmd )
235+ _ , _ = fmt .Fprintln (ctx .Writer , footer )
236+ }
237+
238+ // noteIncomplete writes the stderr note when rows may be missing. Commands that
239+ // own their output format (session list) call it directly.
240+ func (ctx * RunContext ) noteIncomplete (pg listPage ) {
241+ if _ , note := pg .statement (ctx .Cmd ); note != "" {
242+ _ , _ = fmt .Fprintln (ctx .Cmd .ErrOrStderr (), note )
243+ }
244+ }
245+
246+ // PrintPage prints items and then states how complete they are (see listPage).
247+ func (ctx * RunContext ) PrintPage (items any , cols []output.Column , pg listPage ) error {
72248 if err := ctx .Printer .Print (items , cols ); err != nil {
73249 return err
74250 }
75- if ! ctx .Structured () {
76- _ , _ = fmt .Fprintf (ctx .Writer , "Total: %d\n " , total )
77- }
251+ ctx .announce (pg )
78252 return nil
79253}
80254
255+ // PrintList is PrintPage for a response that carried a total.
256+ func (ctx * RunContext ) PrintList (items any , cols []output.Column , count , page , limit , total int ) error {
257+ return ctx .PrintPage (items , cols , listPage {Count : count , Page : page , Base : pageOne , Limit : limit , Total : total })
258+ }
259+
81260// WriteResult prints a success message as plain text or JSON.
82261func (ctx * RunContext ) WriteResult (message string ) {
83262 writeResult (ctx .Writer , message )
0 commit comments