Skip to main content

Overview

Global Plugins allow you to add functionality that works across all applications. Unlike app modules which are specific to one application, global plugins are loaded when NVDA starts and remain active until NVDA exits.
Use global plugins for features that should be available everywhere, such as:
  • New navigation commands
  • System-wide information scripts
  • Custom braille features
  • OCR functionality
  • Cloud synchronization

Basic Global Plugin Structure

Global plugins must:
  1. Be placed in the globalPlugins directory
  2. Have a unique filename that describes their purpose
  3. Define a class called GlobalPlugin that inherits from globalPluginHandler.GlobalPlugin
# timeAnnouncement.py
# Global Plugin for enhanced time announcements

import globalPluginHandler
from scriptHandler import script
import ui
import datetime

class GlobalPlugin(globalPluginHandler.GlobalPlugin):

	@script(
		description="Announces the current time with seconds",
		gesture="kb:NVDA+f12"
	)
	def script_announceTimeDetailed(self, gesture):
		"""Announce time including seconds."""
		now = datetime.datetime.now()
		ui.message(now.strftime("%I:%M:%S %p"))

Lifecycle Methods

Global plugins have two key lifecycle methods:
__init__
method
Called when the plugin is loaded (NVDA startup).
def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    # Initialize your plugin
    self.myData = {}
terminate
method
Called when the plugin is being unloaded (NVDA exit).
def terminate(self):
    # Clean up resources
    super().terminate()

Handling Events

Global plugins receive events from all applications and controls. Event methods are named event_eventName and take three arguments:
events.py
import globalPluginHandler
import ui
import controlTypes

class GlobalPlugin(globalPluginHandler.GlobalPlugin):

	def event_gainFocus(self, obj, nextHandler):
		"""Called when any object gains focus."""
		# Check if this is a specific type of control
		if obj.role == controlTypes.Role.BUTTON:
			# Do custom processing for buttons
			pass
		# Always call nextHandler to propagate the event
		nextHandler()

	def event_nameChange(self, obj, nextHandler):
		"""Called when any object's name changes."""
		if obj.windowClassName == "SpecialControl":
			ui.message(f"Name changed to: {obj.name}")
		nextHandler()

	def event_foreground(self, obj, nextHandler):
		"""Called when the foreground application changes."""
		# Track application switches
		nextHandler()

Event Method Signature

self
GlobalPlugin
The plugin instance
obj
NVDAObject
The NVDA Object on which the event occurred
nextHandler
callable
Function to call to propagate the event to the next handler. Always call this unless you have a specific reason not to.

Creating Scripts

Scripts are commands bound to keyboard shortcuts that users can execute from anywhere:
import globalPluginHandler
from scriptHandler import script
import ui
import api

class GlobalPlugin(globalPluginHandler.GlobalPlugin):

	@script(
		description="Announces current battery status",
		category="System Information",
		gesture="kb:NVDA+shift+b"
	)
	def script_announceBattery(self, gesture):
		"""Announce battery status."""
		# Get battery information
		ui.message("Battery: 80% remaining")

	@script(
		description="Speaks window title and class",
		gestures=["kb:NVDA+alt+t", "kb:NVDA+shift+t"]
	)
	def script_windowInfo(self, gesture):
		"""Announce window information."""
		fg = api.getForegroundObject()
		ui.message(f"{fg.name}, class: {fg.windowClassName}")

Script Decorator Parameters

description
str
required
User-visible description shown in the Input Gestures dialog
category
str
Category for grouping in Input Gestures (uses plugin’s scriptCategory if not specified)
gesture
str
Single keyboard gesture (e.g., "kb:NVDA+shift+v")
gestures
list[str]
Multiple keyboard gestures
canPropagate
bool
default:"False"
Whether the script applies to focus ancestor objects
bypassInputHelp
bool
default:"False"
Run the script even when input help is active
allowInSleepMode
bool
default:"False"
Allow script execution when sleep mode is active
resumeSayAllMode
enum
The say all mode to resume after executing the script (from speech.sayAll.CURSOR)
speakOnDemand
bool
default:"False"
Produce speech when called while speech mode is “on-demand”

Gesture Formats

Gestures identify input that triggers scripts:
# Standard keyboard
"kb:NVDA+shift+v"
"kb:control+alt+delete"
"kb:f1"

# Laptop layout
"kb(laptop):NVDA+t"
"kb(laptop):NVDA+shift+upArrow"

# Named keys
"kb:escape"
"kb:enter"
"kb:space"
"kb:tab"
"kb:backspace"
"kb:delete"
"kb:home"
"kb:end"
"kb:pageUp"
"kb:pageDown"
"kb:upArrow"
"kb:downArrow"
"kb:leftArrow"
"kb:rightArrow"

Customizing NVDA Objects

Global plugins can customize NVDA Objects system-wide:
objectCustomization.py
import globalPluginHandler
from NVDAObjects.IAccessible import IAccessible
import controlTypes

class GlobalPlugin(globalPluginHandler.GlobalPlugin):

	def chooseNVDAObjectOverlayClasses(self, obj, clsList):
		"""Choose overlay classes for any NVDA Object."""
		# Add custom behavior to all edit fields
		if obj.role == controlTypes.Role.EDITABLETEXT:
			clsList.insert(0, EnhancedEditField)
		
		# Add custom behavior to specific window classes
		if obj.windowClassName == "CustomWidget":
			clsList.insert(0, CustomWidget)

class EnhancedEditField(IAccessible):
	"""Enhanced edit field with additional features."""
	
	def _get_description(self):
		"""Add helpful description."""
		return super().description or "Edit field"
	
	def event_gainFocus(self):
		"""Custom focus handling."""
		super().event_gainFocus()
		# Additional announcements

class CustomWidget(IAccessible):
	"""Custom widget support."""
	
	def _get_name(self):
		"""Better name extraction."""
		# Custom logic
		return "Custom widget"

Practical Examples

Example 1: Application Switcher

appSwitcher.py
# Enhanced application switching announcements

import globalPluginHandler
import ui

class GlobalPlugin(globalPluginHandler.GlobalPlugin):

	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		self.lastApp = None

	def event_foreground(self, obj, nextHandler):
		"""Announce application switches with detail."""
		appModule = obj.appModule
		if appModule and appModule.appName != self.lastApp:
			self.lastApp = appModule.appName
			# Announce with more detail
			ui.message(
				f"Switched to {obj.name}, {appModule.productName}"
			)
		nextHandler()

Example 2: Clipboard Monitor

clipboardMonitor.py
# Monitor and enhance clipboard operations

import globalPluginHandler
from scriptHandler import script
import api
import ui
import threading
import time

class GlobalPlugin(globalPluginHandler.GlobalPlugin):

	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		self.clipboardHistory = []
		self.monitoringEnabled = False

	@script(
		description="Toggle clipboard monitoring",
		category="Clipboard",
		gesture="kb:NVDA+shift+c"
	)
	def script_toggleMonitoring(self, gesture):
		"""Toggle clipboard monitoring."""
		self.monitoringEnabled = not self.monitoringEnabled
		state = "enabled" if self.monitoringEnabled else "disabled"
		ui.message(f"Clipboard monitoring {state}")

	@script(
		description="Announce clipboard content",
		gesture="kb:NVDA+c"
	)
	def script_announceClipboard(self, gesture):
		"""Announce what's on the clipboard."""
		text = api.getClipData()
		if text:
			ui.message(f"Clipboard: {text}")
		else:
			ui.message("Clipboard is empty")

	def terminate(self):
		"""Clean up."""
		self.monitoringEnabled = False
		super().terminate()

Example 3: OCR Integration

ocrPlugin.py
# Global OCR functionality

import globalPluginHandler
from scriptHandler import script
import ui
import api

class GlobalPlugin(globalPluginHandler.GlobalPlugin):

	scriptCategory = "OCR"

	@script(
		description="Recognize text in current navigator object",
		gesture="kb:NVDA+r"
	)
	def script_recognizeNavigator(self, gesture):
		"""Perform OCR on navigator object."""
		navigator = api.getNavigatorObject()
		if not navigator:
			ui.message("No navigator object")
			return
		
		try:
			location = navigator.location
			if not location:
				ui.message("Navigator object has no location")
				return
			
			ui.message("Recognizing...")
			# Perform OCR (simplified)
			result = self._performOCR(location)
			ui.message(result)
		except Exception as e:
			ui.message(f"OCR failed: {e}")

	def _performOCR(self, location):
		"""Perform OCR on screen region."""
		# Implementation would use actual OCR library
		return "Sample OCR result"

Example 4: Quick Settings

quickSettings.py
# Quick access to NVDA settings

import globalPluginHandler
from scriptHandler import script
import config
import ui
import speech

class GlobalPlugin(globalPluginHandler.GlobalPlugin):

	scriptCategory = "Quick Settings"

	@script(
		description="Cycle through speech rates",
		gesture="kb:NVDA+control+r"
	)
	def script_cycleRate(self, gesture):
		"""Cycle through preset speech rates."""
		rates = [25, 50, 75, 100]
		currentRate = config.conf["speech"]["synth"]["rate"]
		
		# Find next rate
		try:
			idx = rates.index(currentRate)
			nextRate = rates[(idx + 1) % len(rates)]
		except ValueError:
			nextRate = rates[0]
		
		config.conf["speech"]["synth"]["rate"] = nextRate
		speech.setSpeechRate(nextRate)
		ui.message(f"Speech rate: {nextRate}")

	@script(
		description="Toggle capital pitch changes",
		gesture="kb:NVDA+control+p"
	)
	def script_toggleCapPitch(self, gesture):
		"""Toggle capital letter pitch changes."""
		current = config.conf["speech"]["capPitchChange"]
		config.conf["speech"]["capPitchChange"] = not current
		state = "enabled" if not current else "disabled"
		ui.message(f"Capital pitch {state}")

Configuration Management

Global plugins can save and load settings:
configuration.py
import globalPluginHandler
import config
from scriptHandler import script
import ui

class GlobalPlugin(globalPluginHandler.GlobalPlugin):

	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		# Add configuration spec
		config.conf.spec["myPlugin"] = {
			"enabled": "boolean(default=True)",
			"announceLevel": "integer(default=1, min=0, max=3)",
			"soundEnabled": "boolean(default=False)"
		}
		# Load settings
		self.loadConfig()

	def loadConfig(self):
		"""Load plugin configuration."""
		conf = config.conf["myPlugin"]
		self.enabled = conf["enabled"]
		self.announceLevel = conf["announceLevel"]
		self.soundEnabled = conf["soundEnabled"]

	def saveConfig(self):
		"""Save plugin configuration."""
		conf = config.conf["myPlugin"]
		conf["enabled"] = self.enabled
		conf["announceLevel"] = self.announceLevel
		conf["soundEnabled"] = self.soundEnabled

	@script(description="Toggle plugin")
	def script_toggle(self, gesture):
		"""Toggle plugin on/off."""
		self.enabled = not self.enabled
		self.saveConfig()
		ui.message(f"Plugin {'enabled' if self.enabled else 'disabled'}")

	def terminate(self):
		"""Save config on exit."""
		self.saveConfig()
		super().terminate()

Threading and Timers

Be careful when using threads. NVDA is not fully thread-safe. Use wx.CallAfter or queueHandler.queueFunction to execute code on the main thread.
threading_example.py
import globalPluginHandler
import threading
import wx
import queueHandler
import ui

class GlobalPlugin(globalPluginHandler.GlobalPlugin):

	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		# Start background task
		self.thread = threading.Thread(target=self._backgroundTask)
		self.thread.daemon = True
		self.running = True
		self.thread.start()

	def _backgroundTask(self):
		"""Background task running in separate thread."""
		while self.running:
			# Do background work
			threading.Event().wait(5)  # Wait 5 seconds
			# Queue UI update on main thread
			queueHandler.queueFunction(
				queueHandler.eventQueue,
				ui.message,
				"Background task update"
			)

	def terminate(self):
		"""Stop background task."""
		self.running = False
		if self.thread.is_alive():
			self.thread.join(timeout=1.0)
		super().terminate()

Testing

1

Enable scratchpad

Go to NVDA Settings > Advanced and enable the scratchpad directory.
2

Create plugin

Place your global plugin in %APPDATA%\nvda\scratchpad\globalPlugins\.
3

Restart or reload

Either restart NVDA or press NVDA+Control+F3 to reload plugins.
4

Test functionality

Test your scripts and event handlers in various applications.
5

Check logs

View NVDA log (NVDA+F1) for any errors or debug messages.

Best Practices

Performance Considerations:
  • Avoid heavy processing in event handlers
  • Use threading for long-running operations
  • Always call nextHandler() unless you have a specific reason not to
  • Clean up resources in terminate()
Code Quality:
  • Provide clear script descriptions
  • Use appropriate script categories
  • Log debug information appropriately
  • Handle exceptions gracefully
  • Test in different NVDA configurations
  • Consider internationalization from the start

Common Pitfalls

  1. Forgetting to call nextHandler()
    # Wrong
    def event_gainFocus(self, obj, nextHandler):
        ui.message(obj.name)
        # Missing nextHandler() - breaks event chain!
    
    # Correct
    def event_gainFocus(self, obj, nextHandler):
        ui.message(obj.name)
        nextHandler()  # Always call this
    
  2. Not cleaning up in terminate()
    # Wrong
    class GlobalPlugin(globalPluginHandler.GlobalPlugin):
        def __init__(self):
            self.file = open("data.txt", "w")
        # File never closed!
    
    # Correct
    class GlobalPlugin(globalPluginHandler.GlobalPlugin):
        def __init__(self):
            self.file = open("data.txt", "w")
        
        def terminate(self):
            self.file.close()
            super().terminate()
    
  3. Blocking the main thread
    # Wrong - blocks NVDA
    def script_download(self, gesture):
        data = requests.get("http://example.com")  # Blocking!
        ui.message(data)
    
    # Correct - use threading
    def script_download(self, gesture):
        threading.Thread(target=self._download).start()
    
    def _download(self):
        data = requests.get("http://example.com")
        queueHandler.queueFunction(
            queueHandler.eventQueue,
            ui.message,
            data
        )
    

Build docs developers (and LLMs) love