Player Steps (DSL)

Choreograph interactive code demos with simple, readable steps.

The Player reads an array of “step” objects and executes them in order. Steps can insert or edit code, search and highlight text, show popovers/toasts, wait for input, branch with questions, and more.

Demo

																		// Save
                              const state = api.getState();
                              localStorage.setItem('snipT_state', JSON.stringify(state));
                              // Restore
                              const saved = localStorage.getItem('snipT_state');
                              if (saved) await api.setStateJSON(saved, {
                              mergeOptions: true,
                              applyContent: true,
                              reSearch: true,
                              allowFullscreen: false
                              });
																		

Step Object


A minimal setup looks like this:
const steps = [ { action:'clear' }, { action:'set', args:{ code:'\n...', typed:true } }, { waitMs: 500 }, // delay after the previous step { action:'searchThenPopover', args:{ query:'footer', options:{ inline:true, inlineHL:true, viewOffsetPx:64 }, popover:{ title:'Heads up', content:'Here is the footer', direction:'bottom' } } }, { pause:true }, // pause the Player and wait for user resume { action:'append', args:{ code:'\n', typed:true } }, ]; api.loadSteps(steps, { autoplay:false }); // add controls to the footer // Start when ready: // api.playerPlay();

Each entry in the steps array can contain one or more of the following keys:

Field Type / Default Description
label string
= ''
Optional name. Jump with playerGoto('label') or from question.
action string
= —
Operation to run (see Actions).
args object
= {}
Parameters for the action.
typed boolean
= false
For editing actions, animates typing at typingCPS.
waitBeforeMs number
= 0
Delay before this step (alias waitMsBefore).
waitMs number
= 0
Delay after this step (alias waitAfterMs).
pause boolean
= false
Pause the Player at the end of the step.
toast string | { text, type?, ms?, position? }
= —
Show a toast together with this step.
popup { target?, title?, content?, direction?, autoClose? }
= —
Show a popover at end of step (e.g. near current search hit).
question { title?, text, yes?: { goto }, no?: { goto } }
= —
Ask Yes/No and jump by index (0-based) or label.
note string
= ''
Optional learner note (shown if playerShowNotes is enabled).

Actions


Below are the most common actions you can orchestrate. (All line numbers are 1-based; column offsets are 0-based. For ranges, to is exclusive.)

Action Args (shape) Description
'set' { code: string, typed?: boolean } Replace the entire content (optionally typed).
'append' { code: string, typed?: boolean } Append text at the end.
'clear' {} Clear the document.
'delete' { line: number, from: number, to: number, typed?: boolean } Delete a span within a line.
'cursorInsert' { code: string, typed?: boolean } Insert at the current cursor.
'search' { query: string, options?: { caseSensitive?, regex?, wholeWord?, inline?, inlineHL?, viewOffsetPx? } } Search and highlight; returns matching lines count.
'searchThenPopover' { query: string, options?: SearchOptions, popover?: { title?, content?, direction?, autoClose? } } Async search then popover anchored to the current hit.
'wait' { ms: number } Explicit delay step.
'waitForTap' { text?: string, direction?: 'top'|'right'|'bottom'|'left' } Pause until the user taps/clicks the hint bubble.
'toast' { text: string, type?: 'info'|'success'|'warning'|'error', ms?: number, position?: 'top'|'bottom' } Show a toast (you can also use the step-level toast field).

Content & cursor

set
Replace the entire editor content.
{ action:'set', args:{ code:'...whole file...', typed:true } }
append
Append text at the end of the document.
{ action:'append', args:{ code:'\n', typed:true } }
clear
Clear the document.
{ action:'clear' }
delete
Remove a span on a given line.
{ action:'delete', args:{ line:6, from:12, to:22, typed:true } }
cursorInsert
Insert at the current cursor (or at a specified point, if supported).
{ action:'cursorInsert', args:{ code:'new footer', typed:true } }

Search & navigation

search
Highlight matches; returns number of matching lines.
{ action:'search', args:{ query:'TODO', options:{ inline:true, inlineHL:true, viewOffsetPx:64 } } }
searchThenPopover
Perform an async search, then open a popover anchored to the current hit.
{ action:'searchThenPopover', args:{ query:'router', options:{ inline:true, inlineHL:true, viewOffsetPx:64 }, popover:{ title:'Found', content:'Here is the router', direction:'bottom' } } }

Waiting & interaction

wait
Explicit wait.
{ action:'wait', args:{ ms: 1000 } }
waitForTap
Pause until the user clicks/taps a hint bubble.
{ action:'waitForTap', args:{ text:'Tap to continue…', direction:'bottom' } }
pause
Mark a step as a pause point.
{ pause:true }
question
Ask and branch.
{ question:{ title:'Review', text:'Do you want to see the footer edit again?', yes:{ goto: 3 }, // go back to the search step (index 3) no:{ goto: 'end' } // or jump to a labeled step } }

Messages & overlays

toast
Show a toast (you can also use the top-level toast field).
{ action:'toast', args:{ text:'Step done', type:'info' } }
popup
You can show a popover via searchThenPopover, or attach a top-level popup to a step (see “Final step” in the long example below).
{ label:'end', popup:{ target:'search', title:'done', content:'Rows updated!', direction:'bottom', autoClose:2000 } }

Calling internal methods as actions


Besides the built-in action names (set, append, search, wait, etc.), the Player can also call any internal instance method by name. If an action name isn’t matched by the built-ins, the Player tries to invoke this[name] and forwards the provided arguments.

How arguments are passed:
  • Scalar arg → passed as the first positional argument (e.g., args: 8).
  • Array args → spread as positional arguments (e.g., args: [start, end, opts]).
  • Object arg → passed as a single options object (use when the method expects an object).
  • If the method doesn’t exist, a console warning is logged (player: unknow action).

// 1) Scroll helpers (single-parameter methods) // Scroll down 8 visual lines { action: 'scrollDown', args: 8, note: 'Scroll down 8 lines.' }, // Scroll up 4 lines (equivalent array form) { action: 'scrollUp', args: [4], note: 'Scroll up 4 lines.' }, // Use the method default (1 line) by passing no args { action: 'scrollDown', note: 'Scroll down by the default (1 line).' }, // 2) Multi-parameter method via array // focusLines(start, end, opts) { action: 'focusLines', args: [12, 20, { className: 'pxc-dimmed' }], note: 'Dim everything except lines 12–20.' }, // 3) Method that takes an object // setHeights({ height?, maxHeight? }) { action: 'setHeights', args: { maxHeight: '380px' }, note: 'Set a scrollable max height.' }, // 4) Any other internal method by name { action: 'setFontSize', args: 16, note: 'Set base font size to 16px.' }

Tip


prefer numbers or arrays for methods that take positional parameters (e.g., scrollDown(lines?: number)). Use an object only when the method signature expects a single options object (e.g., setHeights({...})).

Timing patterns (three ways to delay)

// Delay AFTER an action { action:'set', args:{ code:'first…', typed:true }, waitMs: 1000 }, // Delay BEFORE an action { action:'set', args:{ code:'second…', typed:true }, waitBeforeMs: 600 }, // Explicit wait step { action:'wait', args:{ ms: 1000 } },

Prefer a small global cool-down (playerWaitAfterActionMs) over many micro-delays, and use pause:true or waitForTap for learner-driven pacing.

Delay after the action
{ action:'set', args:{ code:'first…', typed:true }, waitMs: 1000 }
Delay before the action
{ action:'set', args:{ code:'second…', typed:true }, waitBeforeMs: 600 }
Explicit wait step
{ action:'wait', args:{ ms: 1000 } }

You can also use a global “cool-down” after each step via the option playerWaitAfterActionMs.

Player Controls (API)


You can drive the Player programmatically or leave it to the on-screen controls. The core methods are:

api.playerPlay()
Start playback from the first step (or restart after a stop).
api.loadSteps(steps, { autoplay:false }); api.playerPlay();
api.playerResume()
Resume from a paused state (e.g. after pause:true or waitForTap).
api.playerPause(); // ...later: api.playerResume();
api.playerPause()
Pause playback at the current step.
api.playerStop()
Stop and reset the run (popovers close if playerClosePopoverOnStop is true).
api.playerNext()
Advance to the next step (handy while authoring).
api.playerPrev()
Go back one step.
api.playerGoto(indexOrLabel: number | string)
Jump to a step by 0-based index or by label (great with question).
api.playerGoto('review'); // by label api.playerGoto(3); // by index
api.setOption(key: string, value: any)
Use to tweak a single Player setting on the fly—ideal for reacting to user input (e.g., show/hide transport buttons, reveal notes, toggle looping). Most UI-related changes apply immediately; timing knobs (like playerWaitAfterActionMs) affect subsequent steps. Note that playerAutoplay is read when you call api.loadSteps(...)—set it before loading steps (to start right now, call api.playerPlay()).
// Toggle UI bits instantly api.setOption('playerButtons', false); api.setOption('playerProgress', true); api.setOption('playerShowNotes', true); // Adjust pacing for upcoming steps api.setOption('playerWaitAfterActionMs', 200); api.setOption('playerLoop', true); api.setOption('playerClosePopoverOnStop', true); // Autoplay is latched at loadSteps time api.setOption('playerAutoplay', true); api.loadSteps(steps); // picks up autoplay // or start now: api.playerPlay();
api.setOptions(opts: Partial<Options>)
Use to batch-configure the Player’s behavior and UI in one, safe call.
api.setOptions({ playerButtons: true, playerProgress: true, playerShowNotes: true, playerLoop: false, playerWaitAfterActionMs: 200, playerClosePopoverOnStop: true }); // If you also want auto-start on next load: api.setOptions({ playerAutoplay: true }); // effective on next api.loadSteps(...)

Player Options

Option Type / Default Description
playerAutoplay boolean
= false
Start automatically after loadSteps.
playerLoop boolean
= false
Loop back to the first step when finished.
playerButtons boolean
= true
Show transport controls.
playerProgress boolean
= true
Show step counter/progress.
playerShowNotes boolean
= true
Display per-step note.
playerWaitAfterActionMs number
= 0
Global delay after each action.
playerClosePopoverOnStop boolean
= true
Close popovers when stopping.
playerStopHoldMs number
= 350
Press-and-hold duration for Stop.

Loading steps and Player options


Attach your steps and optionally tweak the Player behavior:

api.loadSteps(steps, { autoplay: false, // start automatically // The following are configured as editor options too: // playerButtons: true, // show transport buttons // playerProgress: true, // show step counter/progress // playerShowNotes: true, // display per-step 'note' when available // playerLoop: false, // loop to the first step when finished // playerWaitAfterActionMs: 0, // global delay after each step // playerClosePopoverOnStop: true, // tidy up when stopping });

Authoring


  • Use typed:true for show-don’t-tell moments.
    It animates inserts/deletes with the configured typing speed (typingCPS).
  • Prefer searchThenPopover when you want to talk about a specific piece of code.
    It finds the text, scrolls to it, and anchors the popover precisely.
  • Branch with question.
    Let learners loop back to a prior step (goto by index or label) or move forward.
  • Mix delays sparingly.
    If you sprinkle many short waits, consider using a small global playerWaitAfterActionMs instead.
  • Use note for course text.
    When playerShowNotes is enabled, learners can reveal short explanations per step.
  • Debug faster with transport controls.
    While iterating, use playerPrev() / playerNext() and playerGoto('label') to jump around.

More examples

Build a ~20-line JS Program (with notes):
// Player: build a small JS program (fetch + timing) in readable steps const steps = [ { action:'clear', note:'Start from a blank document.', waitAfterMs:150 }, { action:'set', args:{ code:''use strict';\n// Demo: fetch a small JSON and time the request\n', typed:true }, note:'Insert the header and a short description.', waitAfterMs:200 }, { action:'append', args:{ code: class Timer { constructor(){ this.t0 = 0; this.t = 0; } start(){ this.t0 = performance.now(); } stop(){ this.t += performance.now() - this.t0; } reset(){ this.t = 0; } ms(){ return this.t.toFixed(1); } } , typed:true }, note:'Add a tiny Timer utility to measure elapsed time.', waitAfterMs:200 }, { action:'append', args:{ code: async function fetchJSON(url){ const res = await fetch(url); if(!res.ok) throw new Error(res.status + ' ' + res.statusText); return res.json(); } , typed:true }, note:'Helper that fetches JSON and throws on HTTP errors.', waitAfterMs:200 }, { action:'append', args:{ code: async function main(){ const t = new Timer(); t.start(); const data = await fetchJSON('https://jsonplaceholder.typicode.com/todos?_limit=5'); t.stop(); const done = data.filter(x => x.completed).length; console.log('Loaded ' + data.length + ' todos; ' + done + ' completed in ' + t.ms() + 'ms'); } , typed:true }, note:'main(): time the request, fetch 5 items, compute a stat, log a summary.', waitAfterMs:250 }, { action:'append', args:{ code: main().catch(err => { console.error('Request failed:', err.message); }); , typed:true }, note:'Call main() and handle errors.', waitAfterMs:250 }, { action:'searchThenPopover', args:{ query:'jsonplaceholder.typicode.com', options:{ inline:true, inlineHL:true, viewOffsetPx:64 }, popover:{ title:'Data source', content:'Using a public placeholder API.', direction:'bottom' } }, note:'Explain the URL being used.', waitAfterMs:400 }, { action:'toast', args:{ text:'Script ready — try it!', type:'success' }, note:'Friendly confirmation toast.' }, { pause:true, note:'Pause so readers can review before running.' } ]; api.loadSteps(steps, { autoplay:false, playerShowNotes:true }); // api.playerPlay();

Notes & Tips

  • Label key steps. Use label to create anchors (e.g. 'intro', 'edit', 'end') and jump via playerGoto or question.
  • Use searchThenPopover for precise callouts. It scrolls to the match and anchors the popover to the exact text.
  • Prefer a global cool-down. Set playerWaitAfterActionMs instead of sprinkling dozens of tiny waits.
  • Make edits readable. Add typed:true where the “show, don’t tell” effect helps learners.
  • Folds & goto. If a target line sits inside a fold, the Player anchors to the fold header; expand or use a search step to focus before editing.

Good to know


Steps are declarative and composable. Keep scripts small and labeled; mix delays for pacing, pauses for interaction, and questions for branching. You can show/hide player UI via options and drive everything programmatically with playerPlay, playerPause, playerNext, playerPrev, and playerGoto.