actions

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 12, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package actions provides the action handler system for mooncake.

Overview

The actions package defines a standard interface (Handler) that all action implementations must follow, along with a registry for discovering handlers at runtime.

Architecture

Actions are implemented as packages under internal/actions/. Each action package provides a Handler implementation that is registered globally on import via an init() function.

The executor looks up handlers from the registry based on the action type determined from the step configuration.

Backward Compatibility

This new system is designed to work alongside the existing action implementations. The Step struct retains all existing action fields (Shell, File, Template, etc.), and actions are migrated incrementally to the new Handler interface.

Creating a New Action

To create a new action handler:

1. Create a package under internal/actions/ (e.g., internal/actions/notify)

2. Implement the Handler interface:

type Handler struct{}

func (h *Handler) Metadata() actions.ActionMetadata {
    return actions.ActionMetadata{
        Name:           "notify",
        Description:    "Send notifications",
        Category:       actions.CategorySystem,
        SupportsDryRun: true,
    }
}

func (h *Handler) Validate(step *config.Step) error {
    // Validate step.Notify config
    return nil
}

func (h *Handler) Execute(ctx *executor.ExecutionContext, step *config.Step) (*executor.Result, error) {
    // Implement action logic
    return &executor.Result{Changed: true}, nil
}

func (h *Handler) DryRun(ctx *executor.ExecutionContext, step *config.Step) error {
    ctx.Logger.Infof("  [DRY-RUN] Would send notification")
    return nil
}

3. Register the handler in init():

func init() {
    actions.Register(&Handler{})
}

4. Import the package in the executor to ensure registration:

import _ "github.com/alehatsman/mooncake/internal/actions/notify"

Migration Strategy

Existing actions are being migrated incrementally:

  • Phase 1: Create Handler implementations for simple actions (print, vars)
  • Phase 2: Migrate complex actions (shell, file, template)
  • Phase 3: Migrate specialized actions (service, assert, preset)
  • Phase 4: Remove legacy code paths

During migration, both old and new implementations coexist. The executor checks if a handler is registered and prefers it, falling back to legacy implementations for non-migrated actions.

Package actions provides the handler interface and registry for mooncake actions.

The actions package defines a standard interface that all action handlers must implement, along with a registry system for discovering and dispatching to handlers at runtime.

To create a new action handler:

  1. Create a new package under internal/actions (e.g., internal/actions/notify)
  2. Implement the Handler interface
  3. Register your handler in an init() function
  4. The handler will be automatically available for use

Example:

package notify

import "github.com/alehatsman/mooncake/internal/actions"

type Handler struct{}

func init() {
    actions.Register(&Handler{})
}

func (h *Handler) Metadata() actions.ActionMetadata {
    return actions.ActionMetadata{
        Name:        "notify",
        Description: "Send notifications",
        Category:    actions.CategorySystem,
    }
}

// ... implement other interface methods

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Count

func Count() int

Count returns the number of handlers in the global registry.

func GetActionHint

func GetActionHint(actionName string, missingField string) string

GetActionHint generates a helpful hint for an action based on the schema. It includes: - Action description - Required parameters with descriptions - Optional parameters with descriptions - Examples where available

func GetFieldExample

func GetFieldExample(actionName, fieldName string) string

GetFieldExample returns an example value for a field based on schema

func Has

func Has(actionType string) bool

Has checks if a handler exists in the global registry.

func Register

func Register(handler Handler)

Register registers a handler in the global registry. This is the most common way to register handlers from init() functions. Panics if registration fails (e.g., duplicate handler name).

Example:

func init() {
    actions.Register(&MyHandler{})
}

Types

type Action

type Action string

Action names a primitive an Effect represents. Used in plan output, logging, and event emission. See Performer.

const (
	ActionMkdir     Action = "mkdir"
	ActionWriteFile Action = "write_file"
	ActionSymlink   Action = "symlink"
	ActionHardlink  Action = "hardlink"
	ActionTouch     Action = "touch"
	ActionRemove    Action = "remove"
	ActionChmod     Action = "chmod"
	ActionChown     Action = "chown"
	ActionRunCmd    Action = "run_command"
)

type ActionCategory

type ActionCategory string

ActionCategory groups related actions by their primary function.

const (
	// CategoryCommand represents actions that execute commands (shell, command)
	CategoryCommand ActionCategory = "command"

	// CategoryFile represents actions that manipulate files (file, template, copy, download)
	CategoryFile ActionCategory = "file"

	// CategorySystem represents system-level actions (service, assert, preset)
	CategorySystem ActionCategory = "system"

	// CategoryData represents data manipulation actions (vars, include_vars)
	CategoryData ActionCategory = "data"

	// CategoryNetwork represents network-related actions (download, http requests)
	CategoryNetwork ActionCategory = "network"

	// CategoryOutput represents output/display actions (print)
	CategoryOutput ActionCategory = "output"
)

type ActionDefinition

type ActionDefinition struct {
	Type        string                    `json:"type"`
	Description string                    `json:"description"`
	Properties  map[string]PropertySchema `json:"properties"`
	Required    []string                  `json:"required"`
}

ActionDefinition represents an action's schema definition

type ActionMetadata

type ActionMetadata struct {
	// Name is the action name as it appears in YAML (e.g., "shell", "file", "notify")
	Name string

	// Description is a human-readable description of what this action does
	Description string

	// Category groups related actions (command, file, system, etc.)
	Category ActionCategory

	// SupportsDryRun indicates whether this action can be executed in dry-run mode
	SupportsDryRun bool

	// SupportsBecome indicates whether this action supports privilege escalation (sudo)
	SupportsBecome bool

	// EmitsEvents lists the event types this action emits (e.g., "file.created", "notify.sent")
	EmitsEvents []string

	// Version is the action implementation version (semantic versioning)
	Version string

	// SupportedPlatforms lists the operating systems this action supports.
	// Valid values: "linux", "darwin", "windows", "freebsd", "openbsd", "netbsd", "dragonfly", "solaris", "aix"
	// Empty list means all platforms are supported
	SupportedPlatforms []string

	// RequiresSudo indicates whether this action typically requires elevated privileges.
	// This is informational - actual privilege requirements may vary based on the operation.
	RequiresSudo bool

	// ImplementsCheck indicates whether this action implements idempotency checks.
	// Actions with idempotency checks verify current state before making changes.
	ImplementsCheck bool
}

ActionMetadata describes an action type and its capabilities.

func List

func List() []ActionMetadata

List returns all handlers from the global registry.

type Context

type Context interface {
	// GetTemplate returns the template renderer for processing Jinja2-like templates.
	//
	// Use this to render:
	//   - Path strings with variables: "{{ home }}/{{ item }}"
	//   - Content with logic: "{% if os == 'linux' %}...{% endif %}"
	//   - Filters: "{{ path | expanduser }}"
	//
	// The renderer has access to all variables in scope (step vars, globals, facts).
	GetTemplate() template.Renderer

	// GetEvaluator returns the expression evaluator for conditions.
	//
	// Use this to evaluate:
	//   - when: "os == 'linux' && arch == 'amd64'"
	//   - changed_when: "result.rc == 0 and 'changed' in result.stdout"
	//   - failed_when: "result.rc != 0 and result.rc != 5"
	//
	// Returns interface{} which should be cast to bool for conditions.
	GetEvaluator() expression.Evaluator

	// GetLogger returns the logger for handler output.
	//
	// Use levels appropriately:
	//   - Infof: User-visible progress ("Installing package nginx")
	//   - Debugf: Detailed info ("Command: apt install nginx")
	//   - Warnf: Non-fatal issues ("File already exists, skipping")
	//   - Errorf: Failures ("Failed to create directory: permission denied")
	//
	// Output is formatted for TUI or text mode automatically.
	GetLogger() logger.Logger

	// GetVariables returns all variables in the current scope.
	//
	// Includes:
	//   - Step-level vars (defined in step.Vars)
	//   - Global vars (from vars actions)
	//   - System facts (os, arch, cpu_cores, memory_total_mb, etc.)
	//   - Registered results (from register: field on previous steps)
	//   - Loop context (item, item_index when in with_items/with_filetree)
	//
	// Keys are strings, values are interface{} (string, int, bool, []interface{}, map[string]interface{}).
	GetVariables() map[string]interface{}

	// GetEventPublisher returns the event publisher for observability.
	//
	// Emit events for:
	//   - State changes (EventFileCreated, EventServiceStarted)
	//   - Progress tracking (custom events for long operations)
	//   - Artifact generation (paths to created files)
	//
	// Events are consumed by:
	//   - Artifact collector (for rollback support)
	//   - External observers (CI/CD integrations)
	//   - Audit logs
	GetEventPublisher() events.Publisher

	// Mode reports the dispatch mode for this context. Handlers
	// implementing Runner consult this to decide whether to perform side
	// effects (ModeExecute) or only inspect state (ModePlan).
	Mode() Mode

	// Effects returns a Performer wired to this context. Handlers
	// implementing Runner should call Performer methods instead of os.*
	// directly so that ModePlan vs ModeExecute is decided in one place.
	// The returned Performer is cheap to construct and may be called
	// multiple times.
	Effects() Performer

	// GetCurrentStepID returns the unique ID of the currently executing step.
	//
	// Format: "step-{global_step_number}"
	//
	// Use this when:
	//   - Emitting events (so they're associated with the step)
	//   - Creating temporary files (include step ID to avoid conflicts)
	//   - Logging (though step ID is usually added automatically)
	GetCurrentStepID() string
}

Context provides the execution environment for action handlers.

Context is the primary interface through which handlers interact with the mooncake runtime. It provides access to:

  • Template rendering (Jinja2-like syntax with variables and filters)
  • Expression evaluation (when/changed_when/failed_when conditions)
  • Logging (structured output to TUI or text)
  • Variables (step vars, global vars, facts, registered results)
  • Event publishing (for observability and artifacts)
  • Execution mode (dry-run vs actual execution)

This interface avoids circular imports between actions and executor packages.

Example usage in a handler:

func (h *Handler) Execute(ctx actions.Context, step *config.Step) (actions.Result, error) {
    // Render template strings
    path, err := ctx.GetTemplate().RenderString(step.File.Path, ctx.GetVariables())

    // Log progress
    ctx.GetLogger().Infof("Creating file at %s", path)

    // Emit events for observability
    ctx.GetEventPublisher().Publish(events.Event{
        Type: events.EventFileCreated,
        Data: events.FileOperationData{Path: path},
    })

    // Return result
    result := executor.NewResult()
    result.SetChanged(true)
    return result, nil
}

type Effect

type Effect struct {
	Action      Action
	Path        string
	Reason      string
	Performed   bool
	WouldChange bool
	AlreadyOk   bool
	Err         error
	Detail      any
}

Effect is the result of a Performer call.

Field semantics by mode:

ModeExecute:
    Performed   true if a side effect actually happened
    AlreadyOk   true if the target was already in desired state (no-op)
    WouldChange unused (false)
    Err         any error from the underlying syscall / command

ModePlan:
    Performed   false (no side effects in plan mode)
    AlreadyOk   true if the target is already in desired state
    WouldChange true if applying ModeExecute would change state
    Err         any error encountered while *inspecting* state

Performed and WouldChange are mutually exclusive; AlreadyOk is set when the operation would be a no-op in either mode.

func (Effect) Changed

func (e Effect) Changed() bool

Changed reports whether this Effect represents a state change — either performed (ModeExecute) or predicted (ModePlan).

type Handler

type Handler interface {
	// Metadata returns metadata describing this action type.
	Metadata() ActionMetadata

	// Validate checks if the step configuration is valid for this action.
	// Called before Run to fail fast on configuration errors.
	Validate(step *config.Step) error

	// Run executes the action when ctx.Mode() is ModeExecute, or
	// inspects state and returns a prediction when ctx.Mode() is
	// ModePlan. Implementations:
	//
	//   - emit appropriate events via ctx.GetEventPublisher() (execute mode)
	//   - render templates via ctx.GetTemplate()
	//   - use ctx.GetLogger() for logging
	//   - return Result with Changed=true (execute) or
	//     WouldChange=true (plan) when state would change
	//   - route filesystem mutations through ctx.Effects() so that
	//     plan and execute modes share the same predicates
	//
	// Returns an error only on unrecoverable failure.
	Run(ctx Context, step *config.Step) (Result, error)
}

Handler defines the interface that all action handlers must implement.

A handler is responsible for:

  • Validating action configuration
  • Executing the action
  • Handling dry-run mode
  • Emitting appropriate events
  • Returning results

Handlers should be stateless - all execution state is passed via ExecutionContext.

Spec 16 collapsed the previous Execute / DryRun / Check trio into a single Run(ctx, step) method (the Runner interface). The legacy methods may still exist on concrete handler types — they are no longer part of the contract.

func Get

func Get(actionType string) (Handler, bool)

Get retrieves a handler from the global registry.

func NewHandlerFunc

func NewHandlerFunc(
	metadata ActionMetadata,
	validate func(*config.Step) error,
	execute func(Context, *config.Step) (Result, error),
	dryRun func(Context, *config.Step) error,
) Handler

NewHandlerFunc creates a Handler from function implementations.

type HandlerFunc

type HandlerFunc struct {
	// contains filtered or unexported fields
}

HandlerFunc is a function type that implements Handler for simple actions. This allows creating handlers without defining a new type.

func (*HandlerFunc) DryRun

func (h *HandlerFunc) DryRun(ctx Context, step *config.Step) error

func (*HandlerFunc) Execute

func (h *HandlerFunc) Execute(ctx Context, step *config.Step) (Result, error)

func (*HandlerFunc) Metadata

func (h *HandlerFunc) Metadata() ActionMetadata

func (*HandlerFunc) Run

func (h *HandlerFunc) Run(ctx Context, step *config.Step) (Result, error)

Run satisfies the Spec 16 Handler contract. In ModePlan it reports "not checkable" by default; in ModeExecute it delegates to the underlying execute function. HandlerFunc users wanting accurate plan-mode behavior should construct a typed Handler with its own Run method instead.

func (*HandlerFunc) Validate

func (h *HandlerFunc) Validate(step *config.Step) error

type Mode

type Mode int

Mode is the high-level dispatch mode for an action handler.

Spec 16 (docs-working/spec-16-unify-dryrun-execute.md) collapses the previous parallel non-mutating paths (Handler.DryRun and Handler.Check) into a single ModePlan. ModeExecute is the normal mutating path.

During the migration the legacy ExecutionContext.DryRun and CheckMode bools remain the source of truth; ExecutionContext.Mode() derives from them so new code can be written against Mode and migrated incrementally.

const (
	// ModeExecute performs real work: side effects, mutations, commands.
	ModeExecute Mode = iota
	// ModePlan inspects target state and predicts what would change.
	// No side effects. Replaces the legacy DryRun + Check methods.
	ModePlan
)

func (Mode) String

func (m Mode) String() string

String returns a stable human-readable name for the mode.

type Performer

type Performer interface {
	// Mode reports the mode the Performer will use for the next call.
	Mode() Mode

	// Mkdir ensures a directory exists at path with the given mode.
	// Parents are created as needed.
	Mkdir(path string, mode os.FileMode, opts PerformerOpts) Effect

	// WriteFile writes content to path with the given mode, creating
	// parent directories as needed. Idempotent: if existing content
	// already matches, AlreadyOk is set.
	WriteFile(path string, content []byte, mode os.FileMode, opts PerformerOpts) Effect

	// Symlink creates a symbolic link at path pointing to target. If a
	// link already exists with the correct target, AlreadyOk is set.
	Symlink(target, path string, opts PerformerOpts) Effect

	// Hardlink creates a hard link at path pointing to target.
	Hardlink(target, path string, opts PerformerOpts) Effect

	// Touch updates mtime, creating an empty file with the given mode
	// if absent. Not idempotent: WouldChange is always true in ModePlan.
	Touch(path string, mode os.FileMode, opts PerformerOpts) Effect

	// Remove deletes path. If recursive is true, removes directories
	// and their contents.
	Remove(path string, recursive bool, opts PerformerOpts) Effect

	// Chmod sets the permission bits on path.
	Chmod(path string, mode os.FileMode, opts PerformerOpts) Effect

	// Chown sets owner and group on path. Empty owner or group leaves
	// that side unchanged. Owner/group may be names or numeric IDs.
	Chown(path, owner, group string, opts PerformerOpts) Effect
}

Performer executes filesystem and command primitives in either ModeExecute (real side effects) or ModePlan (state inspection only, returning a prediction).

Spec 16 introduces Performer so that mutating primitives have exactly one site that decides what each operation means in each mode. Handlers should call Performer methods (via ctx-supplied accessor) instead of calling os.* directly.

type PerformerOpts

type PerformerOpts struct {
	Become     bool
	BecomeUser string
}

PerformerOpts carries optional flags that apply to most Performer calls.

Today the only supported option is Become — when set, the underlying primitive runs via sudo (with the password supplied to the Performer implementation). Handlers read Step.Become and pass it through; they should not shell out to sudo themselves.

type PropertySchema

type PropertySchema struct {
	Type        string `json:"type"`
	Description string `json:"description"`
	Pattern     string `json:"pattern,omitempty"`
}

PropertySchema represents a property's schema

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry manages registered action handlers through a thread-safe map.

The registry pattern enables:

  1. Dynamic action discovery - handlers register themselves via init()
  2. Loose coupling - executor doesn't import all action packages
  3. Extensibility - new actions added without changing executor
  4. Thread safety - concurrent access from multiple goroutines

Registration flow:

  1. Action package imports actions: import "github.com/.../internal/actions"
  2. Action package defines handler: type Handler struct{}
  3. Action package registers in init(): func init() { actions.Register(&Handler{}) }
  4. Main imports register package: import _ "github.com/.../internal/register"
  5. Register package imports all actions: import _ ".../actions/shell"
  6. All handlers automatically registered before main() runs

Lookup flow:

  1. Executor determines action type from step: actionType := step.DetermineActionType()
  2. Executor queries registry: handler, ok := actions.Get(actionType)
  3. If found, executor calls: handler.Validate(step), handler.Execute(ctx, step)
  4. If not found, executor falls back to legacy implementation

This avoids circular imports because:

  • actions package defines Handler interface
  • action implementations (shell, file, etc.) import actions
  • executor imports actions but NOT action implementations
  • register package imports action implementations (triggers init())
  • cmd imports register (triggers all registrations)

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates a new action registry.

func (*Registry) Count

func (r *Registry) Count() int

Count returns the number of registered handlers.

func (*Registry) Get

func (r *Registry) Get(actionType string) (Handler, bool)

Get retrieves a handler by action type name. Returns the handler and true if found, nil and false otherwise.

func (*Registry) Has

func (r *Registry) Has(actionType string) bool

Has checks if a handler is registered for the given action type.

func (*Registry) List

func (r *Registry) List() []ActionMetadata

List returns metadata for all registered handlers. Useful for introspection and documentation generation.

func (*Registry) Register

func (r *Registry) Register(handler Handler) error

Register adds a handler to the registry. This is typically called from init() functions in action packages. Returns an error if a handler with the same name is already registered.

type Result

type Result interface {
	// SetChanged marks whether this action modified system state.
	//
	// Set to true if the action:
	//   - Created/modified/deleted files or directories
	//   - Started/stopped/restarted services
	//   - Installed/removed packages
	//   - Executed commands that changed state
	//
	// Set to false if the action:
	//   - Found state already as desired (idempotent)
	//   - Only read/queried information
	//   - Failed before making changes
	//
	// Changed count is reported in run summary and used for idempotency tracking.
	SetChanged(changed bool)

	// SetStdout captures standard output from the action.
	//
	// Used primarily by shell/command actions. Output is:
	//   - Available in registered results as {{ result.stdout }}
	//   - Shown in TUI output view
	//   - Logged to artifacts
	//   - Used in changed_when/failed_when expressions
	SetStdout(stdout string)

	// SetStderr captures standard error from the action.
	//
	// Used primarily by shell/command actions. Error output is:
	//   - Available in registered results as {{ result.stderr }}
	//   - Shown in TUI output view (usually in red)
	//   - Logged to artifacts
	//   - Used in changed_when/failed_when expressions
	SetStderr(stderr string)

	// SetFailed marks the result as failed.
	//
	// Usually you should return an error instead of calling this. Use this when:
	//   - The action completed but didn't achieve desired state
	//   - failed_when expression evaluated to true
	//   - Assertion failed (assert action)
	//
	// Failed steps:
	//   - Increment failure count in run summary
	//   - Stop execution (unless ignore_errors: true)
	//   - Are highlighted in TUI
	SetFailed(failed bool)

	// SetData attaches custom data to the result.
	//
	// Data becomes available when the result is registered via register: field.
	//
	// Example:
	//
	//	result.SetData(map[string]interface{}{
	//	    "checksum": "sha256:abc123",
	//	    "size_bytes": 1024,
	//	    "format": "json",
	//	})
	//
	// Then in subsequent steps:
	//	  when: myfile.checksum == "sha256:abc123"
	//	  shell: echo "File size: {{ myfile.size_bytes }}"
	//
	// Keys should be snake_case. Values should be JSON-serializable.
	SetData(data map[string]interface{})

	// RegisterTo registers this result to the variables map.
	//
	// Called automatically by the executor when a step has register: field.
	// Creates a map in variables with:
	//   - changed: bool
	//   - failed: bool
	//   - stdout: string (if set)
	//   - stderr: string (if set)
	//   - rc: int (if applicable)
	//   - ...custom data from SetData()
	//
	// Handlers typically don't call this directly.
	RegisterTo(variables map[string]interface{}, name string)
}

Result represents the outcome of an action execution.

Results track:

  • Whether changes were made (for idempotency reporting)
  • Output data (stdout/stderr from commands)
  • Success/failure status
  • Custom data (for result registration)

Results can be registered to variables for use in subsequent steps via the register: field.

Example:

result := executor.NewResult()
result.SetChanged(true)  // File was created/modified
result.SetData(map[string]interface{}{
    "path": "/etc/myapp/config.yml",
    "size": 1024,
    "checksum": "sha256:abc123...",
})

// If step has register: myfile, data is available as:
// {{ myfile.changed }} = true
// {{ myfile.path }} = "/etc/myapp/config.yml"

This interface avoids circular imports between actions and executor packages.

type Runner

type Runner interface {
	Run(ctx Context, step *config.Step) (Result, error)
}

Runner is the unified handler entry point introduced by Spec 16. The Handler interface now embeds Run as a required method; Runner remains as a named type alias for clarity in call sites that want to express "the Run capability" specifically.

type Schema

type Schema struct {
	Definitions map[string]ActionDefinition `json:"definitions"`
}

Schema represents the JSON schema structure

Directories

Path Synopsis
Package artifact_capture implements the artifact.capture action handler.
Package artifact_capture implements the artifact.capture action handler.
Package artifact_validate implements the artifact.validate action handler.
Package artifact_validate implements the artifact.validate action handler.
Package assert implements the assert action handler.
Package assert implements the assert action handler.
Package command implements the command action handler.
Package command implements the command action handler.
Package copy implements the copy action handler.
Package copy implements the copy action handler.
Package download implements the download action handler.
Package download implements the download action handler.
Package file implements the file action handler.
Package file implements the file action handler.
Package file_delete_range implements the file_delete_range action handler.
Package file_delete_range implements the file_delete_range action handler.
Package file_insert implements the file_insert action handler.
Package file_insert implements the file_insert action handler.
Package file_patch_apply implements the file_patch_apply action handler.
Package file_patch_apply implements the file_patch_apply action handler.
Package file_replace implements the file_replace action handler.
Package file_replace implements the file_replace action handler.
Package package_handler implements the package action handler.
Package package_handler implements the package action handler.
Package preset implements the preset action handler.
Package preset implements the preset action handler.
Package print implements the print action handler.
Package print implements the print action handler.
Package repo_apply_patchset implements the repo_apply_patchset action handler.
Package repo_apply_patchset implements the repo_apply_patchset action handler.
Package repo_search implements the repo_search action handler.
Package repo_search implements the repo_search action handler.
Package repo_tree implements the repo_tree action handler.
Package repo_tree implements the repo_tree action handler.
Package service implements the service action handler.
Package service implements the service action handler.
Package shell implements the shell action handler.
Package shell implements the shell action handler.
Package template implements the template action handler.
Package template implements the template action handler.
Package testutil provides shared testing utilities for action handlers.
Package testutil provides shared testing utilities for action handlers.
Package unarchive implements the unarchive action handler.
Package unarchive implements the unarchive action handler.
Package vars implements the vars action handler.
Package vars implements the vars action handler.
Package wait implements the wait action handler.
Package wait implements the wait action handler.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL