Architectural Overview
Relevant source files
The following files were used as context for generating this wiki page:Ghostbird is a Thunderbird add-on that enables users to compose emails in an external text editor. It achieves this by implementing the GhostText protocol, which facilitates communication between the Thunderbird compose window and a GhostText server running in the user's chosen editor. The architecture is designed to be modular, testable, and maintainable, primarily through the application of the Ports and Adapters pattern and a custom dependency injection mechanism.
The system is composed of three main JavaScript bundles: a background script (background.js) for managing state and communication, a compose script (compose.js) injected into the email compose window, and an options script (options.js) for user configuration. These components work together to provide a seamless editing experience, relaying text between Thunderbird and the external editor via a WebSocket connection.
Sources: README.md, doc/design.md:43-48
High-Level Component Interaction
The core functionality of Ghostbird revolves around the interaction between Thunderbird, the Ghostbird add-on itself, and the external text editor's GhostText server.
The following diagram illustrates the main components and their relationships.
This diagram shows the flow when a user initiates an editing session. The Ghostbird add-on acts as a client to the GhostText server, mediating the flow of text between the Thunderbird compose window and the external editor.
Sources: README.md:80-93, doc/design.md:6-30
Code Structure
The project's source code is organized in the src/ directory, following a Ports and Adapters architecture to enforce the Dependency Inversion Principle. This structure isolates the core application logic from external dependencies like the Thunderbird WebExtension API, making the code more modular and easier to test.
Sources: doc/design.md:129-138
Source Directories
The key directories and their responsibilities are summarized below:
| Directory | Description |
|---|---|
src/root/ | Contains the main entry points for the add-on's scripts (background.ts, compose.ts, options.ts) and the dependency injection "Composition Root". |
src/app-* | Contains infrastructure code for handling Thunderbird events for each respective entry point (e.g., app-background/, app-compose/). |
src/ghosttext-session/ | The core application logic implementing the GhostText protocol. It has no dependencies on other modules within the project. |
src/ghosttext-runner/ | A facilitator module that consumes events and executes decisions made by ghosttext-session. |
src/ghosttext-adaptor/ | Helper modules for ghosttext-runner, acting as adapters to the app-* layers. |
src/thunderbird/ | A wrapper around the Thunderbird MailExtension API, isolating the rest of the application from direct API calls. |
src/util/ | General-purpose utility modules with no external dependencies. |
src/test/ | Unit and integration tests for the application. |
Sources: doc/design.md:143-162
Module Dependencies
Dependencies flow inwards, with the core logic at the center. The ghosttext-session module is the core and has no outgoing dependencies. Modules in app-* and thunderbird/ are at the outer layers, handling interactions with the browser.
The diagram below shows the dependency flow between the main module groups. Arrows point from the dependent to the dependency.
This structure ensures that changes in the Thunderbird API should ideally only affect the thunderbird/ module, minimizing impact on the core application logic.
Sources: doc/design.md:167-181
Dependency Injection and Startup
Ghostbird uses a custom, lightweight dependency injection (DI) mechanism to manage object creation and wiring. This system is implemented in the root/startup/ modules and serves as the Composition Root of the application.
Sources: doc/design.md:210-212, doc/faq-architectural.md:3-5
Automatic Instantiation
The DI container automatically discovers and instantiates classes that have a static isSingleton: boolean property.
static isSingleton: true: A single instance of the class is created and shared.static isSingleton: false: A new instance is created for each dependency request.static wantArray: true: The constructor receives an array of all instances that match a dependency name.
Dependencies are injected via constructors. The DI container resolves dependencies by matching constructor parameter names to the names of registered classes (case-insensitively). This approach automates object creation, reduces boilerplate code, and encourages adherence to the Dependency Inversion Principle, which enhances testability.
The following diagram illustrates the logic for resolving and instantiating a class.
This DI system is a key architectural choice made to improve long-term maintainability, especially when adapting to breaking changes in the Thunderbird API. Sanity tests are in place (src/test/startup.test.ts) to verify that all registered dependencies can be successfully resolved and instantiated at build time.
Sources: doc/design.md:213-243, doc/faq-architectural.md:6-26, src/test/startup.test.ts:39-47
Communication and Data Flow
When a user clicks the Ghostbird button in a compose window, a specific sequence of events is triggered to establish a connection with the external editor.
- Initiation: The user clicks the Ghostbird button. The
background.jsscript handles this action. - Port Connection:
background.jsinjectscompose.jsinto the compose window (if not already present) and establishes aruntime.Portfor communication. - WebSocket Connection:
background.jssends an HTTPGET /request to the GhostText server (default:localhost:4001) to get the WebSocket port. It then establishes a WebSocket connection. - Data Sync:
compose.jssends the initial email text tobackground.jsvia the port.background.jsrelays this text to the GhostText server via the WebSocket. - Live Editing: Any changes made in the external editor are sent back through the WebSocket to
background.js, which relays them tocompose.jsto update the compose window's content.
The connection is terminated when the user closes the editor, closes the compose window, or presses the "stop" shortcut.
The following sequence diagram details this interaction:
Sources: doc/design.md:60-101
Build System
The project uses tsdown as its bundler, configured via tools/tsdown_config.ts. The build process is responsible for transpiling TypeScript, generating the manifest.json, creating localization files, and packaging the final .xpi file.
The build process has two main output formats:
- ESM (
es): Forbackground.tsandoptions.ts. - IIFE (
iife): Forcompose.ts, which is injected as a plain script.
The build pipeline uses several custom and built-in plugins to perform its tasks.
The following diagram shows the inputs and outputs of the build system.
This process ensures that all necessary files are correctly generated, bundled, and placed in the dist/ext/ directory before being packaged by web-ext.
Sources: doc/building.md, tools/tsdown_config.ts, package.json:20-47