Skip to main content
Scripts are methods that execute in response to user input (gestures). NVDA’s script system provides a powerful way to bind keyboard shortcuts, braille display buttons, and other input to custom functionality.

What are Scripts?

A script is a Python method that:
  • Has a name starting with script_
  • Takes a gesture parameter
  • Is bound to one or more input gestures
  • Can be assigned to a category for organization
Basic script structure
from scriptHandler import script
import ui

class MyObject(ScriptableObject):
    
    @script(
        description="Says hello to the user",
        gesture="kb:NVDA+h"
    )
    def script_sayHello(self, gesture):
        ui.message("Hello!")

What are Gestures?

A gesture is user input that can trigger a script:
  • Keyboard: kb:NVDA+t, kb:control+shift+a
  • Braille display: br(freedomScientific):leftWizWheelUp
  • Touch screen: ts:2finger_flickRight
  • Braille keyboard: bk:space+dot1+dot3
Gesture identifiers have the format:
<source>[(device)]:<key1>+<key2>+...

The Script Decorator

The modern way to define scripts uses the @script decorator from scriptHandler:
Script decorator syntax
from scriptHandler import script
import inputCore

class MyPlugin(GlobalPlugin):
    
    @script(
        # Description shown in input gestures dialog
        description="Reports the current date and time",
        
        # Category for organization
        category=inputCore.SCRCAT_MISC,
        
        # Single gesture
        gesture="kb:NVDA+shift+t",
        
        # Or multiple gestures
        gestures=["kb:NVDA+shift+t", "kb:NVDA+alt+t"],
        
        # Allow propagation to focus ancestors
        canPropagate=False,
        
        # Run even in input help mode
        bypassInputHelp=False,
        
        # Run even in sleep mode
        allowInSleepMode=False,
        
        # Resume say all after execution
        resumeSayAllMode=None,
        
        # Speak when speech mode is "on-demand"
        speakOnDemand=False
    )
    def script_sayDateTime(self, gesture):
        import datetime
        now = datetime.datetime.now()
        ui.message(f"{now.strftime('%I:%M %p, %A %B %d, %Y')}")

Decorator Parameters

description
string
required
Translatable description shown to users in the input gestures dialog
category
string
Category for grouping related scripts. Use constants from inputCore like:
  • SCRCAT_BROWSE - Browse mode
  • SCRCAT_MISC - Miscellaneous
  • SCRCAT_SPEECH - Speech
  • SCRCAT_SYSTEM - System
  • SCRCAT_CONFIG - Configuration
gesture
string
Single gesture identifier to bind
gestures
list
List of gesture identifiers to bind
canPropagate
boolean
default:"false"
Whether script applies to focus ancestor objects
bypassInputHelp
boolean
default:"false"
Whether script runs when input help is active
allowInSleepMode
boolean
default:"false"
Whether script runs in sleep mode
resumeSayAllMode
int
Say all mode to resume after script executes (from sayAll.CURSOR_* constants)
speakOnDemand
boolean
default:"false"
Whether script produces speech in “on-demand” mode

Gesture Binding Methods

Using decorator
from scriptHandler import script

class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    
    @script(
        description="Announce NVDA version",
        gesture="kb:NVDA+shift+v"
    )
    def script_announceVersion(self, gesture):
        import buildVersion
        ui.message(buildVersion.version)

Method 2: __gestures Dictionary

Using __gestures dictionary
class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    
    __gestures = {
        "kb:NVDA+shift+v": "announceVersion",
        "kb:NVDA+shift+t": "announceTime",
    }
    
    def script_announceVersion(self, gesture):
        """Announce NVDA version"""  # Used as description
        import buildVersion
        ui.message(buildVersion.version)
    
    def script_announceTime(self, gesture):
        """Announce current time"""
        import datetime
        ui.message(str(datetime.datetime.now().time()))
With __gestures, the docstring is used as the description. However, this makes it non-translatable unless you manually set the __doc__ attribute. The decorator approach is preferred.

Method 3: Dynamic Binding

Dynamic binding
class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    
    def __init__(self):
        super().__init__()
        # Bind gestures dynamically
        self.bindGesture("kb:NVDA+shift+v", "announceVersion")
    
    def script_announceVersion(self, gesture):
        import buildVersion
        ui.message(buildVersion.version)

Script Resolution Order

When a user presses a key, NVDA searches for a matching script in this order:
1

User Gesture Map

User’s custom key bindings from the input gestures dialog
2

Locale Gesture Map

Locale-specific gesture remappings
3

Braille Display Gestures

Gestures from the active braille display driver
4

Global Plugins

Scripts defined in loaded global plugins
5

App Module

Scripts in the app module for the active application
6

Tree Interceptor

Scripts in the tree interceptor (e.g., browse mode)
7

NVDA Object with Focus

Scripts on the focused NVDA Object
8

Focus Ancestors

Scripts on parent objects (if canPropagate=True)
9

Global Commands

Built-in NVDA commands
The first matching script is executed and the search stops. This allows plugins to override built-in commands by defining them earlier in the chain.

Real-World Examples

Example 1: Simple Global Command

From the NVDA Developer Guide:
Version announcement
# Version announcement plugin for NVDA
# Developer guide example 3

import globalPluginHandler
from scriptHandler import script
import ui
import buildVersion

class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    
    @script(gesture="kb:NVDA+shift+v")
    def script_announceNVDAVersion(self, gesture):
        ui.message(buildVersion.version)

Example 2: Window Information Scripts

From the NVDA Developer Guide:
Window utility scripts
# Window utility scripts for NVDA
# Developer guide example 4

import globalPluginHandler
from scriptHandler import script
import ui
import api

class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    
    @script(
        description="Announces the window class name of the current focus object",
        gesture="kb:NVDA+leftArrow"
    )
    def script_announceWindowClassName(self, gesture):
        focusObj = api.getFocusObject()
        name = focusObj.name
        windowClassName = focusObj.windowClassName
        ui.message(f"class for {name} window: {windowClassName}")
    
    @script(
        description="Announces the window control ID of the current focus object",
        gesture="kb:NVDA+rightArrow"
    )
    def script_announceWindowControlID(self, gesture):
        focusObj = api.getFocusObject()
        name = focusObj.name
        windowControlID = focusObj.windowControlID
        ui.message(f"Control ID for {name} window: {windowControlID}")

Example 3: Object-Specific Script

From the NVDA Developer Guide:
Enhanced edit field
import appModuleHandler
from scriptHandler import script
from NVDAObjects.IAccessible import IAccessible
import controlTypes
import ui

class AppModule(appModuleHandler.AppModule):
    
    def chooseNVDAObjectOverlayClasses(self, obj, clsList):
        if obj.windowClassName == "Edit" and obj.role == controlTypes.Role.EDITABLETEXT:
            clsList.insert(0, EnhancedEditField)

class EnhancedEditField(IAccessible):
    
    @script(gesture="kb:NVDA+l")
    def script_reportLength(self, gesture):
        """Report the number of characters in this edit field"""
        ui.message(f"{len(self.value)}")
This script only works when an edit field is focused because it’s defined on the EnhancedEditField class.

Example 4: Script with Multiple Gestures

Multiple gestures
class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    
    @script(
        description="Report current time",
        gestures=[
            "kb:NVDA+f12",
            "kb:NVDA+shift+t",
            "br(freedomScientific):leftWizWheelDown"
        ]
    )
    def script_reportTime(self, gesture):
        import datetime
        now = datetime.datetime.now()
        ui.message(now.strftime("%I:%M %p"))

Example 5: Script with Repeat Detection

Repeat detection
from scriptHandler import script, getLastScriptRepeatCount
import ui
import datetime

class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    
    @script(
        description="Report time/date",
        gesture="kb:NVDA+f12"
    )
    def script_reportTimeOrDate(self, gesture):
        """Report time on first press, date on second press"""
        now = datetime.datetime.now()
        
        if getLastScriptRepeatCount() == 0:
            # First press: report time
            ui.message(now.strftime("%I:%M %p"))
        else:
            # Second press: report date
            ui.message(now.strftime("%A, %B %d, %Y"))

Gesture Identifier Format

Keyboard Gestures

Keyboard gesture examples
# Format: kb[(layout)]:<modifiers>+<key>

"kb:NVDA+t"                 # NVDA key + t
"kb:control+shift+a"        # Ctrl+Shift+A
"kb:alt+f4"                 # Alt+F4
"kb(laptop):NVDA+t"         # Laptop layout specific
"kb:NVDA+shift+upArrow"     # With arrow keys
"kb:applications"           # Applications (context menu) key
"kb:escape"                 # Escape key

Braille Display Gestures

Braille display gestures
# Format: br(<driver>):<button>

"br(freedomScientific):leftWizWheelUp"
"br(alva.BC640):t3"
"br(baum):d1"
"br(brailliant):space+dot1+dot3"

Touch Screen Gestures

Touch gestures
# Format: ts:<gesture>

"ts:2finger_flickRight"
"ts:3finger_tap"
"ts:flickLeft"
"ts:tap"

Script Categories

Organize scripts into categories for the input gestures dialog:
Script categories
import inputCore

# Built-in categories
inputCore.SCRCAT_BROWSE      # Browse mode
inputCore.SCRCAT_SPEECH       # Speech
inputCore.SCRCAT_BRAILLE      # Braille
inputCore.SCRCAT_VISION       # Vision
inputCore.SCRCAT_FOCUS        # Focus
inputCore.SCRCAT_SYSTEM       # System
inputCore.SCRCAT_CONFIG       # Configuration
inputCore.SCRCAT_MISC         # Miscellaneous
inputCore.SCRCAT_KBEMU        # Keyboard emulation

# Or set on the class
class MyPlugin(globalPluginHandler.GlobalPlugin):
    scriptCategory = "My Plugin"
    
    @script(description="Do something")
    def script_doSomething(self, gesture):
        pass

Advanced Features

Propagating Scripts

Allow scripts to work on focus ancestors:
Propagating script
class MyDialog(NVDAObject):
    
    @script(
        description="Close dialog",
        gesture="kb:escape",
        canPropagate=True  # Works on focused children too
    )
    def script_closeDialog(self, gesture):
        # This runs even when a child control has focus
        ui.message("Closing dialog")
        # ... close logic

Bypassing Input Help

Bypass input help
class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    
    @script(
        description="Always-active script",
        gesture="kb:NVDA+escape",
        bypassInputHelp=True  # Runs even in input help mode
    )
    def script_emergency(self, gesture):
        # This runs even when input help (NVDA+1) is active
        ui.message("Emergency command")

Sleep Mode Scripts

Sleep mode script
class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    
    @script(
        description="Wake up NVDA",
        gesture="kb:NVDA+shift+s",
        allowInSleepMode=True  # Runs in sleep mode
    )
    def script_toggleSleep(self, gesture):
        # This can run when NVDA is in sleep mode
        pass

Gesture Pass-Through

Pass gesture to the application:
Pass-through
class AppModule(appModuleHandler.AppModule):
    
    @script(
        description="Conditional pass-through",
        gesture="kb:control+c"
    )
    def script_smartCopy(self, gesture):
        # Do custom processing first
        focusObj = api.getFocusObject()
        
        if shouldInterceptCopy(focusObj):
            # Do custom copy
            performCustomCopy()
        else:
            # Pass gesture to application
            gesture.send()

Working with Gesture Objects

The gesture parameter provides information about the input:
Gesture object
def script_example(self, gesture):
    # Gesture properties
    displayName = gesture.displayName  # Human-readable name
    identifiers = gesture.identifiers  # All matching identifiers
    
    # Send gesture to application
    gesture.send()
    
    # Check gesture type
    if gesture.isModifier:
        ui.message("Modifier key pressed")
    
    # Access keyboard-specific info
    if hasattr(gesture, 'vkCode'):
        vkCode = gesture.vkCode
        scanCode = gesture.scanCode

Unbinding Gestures

Remove gesture bindings:
Unbinding gestures
class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    
    def __init__(self):
        super().__init__()
        # Remove a gesture binding
        try:
            self.removeGestureBinding("kb:NVDA+t")
        except LookupError:
            pass  # Gesture wasn't bound
    
    # Or unbind in __gestures
    __gestures = {
        "kb:NVDA+t": None,  # Unbinds this gesture
    }

Best Practices

Script names should be clear and descriptive:
# Good
def script_announceWindowTitle(self, gesture):
    pass

# Bad
def script_dwt(self, gesture):
    pass
Write clear, translatable descriptions:
@script(
    # Good - clear and specific
    description="Reports the current date and time",
)
def script_reportDateTime(self, gesture):
    pass

# Bad - vague
# description="Does something"
Avoid conflicts with system or NVDA shortcuts:
# Good - uses NVDA key
gesture="kb:NVDA+shift+t"

# Bad - conflicts with copy
# gesture="kb:control+c"
Assign appropriate categories:
@script(
    category=inputCore.SCRCAT_MISC,
    description="My script"
)
def script_myScript(self, gesture):
    pass
Scripts should not crash:
def script_example(self, gesture):
    try:
        # Do work
        result = doSomething()
        ui.message(result)
    except Exception:
        log.exception("Error in script_example")
        ui.message("An error occurred")

Testing Scripts

Testing scripts
# 1. Enable developer scratchpad in NVDA settings
# 2. Create globalPlugins/myTest.py:

import globalPluginHandler
from scriptHandler import script
import ui
from logHandler import log

class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    
    @script(
        description="Test script",
        gesture="kb:NVDA+shift+f12"
    )
    def script_test(self, gesture):
        log.info("Test script executed")
        ui.message("Test!")

# 3. Reload plugins: NVDA menu → Tools → Reload plugins
# 4. Test the gesture
# 5. Check log: NVDA menu → Tools → View log

See Also

NVDA Objects

Define scripts on NVDA Objects

Events

React to system events

Writing Plugins

Create plugins with scripts

Architecture Overview

Understand the system design

Build docs developers (and LLMs) love