Tool of Thought

APL for the Practical Man

"Vibe coding since 1987"

ProgressBar Component Revisited

October 24, 2025

Way back in May of 2024 we made a first attempt at a ProgressBar component.

No battle plan survives first contact with the enemy, so now that one intrepid user is attempting some real work with Abacus, it's time to make the ProgressBar actually work.

While the operator approach is interesting and indeed useful, it does not allow us to easily integrate a progress bar into an existing codebase, especially one with looping control structures like :For, :While, and :Repeat. For this we need a Create function to create a progress bar, an Update function to update it inside the loop and to check for user action, and finally a Close function to get rid of it when done. For example if we know the number of iterations ahead of time, we might write something like:

p←d A.ProgressBar.Create ⊂'Iterations' 25
:For i :In ⍳25
     r←p A.ProgressBar.Update'Doing iteration: ',⍕1+i
     :If r≢'Resume' ⋄ :Leave ⋄ :EndIf
     ⎕DL 0.2
:EndFor
p A.ProgressBar.Close''
:Select r
:Case 'Cancel'
     z←d A.Alert'You canceled the operation.'
:Case 'Resume'
     z←d A.Alert'The operation completed.'
:EndSelect

Here the looping and logic is exposed and explicit. The result of Update is examined inside the loop on each iteration to determine if we should continue or not, and again after the loop to determine the final action. Of course this logic can be implemented many different ways.

If we don't know the number of iterations, we might write code like:

d A.ProgressBar.Create ''
t←0
:Repeat
    r←A.ProgressBar.Update'Total time in seconds elapsed: ',⍕t
    :If r≢'Resume' ⋄ :Leave ⋄ :EndIf
t+←⎕DL 0.2
:Until t>20
A.ProgressBar.Close''

We still provide the Run operator which has been significantly simplified, with one operator covering both the determinate and indeterminate cases:

Run←{
     p←New 1⊃⍵
     _←⍺ A.ShowModal p
     r←p ⍺⍺{
         s←((2=≡⍺.Status)/1+⍺.Iteration)⊃⍺.Status
         c←⍺ Update s
         c≡'Cancel':1 ⍺.Result
         c≡'Truncate':2 ⍺.Result
         ⍺.Result,←⊂⍺ ⍺⍺ ⍵
         ⍺.Done:0 ⍺.Result
         ⍺.Iteration=⍺.Iterations-1:0 ⍺.Result
         ⍺ ∇ ⍵
     }0⊃⍵
     r⊣A.DeleteElement p
 }

Both Create and Run cover the New function (passing through the right argument) :

New←{
     w←(
         'Title' 'Progress'
         'Iterations' 0
         'Truncate' 0
         'UpdatePeriod' 1
         'Width' '30rem'
         'Status' 'Working...'
         'LanguageTable'(0 2⍴⊂'')
         'DefaultLanguageTable'[
             'Pause' 'Pause'
             'Cancel' 'Cancel'
             'Resume' 'Resume'
             'Truncate' 'Truncate'
             'Paused' 'Paused'
         ]
     )A.Default ⍵
     w.Iterations←(2=≡w.Status)⊃w.Iterations(≢w.Status)
     s←A.New'p'
     s.id←'progress-bar-status'
     p←A.New'progress'
     p.id←'progress'
     p.max←w.Iterations
     p.value←'0'
     fp←A.New'p'(w A.Translate'Paused')
     fp.id←'progress-paused-indicator'
     fp.class←'pulse invisible'
     w.BodyContent←s,((0<w.Iterations)/p),fp
     w.Buttons←1 1 w.Truncate/w A.Translate'Cancel' 'Pause' 'Truncate'
     w.OnClose←A.FQP'OnClose'
     w.OnClick←A.FQP¨1 1 w.Truncate/'OnCancel' 'OnPause' 'OnTruncate'
     d←A.DialogBox.New w
     d.id←'progress-bar'
     d.(Iteration Result)←¯1 ⍬
     d.(Done Paused Canceled Truncated)←0
     f←2⊃d.Content
     f.Unqueued←'click'
     f.Content.id←1 1 w.Truncate/'progress-'∘,¨'cancel' 'pause' 'truncate'
     d.Unqueued←'close'
     d.PauseToken←⎕TID
     d
 }

Here we can see the various options. The Iterations property defaults to 0, which indicates that we don't know the number of iterations ahead of time. If 0, then no actual graphical bar is displayed, only a status message.

The Truncate property indicates whether or not a Truncate option is offered in addition to Cancel and Resume when the work is paused.

The UpdatePeriod property determines how often the progress bar should be updated. It defaults to 1, meaning at every iteration. For loops with large numbers of iterations and small amounts of work in each iteration, it may make sense to set this higher.

The Status property is either a simple character vector or a vector of vectors. This property is only applicable when the Run operator is used, as in the case of Create and Close, the Update function is called explicitly by the programmer and passed the status as its argument. More on this below.

Let's look a little closer at the Run operator, which has changed significantly. It is called like so:

      rc rv←d f ProgressBar.Run s p 

Here f is the iteration function, s is an appropriate right argument to f, and p is an appropriate right argument to ProgressBar.New, and d is just a reference to the document. The result rc is a return code of 0 for complete, 1 for cancel, and 2 for truncate. The result rv is a vector of the results of f, one item for each iteration.

The iteration function f is passed a reference to the progress bar as its left argument. This lets f do and access a few important things. First, for the indeterminate case, allows f to set the Done property to 1, to indicate that no more iterations are necessary, and end the process. Second, it allows f to set the Status property to update the progress bar text. Finally, it allows f to reference the current iteration number.

The right argument to f may be anything, It might be a simple file tie number, or it could be a namespace. If the latter, in addition to providing a nice way for many named inputs to f, it provides storage for accumulating or aggregating results between iterations. Note that the progress bar itself, the left argument, can also provide storage that survives iterations.

The iteration function f must have a result. This explicit result may or may not be significant or useful, as the important result may be implicitly available in s.

As noted, the Status property may be set by f on each iteration. However, when the number of iterations is known, the status property may be set at startup to a vector of vectors, one item for each iteration. This defines the number of iterations and overrides the value of the Iterations property. The Run operator then automatically updates the status on each iteration.

There is an additional major change to the UI. Instead of a single Pause button that then presents a ConfirmBox with 'Cancel', 'Resume', and Truncate buttons, we provide Cancel, Pause, and Truncate buttons on the main progress element, avoiding the additional modal element. This means that Cancel and Truncate are always available and execute without additional confirmation. (Note that if using Create/Update/Close rather than Run, the programmer is in complete control of the loop and can add confirmation boxes when desired.) The Pause button switches to Resume and back as necessary. We generally don't like buttons that change captions, and we may change this, but in this case it seems reasonable.

Finally, with this version of the ProgressBar component, we introduce the LanguageTable property. This provides a way for the programmer to specify any or all of the visible text elements of the component. This may be used for language support, or simply to change the label of a particular element to something more appropriate for the task at hand.

You can see many examples of the ProgressBar component in the CSVEditor application.