Table of Contents
- Extension Development
- Extension Architecture
- Available Variables
- Extension Types
- 1. Post Processing Extensions
- 2. Command Extensions
- 3. Notification Extensions
- 4. Time-based Extensions
- Configuration and State Management
- RC File Configuration
- File-based Configuration Storage
- XDG Directory Handling
- Persistent Storage with $store
- Post Data Structure
- Notification Classes
- Example: Complete Extension
- Extension Development Workflow
- Advanced Extension Patterns
- Handling Initial Timeline Load
- Safe Text Processing
- Environment Detection
- Notification Function Registration
- Best Practices
- Extension Loading
- Testing Your Extension
- Distribution
Extension Development
TTYverse extensions are Perl scripts that integrate with TTYverse's event system to add custom functionality. This guide covers creating, testing, and deploying custom extensions.
Extension Architecture
Extensions hook into TTYverse's multi-process architecture through defined callback functions:
$handle
- Called for each incoming post (main processing hook)$addaction
- Called for user commands (adds new slash commands)$prepost
- Called before sending posts$postpost
- Called after sending posts$heartbeat
- Called periodically for background tasks$notifier
- Called for notification events (sounds, desktop alerts, etc.)
Available Variables
Extensions have access to these TTYverse variables:
Core Variables
$whoami
- Current user's username$silent
- Silent mode flag (suppress output when true)$verbose
- Verbose mode flag (show debug info when true)$stdout
- Output filehandle for user messages$streamout
- Output filehandle for timeline content
File Paths
$data
- XDG data directory (~/.local/share/ttyverse
)$config
- XDG config directory (~/.config/ttyverse
)$store
- Extension persistent storage hash
Post Processing
&defaulthandle($ref)
- Default post display function&standardpost($ref, $text_only)
- Format post for notifications&findtarget($code)
- Find post by menu code (a1, b3, etc.)&descape($text)
- Decode HTML entities and escape sequences safely&common_split_post($text, $reply_to, $media)
- Send posts via TTYverse
Timeline State
$last_id
- Current timeline position (0 during initial load)$initial_load_in_progress
- True during startup timeline fetch
Environment Access
$ENV{'HOME'}
- User's home directory$ENV{'DISPLAY'}
- X11 display (for GUI detection)$ENV{'XDG_DATA_HOME'}
- XDG data directory override$ENV{'XDG_CONFIG_HOME'}
- XDG config directory override
Extension Types
1. Post Processing Extensions
Handle incoming posts and modify display:
$handle = sub {
my $post_ref = shift;
my $class = shift; # Notification class (default, mention, dm, etc.)
# Your custom processing here
# Access post data: $post_ref->{'content'}, $post_ref->{'account'}->{'username'}
# Always call default handler to display the post
&defaulthandle($post_ref);
return 1;
};
2. Command Extensions
Add new slash commands:
$addaction = sub {
my $command_line = $_; # Full command line input
if ($command_line =~ /^\/mycommand\s+(.*)$/) {
my $args = $1;
# Process your command
print $streamout "-- My command executed with: $args\n";
return 1; # Command handled
}
return 0; # Not our command, let TTYverse handle it
};
3. Notification Extensions
Handle sound, visual, or other notifications:
sub notifier_mynotifier {
my $class = shift; # Notification type (dm, mention, boost, etc.)
my $text = shift; # Formatted notification text
my $post_ref = shift; # Original post data
# Handle initialization (called with no $class)
if (!defined($class)) {
print $stdout "-- My notifier loaded\n";
return 1;
}
# Process notification based on class
if ($class eq 'dm') {
# Handle direct messages with higher priority
} elsif ($class eq 'mention') {
# Handle mentions
}
return 1;
}
# Register the notifier (must be done globally)
# TTYverse automatically finds functions named notifier_*
4. Time-based Extensions
Extensions that perform actions based on time intervals:
# Extension global variables for state tracking
my $last_action_time = 0;
my $first_run = 0;
$handle = sub {
my $post_ref = shift;
# Initialize on first run
if (!$first_run) {
$first_run = 1;
$last_action_time = time;
print $stdout "-- Time-based extension loaded\n" if (!$silent);
}
# Check if enough time has passed (5 minutes = 300 seconds)
my $current_time = time;
if ($current_time - $last_action_time >= 300) {
$last_action_time = $current_time;
# Perform periodic action
my ($sec,$min,$hour,$day,$mon,$year) = localtime($current_time);
my $timestamp = sprintf("%02d:%02d", $hour, $min);
print $streamout "-- $timestamp --\n" if (!$silent);
}
# Always call default handler
&defaulthandle($post_ref);
return 1;
};
Configuration and State Management
RC File Configuration
Extensions can use configuration variables from the user's RC file:
# In user's ~/.config/ttyverse/ttyverserc:
# extpref_myext_setting=value
# extpref_myext_enabled=1
# In your extension:
my $setting = $extpref_myext_setting || "default_value";
my $enabled = $extpref_myext_enabled || 0;
File-based Configuration Storage
For settings that change during runtime, use separate config files:
# Read extension-specific config file
my $config_file = "$ENV{'HOME'}/.config/ttyverse/myext_settings";
my $setting_value;
if (-e $config_file) {
open my $fh, '<', $config_file or die "Cannot read config: $!";
$setting_value = <$fh>;
close $fh;
chomp($setting_value) if defined($setting_value);
} else {
$setting_value = "default";
}
# Write extension-specific config file
sub save_setting {
my $value = shift;
my $config_file = "$ENV{'HOME'}/.config/ttyverse/myext_settings";
open my $fh, '>', $config_file or die "Cannot write config: $!";
print $fh $value;
close $fh;
}
XDG Directory Handling
Use XDG standards for cross-platform compatibility:
# Get XDG-compliant data directory
my $data_dir = $ENV{'XDG_DATA_HOME'} || "$ENV{'HOME'}/.local/share";
my $ext_data_dir = "$data_dir/ttyverse";
# Get XDG-compliant config directory
my $config_dir = $ENV{'XDG_CONFIG_HOME'} || "$ENV{'HOME'}/.config";
my $ext_config_dir = "$config_dir/ttyverse";
# Create directories if needed
mkdir $ext_data_dir unless -d $ext_data_dir;
mkdir $ext_config_dir unless -d $ext_config_dir;
Persistent Storage with $store
Use the $store
hash for data that persists during the TTYverse session:
# Store data (survives between function calls)
$store->{'my_counter'} = 0;
$store->{'user_preferences'} = { volume => 50, enabled => 1 };
# Access stored data
my $count = $store->{'my_counter'} || 0;
$count++;
$store->{'my_counter'} = $count;
# Complex data structures
my $prefs = $store->{'user_preferences'} || {};
$prefs->{'last_used'} = time;
$store->{'user_preferences'} = $prefs;
Post Data Structure
Post references contain Mastodon API data:
$post_ref = {
'id' => '12345',
'content' => 'Post text with <em>HTML</em>',
'account' => {
'username' => 'user',
'acct' => 'user@domain.com',
'display_name' => 'Display Name'
},
'mentions' => [
{
'username' => 'mentioned_user',
'acct' => 'mentioned_user@domain.com'
}
],
'created_at' => '2024-01-01T12:00:00Z',
'visibility' => 'public'
};
Notification Classes
TTYverse uses these notification classes:
default
- Regular timeline postsmention
- Posts that mention your usernamedm
- Direct messages (highest priority)me
- Your own postsfollow
- New followersboost
- Your posts were boosted/sharedfavourite
- Your posts were favourited/likedpoll
- Poll notificationsannouncement
- Server announcements
Example: Complete Extension
Here's a comprehensive example extension that demonstrates multiple patterns:
# Advanced Custom Extension
# Adds /greet command and tracks greeting statistics
# Demonstrates configuration, state management, and safe text processing
# Extension configuration with defaults
my $enabled = $extpref_greet_enabled || 1;
my $greeting_style = $extpref_greet_style || "friendly";
# Initialize extension state
if (!$store->{'greet_loaded'}) {
$store->{'greet_loaded'} = 1;
$store->{'greet_count'} = 0;
print $stdout "-- Greeting extension loaded (style: $greeting_style)\n" if (!$silent);
}
# Command handler
$addaction = sub {
my $command = $_;
# Toggle extension on/off
if ($command eq '/greet toggle') {
my $config_file = "$ENV{'HOME'}/.config/ttyverse/greet_enabled";
my $new_state = $enabled ? 0 : 1;
open my $fh, '>', $config_file or die "Cannot save config: $!";
print $fh $new_state;
close $fh;
$enabled = $new_state;
my $status = $enabled ? "enabled" : "disabled";
print $streamout "-- Greeting extension $status\n";
return 1;
}
# Show greeting statistics
if ($command eq '/greet stats') {
my $count = $store->{'greet_count'} || 0;
print $streamout "-- Total greetings sent: $count\n";
return 1;
}
# Send greeting
if ($command =~ /^\/greet\s+(\S+)\s*(.*)$/) {
return 0 unless $enabled; # Respect user preference
my $username = $1;
my $custom_message = $2;
# Clean and validate username
$username = &descape($username);
$username =~ s/^@//; # Remove @ if user included it
# Choose greeting based on style and custom message
my $greeting;
if ($custom_message) {
$greeting = &descape($custom_message);
} elsif ($greeting_style eq "formal") {
$greeting = "Good day!";
} elsif ($greeting_style eq "casual") {
$greeting = "Hey there! 😊";
} else { # friendly (default)
$greeting = "Hello! 👋";
}
# Send the greeting
my $post_text = "\@$username $greeting";
my $result = &common_split_post($post_text, undef, undef);
# Update statistics on success
if ($result) {
$store->{'greet_count'}++;
print $stdout "-- Greeting sent to $username (total: $store->{'greet_count'})\n" if ($verbose);
}
return $result;
}
return 0; # Not our command
};
# Post handler (optional - could add auto-greeting detection)
$handle = sub {
my $post_ref = shift;
my $class = shift;
# Skip during initial load
if ($last_id eq 0) {
&defaulthandle($post_ref);
return 1;
}
# Could add logic here to detect greetings and respond
# For now, just process normally
&defaulthandle($post_ref);
return 1;
};
# Extension successfully loaded
$store->{'loaded'} = 1;
# Required return value
1;
Extension Development Workflow
1. Create Extension File
# Create extension in the extensions directory
~/.local/share/ttyverse/extensions/myextension.pl
2. Test Extension
# Test Perl syntax
perl -c ~/.local/share/ttyverse/extensions/myextension.pl
# Load extension with TTYverse
./ttyverse.pl -exts=myextension -verbose
3. Debug Extension
Use verbose mode to see debug output:
./ttyverse.pl -exts=myextension -verbose
Add debug output in your extension:
print $stdout "-- DEBUG: My extension called\n" if ($verbose);
Advanced Extension Patterns
Handling Initial Timeline Load
Prevent extension spam during startup by checking timeline state:
$handle = sub {
my $post_ref = shift;
my $class = shift;
# Skip processing during initial timeline load
if ($last_id eq 0) {
&defaulthandle($post_ref);
return 1;
}
# Or use the more explicit flag
if ($initial_load_in_progress) {
&defaulthandle($post_ref);
return 1;
}
# Your extension logic here...
&defaulthandle($post_ref);
return 1;
};
Safe Text Processing
Always use &descape()
when processing post content:
# Extract and clean post text
my $author = &descape($post_ref->{'account'}->{'display_name'} ||
$post_ref->{'account'}->{'username'});
my $content = &descape($post_ref->{'content'});
# Additional cleaning for text-to-speech or notifications
$content =~ s/<[^>]*>//g; # Remove HTML tags
$content =~ s/&[a-zA-Z0-9]+;//g; # Remove remaining entities
$content =~ s/https?:\/\/\S+/ link /g; # Replace URLs
$content =~ s/#(\w+)/ hashtag $1 /g; # Make hashtags readable
Environment Detection
Check system capabilities before using features:
# Check for GUI environment
if (!$ENV{'DISPLAY'}) {
print $stdout "-- Warning: No GUI display available\n" if (!$silent);
return 1;
}
# Check for required commands
sub command_exists {
my $cmd = shift;
return system("which $cmd >/dev/null 2>&1") == 0;
}
if (!command_exists('paplay')) {
print $stdout "-- Warning: paplay not found, trying alternative\n" if (!$silent);
# Try alternative commands...
}
Notification Function Registration
Proper naming convention for notification handlers:
# Function name must be: notifier_extensionname
sub notifier_soundpack {
my $class = shift;
my $text = shift;
my $post_ref = shift;
# TTYverse automatically discovers and calls functions matching this pattern
# No explicit registration needed
}
# Multiple notifiers can exist - TTYverse calls all of them
sub notifier_myother {
# Another notification handler
}
Best Practices
Error Handling
$handle = sub {
my $post_ref = shift;
eval {
# Your processing code here
# ...
};
if ($@) {
print $stdout "-- Extension error: $@\n" if (!$silent);
}
# Always call default handler
&defaulthandle($post_ref);
return 1;
};
Persistent Storage
# Store data between sessions
$store->{'my_data'} = "persistent value";
# Access stored data
my $saved_value = $store->{'my_data'} || "default";
Respecting User Preferences
# Check if user wants your extension to be quiet
return 1 if ($silent);
# Only show messages in verbose mode
print $stdout "-- Extension info\n" if ($verbose && !$silent);
Extension Loading
Extensions must end with:
# Mark extension as successfully loaded
$store->{'loaded'} = 1;
# Required Perl module return value
1;
Testing Your Extension
- Syntax Check:
perl -c yourextension.pl
- Load Test:
./ttyverse.pl -exts=yourextension -verbose
- Function Test: Try your commands/hooks with real data
- Error Test: Test with invalid inputs and network failures
Distribution
To share your extension:
- Add clear documentation header with license
- List any dependencies
- Provide configuration examples
- Test on different systems
See existing extensions in ~/.local/share/ttyverse/extensions/
for examples.
See also: Extensions | Configuration | Commands Reference