RTFM Plugin Development Guide
November 4, 2025 ยท View on GitHub
Extend RTFM with custom preview handlers and key bindings.
Plugin System Overview
RTFM supports two types of plugins:
- Preview Handlers (
~/.rtfm/plugins/preview.rb) - Custom file type previews - Key Bindings (
~/.rtfm/plugins/keys.rb) - Custom commands and key mappings
Both are Ruby files evaluated in RTFM's context, giving you full access to RTFM's internals.
Preview Handlers
Location
~/.rtfm/plugins/preview.rb
Syntax
# extension1, extension2, extension3 = command with @s placeholder
#
# @s is replaced with shell-escaped filename
# Examples:
txt, log = bat -n --color=always @s
md = pandoc @s -t plain
pdf = pdftotext -f 1 -l 4 @s -
json = jq . @s
xml = xmllint --format @s
How It Works
- RTFM matches file extension
- Replaces
@swith escaped filename - Executes command
- Displays output in right pane
Examples
Syntax Highlighting
# Programming languages
rb, py, js = bat -n --color=always @s
c, cpp, h = highlight -O ansi --force --line-numbers @s
Document Formats
# Markdown
md, markdown = pandoc @s -t plain
# PDF
pdf = pdftotext -f 1 -l 10 @s -
# LibreOffice
odt = odt2txt @s
ods = ssconvert --export-type=Gnumeric_stf:stf_csv @s fd://1
# MS Office
docx = docx2txt @s
xlsx = ssconvert --export-type=Gnumeric_stf:stf_csv @s fd://1
Data Formats
# JSON with syntax highlighting
json = jq -C . @s
# YAML
yaml, yml = bat -l yaml @s
# XML
xml = xmllint --format @s | bat -l xml
Media Info
# Video metadata
mp4, mkv, avi = ffprobe -hide_banner @s 2>&1
# Audio metadata
mp3, flac = mediainfo @s
# Image metadata (already built-in, but you can override)
# png, jpg = identify -verbose @s
Archives (Preview Contents)
Built-in support for: zip, tar, gz, bz2, xz, rar, 7z
Override if needed:
zip = unzip -l @s
tar = tar -tvf @s
Complex Preview Handlers
For more complex logic, use Ruby in preview.rb:
# Define handler as Ruby code instead of shell command
PREVIEW_HANDLERS << [/\.log$/i, -> {
# Custom Ruby handler
content = File.read(@selected).lines.last(50).join
@pR.say("Last 50 lines:\n" + content)
}]
Key Bindings
Location
~/.rtfm/plugins/keys.rb
Basic Syntax
# Add or override key binding
KEYMAP['X'] = :my_handler
# Define handler method
def my_handler(_chr)
@pB.say("You pressed X!")
end
Available Panes
| Variable | Description |
|---|---|
@pT | Top pane (path/metadata) |
@pL | Left pane (file list) |
@pR | Right pane (preview) |
@pB | Bottom pane (status) |
@pCmd | Command prompt pane |
@pSearch | Search prompt pane |
@pAI | AI chat pane |
@pRuby | Ruby debug pane |
Pane Methods
# Display text
@pR.say("Hello world")
@pB.say("Status message")
# Ask for input
answer = @pCmd.ask('Enter value: ', 'default')
# Clear pane
@pR.clear
# Update pane (mark for refresh)
@pR.update = true
# Force immediate refresh
@pR.refresh
@pR.full_refresh # Complete redraw
Available Variables
| Variable | Type | Description |
|---|---|---|
@selected | String | Currently selected file/dir path |
@tagged | Array | Paths of tagged items |
@marks | Hash | Bookmarks {'a' => '/path', ...} |
@files | Array | Current directory file list |
@index | Integer | Selected item index |
@w / @h | Integer | Terminal width/height |
@preview | Boolean | Preview enabled? |
@showimage | Boolean | Image preview enabled? |
@trash | Boolean | Trash bin enabled? |
Helper Functions
Execute Commands
# Capture output (auto-shows errors in right pane)
output = command("ls -la", timeout: 5)
@pR.say(output)
# Fire-and-forget (shows errors if any)
shell("mv file1 file2", background: false)
# Show both stdout and stderr in right pane
shellexec("grep -r pattern .")
File Operations
# Check if file exists
File.exist?(@selected)
# Get file size
File.size(@selected)
# Read file
content = File.read(@selected)
# Write file
File.write('/tmp/output.txt', content)
Example Plugins
Example 1: Git Commit Shortcut
# ~/.rtfm/plugins/keys.rb
KEYMAP['C-G'] = :git_quick_commit
def git_quick_commit
message = @pCmd.ask('Commit message: ', '')
return if message.strip.empty?
shellexec("git add . && git commit -m '#{message}' && git push")
@pB.say("Git commit and push completed")
end
Usage: Press Ctrl-g, enter message, done!
Example 2: Quick Note Taker
KEYMAP['C-N'] = :quick_note
def quick_note
note = @pCmd.ask('Note: ', '')
return if note.strip.empty?
File.open("#{Dir.home}/notes.txt", 'a') do |f|
f.puts "[#{Time.now}] #{note}"
end
@pB.say("Note saved to ~/notes.txt")
end
Example 3: Batch File Converter
KEYMAP['C-C'] = :convert_images
def convert_images
return @pB.say("Tag images first!") if @tagged.empty?
format = @pCmd.ask('Convert to (png/jpg/webp): ', 'png')
@tagged.each do |file|
next unless file.match(/\.(jpg|png|gif|bmp)$/i)
output = file.sub(/\.\w+$/, ".#{format}")
command("convert #{Shellwords.escape(file)} #{Shellwords.escape(output)}")
end
@pB.say("Converted #{@tagged.size} images to #{format}")
@tagged.clear
@pL.update = true
end
Example 4: Custom File Opener
KEYMAP['O'] = :open_with
def open_with
program = @pCmd.ask('Open with: ', 'vim')
return if program.strip.empty?
escaped = Shellwords.escape(@selected)
# Set flag to prevent SIGWINCH redrawing over program
@external_program_running = true
system("stty sane < /dev/tty")
system("clear < /dev/tty > /dev/tty")
Cursor.show
system("#{program} #{escaped}")
@external_program_running = false
# Restore terminal for RTFM
system('stty raw -echo isig < /dev/tty')
$stdin.raw!
$stdin.echo = false
Cursor.hide
Rcurses.clear_screen
refresh
render
end
Example 5: Directory Size Calculator
KEYMAP['#'] = :calc_dir_size
def calc_dir_size
return @pB.say("Select a directory") unless File.directory?(@selected)
@pR.say("Calculating size...")
output = command("du -sh #{Shellwords.escape(@selected)}")
size = output.split("\t").first
@pR.say("Directory Size\n\n#{@selected}\n\n#{size}")
end
Advanced Techniques
Launching External TUI Programs
For full-screen terminal programs (vim, htop, etc.):
def launch_external_program(cmd)
@external_program_running = true
# Save and restore terminal state
system("stty -g < /dev/tty > /tmp/rtfm_stty_$$")
system('stty sane < /dev/tty')
system('clear < /dev/tty > /dev/tty')
Cursor.show
# Spawn on real tty
pid = Process.spawn(cmd,
in: '/dev/tty',
out: '/dev/tty',
err: '/dev/tty')
begin
Process.wait(pid)
rescue Interrupt
Process.kill('TERM', pid) rescue nil
retry
ensure
@external_program_running = false
end
# Restore RTFM terminal state
system('stty raw -echo isig < /dev/tty')
$stdin.raw!
$stdin.echo = false
Cursor.hide
Rcurses.clear_screen
refresh
render
end
Working with Tagged Items
def process_tagged_items
if @tagged.empty?
@pB.say("No items tagged")
return
end
@tagged.each do |item|
# Process each item
if File.file?(item)
# Handle file
elsif File.directory?(item)
# Handle directory
end
end
# Clear tags after processing
@tagged.clear
@pL.update = true
end
Interactive Prompts
def interactive_handler
# Text input
text = @pCmd.ask('Enter text: ', 'default value')
# Number input
num = @pCmd.ask('Enter number: ', '10').to_i
# Yes/no confirmation
confirm = @pCmd.ask('Proceed? (y/n): ', 'y')
return unless confirm =~ /^y/i
# Process...
end
Updating Display
def custom_display
# Update right pane
@pR.clear
@pR.say("Custom content here")
@pR.update = true
# Update bottom status
@pB.say("Operation complete")
@pB.update = true
# Trigger render
render
end
Plugin Best Practices
1. Check Prerequisites
def my_handler
unless cmd?('required-program')
@pB.say("Error: required-program not installed")
return
end
# Continue...
end
2. Handle Errors Gracefully
def safe_handler
begin
# Your code
rescue => e
@pB.say("Error: #{e.message}")
end
end
3. Provide Feedback
def verbose_handler
@pB.say("Processing...")
# Long operation
result = command("slow-command")
@pB.say("Completed!")
@pR.say(result)
end
4. Use Shellwords for Safety
require 'shellwords'
escaped = Shellwords.escape(@selected)
command("program #{escaped}")
5. Respect Image Display
def text_display_handler
# Clear image before showing text
clear_image
@pR.say("Your text content")
end
Debugging Plugins
Test in Ruby Mode
- Press
@to enter Ruby mode - Test your code:
my_handler(nil) - Check for errors in right pane
Reload Plugins
# In Ruby mode
load '~/.rtfm/plugins/keys.rb'
Or restart RTFM.
Check Variables
# In Ruby mode
puts KEYMAP.keys.sort
puts @selected
puts defined?(my_handler)
Plugin Ideas
Workflow Automation
- Git workflow shortcuts
- Deployment scripts
- Backup automation
- File organization rules
File Processing
- Batch image conversion
- Document generation
- Log analysis
- Data extraction
Integration
- Integration with other tools
- API calls
- Database queries
- Cloud storage sync
Information Display
- Custom file info
- Directory statistics
- Metadata extraction
- Health checks
Sharing Plugins
Consider sharing useful plugins:
- Post as GitHub gist
- Share in RTFM issues/discussions
- Create plugin collection repository
Plugin Template
# ~/.rtfm/plugins/keys.rb
#
# Plugin: [Name]
# Description: [What it does]
# Author: [Your name]
# Dependencies: [Required programs]
KEYMAP['[KEY]'] = :plugin_name
def plugin_name(_chr)
# Check prerequisites
return @pB.say("Error: dependency missing") unless cmd?('program')
# Get input if needed
input = @pCmd.ask('Prompt: ', 'default')
return if input.strip.empty?
# Show progress
@pB.say("Processing...")
# Do work
begin
result = command("your-command #{Shellwords.escape(input)}")
# Display result
@pR.say(result)
@pB.say("Completed!")
rescue => e
@pB.say("Error: #{e.message}")
end
end
Reference
All Available Methods
# Command execution
command(cmd, timeout: 5, return_both: false)
shell(cmd, background: false, err: nil)
shellexec(cmd, timeout: 10)
# Utilities
cmd?(program) # Check if program exists
dirlist(left: false) # Get directory listing
mark_latest # Update directory marks
track_file_access(path) # Track file access
track_directory_access(path) # Track directory access
# Display
refresh # Refresh layout
render # Render all panes
clear_image # Clear displayed image
showimage(path) # Display image
# Operations
add_undo_operation(info) # Add to undo history
copy_to_clipboard(text, 'primary' or 'clipboard')
Global Variables
# File system
Dir.pwd # Current directory
@selected # Selected item path
@files # Current dir file list
@tagged # Tagged items array
# Configuration
@preview # Preview enabled?
@showimage # Images enabled?
@trash # Trash enabled?
@lsall / @lslong / @lsorder / @lsinvert
# State
@index # Selected item index
@marks # Bookmarks hash
@history # Command history array
@remote_mode # In SSH mode?
# Display
@w / @h # Terminal dimensions
@pL / @pR / @pT / @pB # Panes