heathrow 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +58 -0
  3. data/README.md +205 -0
  4. data/bin/heathrow +42 -0
  5. data/bin/heathrowd +283 -0
  6. data/docs/ARCHITECTURE.md +1172 -0
  7. data/docs/DATABASE_SCHEMA.md +685 -0
  8. data/docs/DEVELOPMENT_WORKFLOW.md +867 -0
  9. data/docs/DISCORD_SETUP.md +142 -0
  10. data/docs/GMAIL_OAUTH_SETUP.md +120 -0
  11. data/docs/PLUGIN_SYSTEM.md +1370 -0
  12. data/docs/PROJECT_PLAN.md +1022 -0
  13. data/docs/README.md +417 -0
  14. data/docs/REDDIT_SETUP.md +174 -0
  15. data/docs/REPLY_FORWARD.md +182 -0
  16. data/docs/WHATSAPP_TELEGRAM_SETUP.md +306 -0
  17. data/heathrow.gemspec +34 -0
  18. data/heathrowd.service +21 -0
  19. data/img/heathrow.svg +95 -0
  20. data/img/rss_threaded.png +0 -0
  21. data/img/sources.png +0 -0
  22. data/lib/heathrow/address_book.rb +42 -0
  23. data/lib/heathrow/config.rb +332 -0
  24. data/lib/heathrow/database.rb +731 -0
  25. data/lib/heathrow/database_new.rb +392 -0
  26. data/lib/heathrow/event_bus.rb +175 -0
  27. data/lib/heathrow/logger.rb +122 -0
  28. data/lib/heathrow/message.rb +176 -0
  29. data/lib/heathrow/message_composer.rb +399 -0
  30. data/lib/heathrow/message_organizer.rb +774 -0
  31. data/lib/heathrow/migrations/001_initial_schema.rb +248 -0
  32. data/lib/heathrow/notmuch.rb +45 -0
  33. data/lib/heathrow/oauth2_smtp.rb +254 -0
  34. data/lib/heathrow/plugin/base.rb +212 -0
  35. data/lib/heathrow/plugin_manager.rb +141 -0
  36. data/lib/heathrow/poller.rb +93 -0
  37. data/lib/heathrow/smtp_sender.rb +204 -0
  38. data/lib/heathrow/source.rb +39 -0
  39. data/lib/heathrow/sources/base.rb +74 -0
  40. data/lib/heathrow/sources/discord.rb +357 -0
  41. data/lib/heathrow/sources/gmail.rb +294 -0
  42. data/lib/heathrow/sources/imap.rb +198 -0
  43. data/lib/heathrow/sources/instagram.rb +307 -0
  44. data/lib/heathrow/sources/instagram_fetch.py +101 -0
  45. data/lib/heathrow/sources/instagram_send.py +55 -0
  46. data/lib/heathrow/sources/instagram_send_marionette.py +104 -0
  47. data/lib/heathrow/sources/maildir.rb +606 -0
  48. data/lib/heathrow/sources/messenger.rb +212 -0
  49. data/lib/heathrow/sources/messenger_fetch.js +297 -0
  50. data/lib/heathrow/sources/messenger_fetch_marionette.py +138 -0
  51. data/lib/heathrow/sources/messenger_send.js +32 -0
  52. data/lib/heathrow/sources/messenger_send.py +100 -0
  53. data/lib/heathrow/sources/reddit.rb +461 -0
  54. data/lib/heathrow/sources/rss.rb +299 -0
  55. data/lib/heathrow/sources/slack.rb +375 -0
  56. data/lib/heathrow/sources/source_manager.rb +328 -0
  57. data/lib/heathrow/sources/telegram.rb +498 -0
  58. data/lib/heathrow/sources/webpage.rb +207 -0
  59. data/lib/heathrow/sources/weechat.rb +479 -0
  60. data/lib/heathrow/sources/whatsapp.rb +474 -0
  61. data/lib/heathrow/ui/application.rb +8098 -0
  62. data/lib/heathrow/ui/navigation.rb +8 -0
  63. data/lib/heathrow/ui/panes.rb +8 -0
  64. data/lib/heathrow/ui/source_wizard.rb +567 -0
  65. data/lib/heathrow/ui/threaded_view.rb +780 -0
  66. data/lib/heathrow/ui/views.rb +8 -0
  67. data/lib/heathrow/version.rb +3 -0
  68. data/lib/heathrow/wizards/discord_wizard.rb +193 -0
  69. data/lib/heathrow/wizards/slack_wizard.rb +140 -0
  70. data/lib/heathrow.rb +55 -0
  71. metadata +147 -0
@@ -0,0 +1,182 @@
1
+ # Reply, Forward, and Compose in Heathrow
2
+
3
+ Heathrow provides full email-style reply, forward, and compose functionality using your system's $EDITOR, similar to mutt.
4
+
5
+ ## Features
6
+
7
+ - **Reply** (`r`) - Reply to the sender
8
+ - **Reply All** (`R`) - Reply to sender and all recipients
9
+ - **Forward** (`f`) - Forward message to new recipients
10
+ - **Compose** (`m`) - Create new message
11
+
12
+ ## How It Works
13
+
14
+ 1. Press the appropriate key while viewing a message
15
+ 2. Your $EDITOR (default: vim) opens with a pre-formatted template
16
+ 3. Edit the message, keeping the To: and Subject: headers
17
+ 4. Save and exit to send (`:wq` in vim)
18
+ 5. Exit without saving to cancel
19
+
20
+ ## SMTP Configuration
21
+
22
+ Heathrow supports OAuth2 SMTP for Gmail and custom domains. Configure your domains in the `OAUTH2_DOMAINS` list inside `smtp_sender.rb`:
23
+ - gmail.com
24
+ - yourdomain.com (add your own)
25
+
26
+ The SmtpSender module (`lib/heathrow/smtp_sender.rb`) automatically detects configured domains and routes mail through your OAuth2 SMTP command (configurable in settings).
27
+
28
+ ### OAuth2 Setup
29
+
30
+ Your OAuth2 credentials are stored in:
31
+ - `~/.heathrow/mail/[email].json` - Client credentials
32
+ - `~/.heathrow/mail/[email].txt` - Refresh token
33
+
34
+ The SMTP script handles token refresh automatically.
35
+
36
+ ### Other Email Accounts
37
+
38
+ For non-OAuth2 accounts, configure SMTP settings when adding the source:
39
+ - SMTP server address
40
+ - SMTP port (usually 587 for TLS, 25 for plain)
41
+ - Username and password
42
+
43
+ ## Message Templates
44
+
45
+ ### Reply Template
46
+ ```
47
+ To: original-sender@example.com
48
+ Subject: Re: Original Subject
49
+ # Lines starting with # will be ignored
50
+ # Write your message below this line
51
+ #==================================================
52
+
53
+ [Your reply here]
54
+
55
+ On 2025-08-25, sender wrote:
56
+ > Original message quoted
57
+ > with > prefix
58
+ ```
59
+
60
+ ### Reply All Template
61
+ ```
62
+ To: sender@example.com, recipient1@example.com, recipient2@example.com
63
+ Subject: Re: Original Subject
64
+ [Rest same as reply]
65
+ ```
66
+
67
+ ### Forward Template
68
+ ```
69
+ To: [Enter recipients]
70
+ Subject: Fwd: Original Subject
71
+ # Lines starting with # will be ignored
72
+ # Enter recipients and write your message
73
+ #==================================================
74
+
75
+ [Your introduction]
76
+
77
+ ---------- Forwarded message ----------
78
+ From: original-sender@example.com
79
+ Date: 2025-08-25
80
+ Subject: Original Subject
81
+
82
+ [Original message content]
83
+ ```
84
+
85
+ ## Supported Sources
86
+
87
+ Sources with reply capability:
88
+ - **Gmail** - Full OAuth2 support via configurable SMTP command
89
+ - **IMAP** - Uses OAuth2 SMTP for supported domains or configured SMTP
90
+ - **Discord** - Bot/user messages to channels
91
+ - **Telegram** - Bot messages to chats
92
+
93
+ ## Key Bindings
94
+
95
+ | Key | Action | Description |
96
+ |-----|--------|-------------|
97
+ | `r` | Reply | Reply to sender only |
98
+ | `R` | Reply All | Reply to all recipients |
99
+ | `f` | Forward | Forward to new recipients |
100
+ | `m` | Mail/Compose | Create new message |
101
+ | `Ctrl-R` | Refresh | Refresh all panes |
102
+
103
+ ## Editor Tips
104
+
105
+ ### Vim Configuration
106
+ Your vim is configured with:
107
+ ```vim
108
+ setlocal tw=0 fo-=tcal ff=unix
109
+ ```
110
+
111
+ This ensures:
112
+ - No automatic text wrapping
113
+ - Unix line endings
114
+ - Proper email formatting
115
+
116
+ ### Canceling a Message
117
+ - Exit without saving: `:q!` in vim
118
+ - Or make no changes and save (detected as cancel)
119
+
120
+ ## Troubleshooting
121
+
122
+ ### Message Not Sending
123
+
124
+ 1. Check `~/.heathrow/mail/.smtp.log` for errors
125
+ 2. Verify OAuth2 credentials exist for your email
126
+ 3. For new domains, add to `OAUTH2_DOMAINS` in `smtp_sender.rb`
127
+
128
+ ### OAuth2 Token Issues
129
+
130
+ If you see token errors:
131
+ ```bash
132
+ # Regenerate token
133
+ oauth2.py --generate_oauth2_token \
134
+ --client_id=[your-client-id] \
135
+ --client_secret=[your-secret] \
136
+ --refresh_token=[your-refresh-token]
137
+ ```
138
+
139
+ ### SMTP Server Issues
140
+
141
+ For non-OAuth2 accounts, verify:
142
+ - SMTP server address is correct
143
+ - Port is appropriate (587 for TLS, 25 for plain)
144
+ - Username/password are correct
145
+ - Firewall allows outgoing SMTP
146
+
147
+ ## Testing
148
+
149
+ Test the SMTP module:
150
+ ```bash
151
+ ruby test_smtp_module.rb
152
+ ```
153
+
154
+ Test sending from a specific source:
155
+ ```bash
156
+ ruby test_gmail_send.rb
157
+ ```
158
+
159
+ ## Implementation Details
160
+
161
+ The reply/forward system consists of:
162
+
163
+ 1. **MessageComposer** (`lib/heathrow/message_composer.rb`)
164
+ - Creates templates
165
+ - Launches editor
166
+ - Parses composed messages
167
+
168
+ 2. **SmtpSender** (`lib/heathrow/smtp_sender.rb`)
169
+ - Routes to OAuth2 SMTP for configured domains
170
+ - Falls back to standard SMTP for others
171
+ - Handles error reporting
172
+
173
+ 3. **Source Integration**
174
+ - Each source implements `can_reply?` and `send_message`
175
+ - Threading headers (In-Reply-To, References) preserved
176
+
177
+ ## Security Notes
178
+
179
+ - OAuth2 tokens stored in `~/.heathrow/mail/`
180
+ - No passwords stored in Heathrow database for OAuth2 accounts
181
+ - Temporary message files deleted after sending
182
+ - SMTP passwords encrypted in source config
@@ -0,0 +1,306 @@
1
+ # WhatsApp and Telegram Setup Guide for Heathrow
2
+
3
+ This guide helps you set up WhatsApp and Telegram sources in Heathrow to receive messages from these platforms.
4
+
5
+ ## WhatsApp Setup
6
+
7
+ WhatsApp integration requires a separate API server that implements the WhatsApp Web protocol.
8
+
9
+ ### Prerequisites
10
+
11
+ 1. **Go Language** - Required for the WhatsApp API server
12
+ ```bash
13
+ sudo apt install golang-go
14
+ ```
15
+
16
+ 2. **WhatsApp API Server** - A REST API wrapper for WhatsApp Web
17
+ - The server needs to be running before setting up the source
18
+ - Default port: 8080
19
+
20
+ ### Quick Setup
21
+
22
+ 1. Start the WhatsApp API server (in a separate terminal):
23
+ ```bash
24
+ # Clone and run the server
25
+ git clone [whatsmeow-api-repo]
26
+ cd whatsmeow-api
27
+ go run main.go
28
+ ```
29
+
30
+ 2. Run the WhatsApp setup script:
31
+ ```bash
32
+ ruby setup_whatsapp.rb
33
+ ```
34
+
35
+ 3. Choose authentication method:
36
+ - **QR Code** (default): Scan with WhatsApp mobile app
37
+ - **Pairing Code**: Enter code in WhatsApp settings
38
+
39
+ ### Authentication Methods
40
+
41
+ #### QR Code Method
42
+ 1. Open WhatsApp on your phone
43
+ 2. Go to Settings → Linked Devices
44
+ 3. Tap "Link a Device"
45
+ 4. Scan the QR code displayed in terminal
46
+
47
+ #### Pairing Code Method
48
+ 1. Provide your phone number during setup
49
+ 2. Receive an 8-digit pairing code
50
+ 3. In WhatsApp: Settings → Linked Devices → Link with phone number
51
+ 4. Enter the pairing code
52
+
53
+ ### Configuration
54
+
55
+ The WhatsApp source supports these settings:
56
+
57
+ ```ruby
58
+ {
59
+ api_url: 'http://localhost:8080', # API server URL
60
+ device_id: 'unique_device_id', # Device identifier
61
+ fetch_limit: 50, # Messages per fetch
62
+ incremental_sync: true, # Only fetch new messages
63
+ use_pairing_code: false, # Use QR code by default
64
+ phone_number: '1234567890', # For pairing code only
65
+ polling_interval: 60 # Check every minute
66
+ }
67
+ ```
68
+
69
+ ### Message Types Supported
70
+
71
+ - Text messages
72
+ - Images, videos, audio
73
+ - Documents and files
74
+ - Location sharing
75
+ - Contact cards
76
+ - Stickers
77
+ - Group messages
78
+
79
+ ## Telegram Setup
80
+
81
+ Telegram offers two integration methods: Bot API (simple) or User Account (full access).
82
+
83
+ ### Method 1: Telegram Bot (Recommended for Start)
84
+
85
+ #### Creating a Bot
86
+
87
+ 1. Open Telegram and search for **@BotFather**
88
+ 2. Send `/newbot` command
89
+ 3. Choose a name for your bot
90
+ 4. Choose a username (must end in 'bot')
91
+ 5. Copy the bot token provided
92
+
93
+ #### Setup
94
+
95
+ Run the Telegram setup script:
96
+ ```bash
97
+ ruby setup_telegram.rb
98
+ ```
99
+
100
+ Choose option 1 (Bot) and enter your bot token.
101
+
102
+ #### Bot Limitations
103
+
104
+ - Only receives messages sent directly to the bot
105
+ - Cannot access your personal chats
106
+ - Users must start conversation with `/start`
107
+ - Cannot see messages in groups unless mentioned
108
+
109
+ ### Method 2: User Account (Full Access)
110
+
111
+ #### Prerequisites
112
+
113
+ 1. Get API credentials from https://my.telegram.org:
114
+ - Log in with your phone number
115
+ - Go to "API development tools"
116
+ - Create an app (if needed)
117
+ - Copy the **api_id** and **api_hash**
118
+
119
+ 2. For full functionality, you need an MTProto proxy server (advanced)
120
+
121
+ #### Setup
122
+
123
+ Run the setup script and choose option 2:
124
+ ```bash
125
+ ruby setup_telegram.rb
126
+ ```
127
+
128
+ Provide:
129
+ - API ID
130
+ - API Hash
131
+ - Phone number (with country code)
132
+
133
+ #### Authentication Process
134
+
135
+ 1. You'll receive a code in your Telegram app
136
+ 2. Enter the code when prompted
137
+ 3. If you have 2FA enabled, enter your password
138
+ 4. Session is saved for future use
139
+
140
+ ### Configuration Options
141
+
142
+ #### Bot Configuration
143
+ ```ruby
144
+ {
145
+ bot_token: 'YOUR_BOT_TOKEN',
146
+ fetch_limit: 100,
147
+ polling_interval: 60
148
+ }
149
+ ```
150
+
151
+ #### User Account Configuration
152
+ ```ruby
153
+ {
154
+ api_id: 'YOUR_API_ID',
155
+ api_hash: 'YOUR_API_HASH',
156
+ phone_number: '+1234567890',
157
+ session_string: 'saved_after_auth',
158
+ mtproto_api_url: 'http://localhost:8081'
159
+ }
160
+ ```
161
+
162
+ ### Message Types Supported
163
+
164
+ - Text messages
165
+ - Photos and videos
166
+ - Voice messages and audio
167
+ - Documents and files
168
+ - Stickers and GIFs
169
+ - Location sharing
170
+ - Contact cards
171
+ - Channel posts
172
+ - Group messages
173
+
174
+ ## Testing the Sources
175
+
176
+ After setup, test your sources:
177
+
178
+ 1. In Heathrow, press `s` to view sources
179
+ 2. Navigate to WhatsApp or Telegram source
180
+ 3. Press `t` to test connection
181
+ 4. Check for success message
182
+
183
+ ## Troubleshooting
184
+
185
+ ### WhatsApp Issues
186
+
187
+ **"API server not running"**
188
+ - Ensure the WhatsApp API server is running on port 8080
189
+ - Check with: `curl http://localhost:8080/health`
190
+
191
+ **"Device not authenticated"**
192
+ - Re-run the setup script
193
+ - Try QR code method if pairing code fails
194
+ - Ensure your phone has internet connection
195
+
196
+ **"No messages fetched"**
197
+ - WhatsApp Web must stay connected
198
+ - Check phone's internet connection
199
+ - Verify in WhatsApp: Settings → Linked Devices
200
+
201
+ ### Telegram Issues
202
+
203
+ **Bot not receiving messages**
204
+ - Users must start conversation with `/start`
205
+ - In groups, bot needs to be admin or mentioned
206
+ - Check bot token is correct
207
+
208
+ **User account authentication fails**
209
+ - Verify api_id and api_hash are correct
210
+ - Phone number must include country code
211
+ - Code expires after 5 minutes
212
+ - 2FA password is case-sensitive
213
+
214
+ **"Session expired"**
215
+ - Re-run authentication process
216
+ - Session strings expire after long inactivity
217
+
218
+ ## Security Notes
219
+
220
+ ### WhatsApp
221
+ - Messages are end-to-end encrypted
222
+ - Device sessions persist until manually disconnected
223
+ - Each device has unique encryption keys
224
+ - Disconnect unused devices from WhatsApp settings
225
+
226
+ ### Telegram
227
+ - Bot tokens should be kept secret
228
+ - API credentials are sensitive - don't share
229
+ - Session strings grant full account access
230
+ - Use environment variables for credentials in production
231
+
232
+ ## Advanced Configuration
233
+
234
+ ### Running API Servers as Services
235
+
236
+ #### WhatsApp API Service
237
+ Create `/etc/systemd/system/whatsmeow-api.service`:
238
+ ```ini
239
+ [Unit]
240
+ Description=WhatsApp API Server
241
+ After=network.target
242
+
243
+ [Service]
244
+ Type=simple
245
+ User=your-user
246
+ WorkingDirectory=/path/to/whatsmeow-api
247
+ ExecStart=/usr/bin/go run main.go
248
+ Restart=on-failure
249
+
250
+ [Install]
251
+ WantedBy=multi-user.target
252
+ ```
253
+
254
+ #### Start the service:
255
+ ```bash
256
+ sudo systemctl enable whatsmeow-api
257
+ sudo systemctl start whatsmeow-api
258
+ ```
259
+
260
+ ### Multiple Accounts
261
+
262
+ You can add multiple WhatsApp or Telegram sources:
263
+ - Use different device_ids for WhatsApp
264
+ - Use different bot tokens or phone numbers for Telegram
265
+ - Each source syncs independently
266
+
267
+ ### Filtering and Rules
268
+
269
+ Configure message filtering in Heathrow:
270
+ - Create views for specific contacts
271
+ - Filter by sender or group
272
+ - Set up notifications for important messages
273
+
274
+ ## API Server Requirements
275
+
276
+ ### WhatsApp (whatsmeow)
277
+ - Go 1.19 or higher
278
+ - SQLite for session storage
279
+ - ~50MB RAM per connected device
280
+ - Persistent storage for media cache
281
+
282
+ ### Telegram (MTProto)
283
+ - Python 3.8+ or Go 1.19+
284
+ - Session storage (file or database)
285
+ - ~30MB RAM per session
286
+ - Optional media download storage
287
+
288
+ ## Rate Limits
289
+
290
+ ### WhatsApp
291
+ - No official rate limits for receiving
292
+ - Sending limited to prevent spam
293
+ - Reconnection backoff on disconnects
294
+
295
+ ### Telegram
296
+ - Bot API: 30 messages/second
297
+ - MTProto: Adaptive rate limiting
298
+ - Respect flood wait errors
299
+
300
+ ## Next Steps
301
+
302
+ 1. Start Heathrow and verify sources appear
303
+ 2. Monitor initial message sync
304
+ 3. Configure polling intervals as needed
305
+ 4. Set up views for different platforms
306
+ 5. Consider running API servers as background services
data/heathrow.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ require_relative 'lib/heathrow/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'heathrow'
5
+ spec.version = Heathrow::VERSION
6
+ spec.authors = ['Geir Isene', 'Claude Code']
7
+ spec.email = ['g@isene.com']
8
+
9
+ spec.summary = 'Communication Hub In The Terminal'
10
+ spec.description = 'A unified TUI application for managing all your communication sources in one place. Brings together emails, WhatsApp, Discord, Reddit, RSS feeds, and more into a single, efficient terminal interface.'
11
+ spec.homepage = 'https://github.com/isene/heathrow'
12
+ spec.license = 'Unlicense'
13
+
14
+ spec.required_ruby_version = '>= 2.7.0'
15
+
16
+ spec.metadata['homepage_uri'] = spec.homepage
17
+ spec.metadata['source_code_uri'] = 'https://github.com/isene/heathrow'
18
+ spec.metadata['changelog_uri'] = 'https://github.com/isene/heathrow/blob/master/CHANGELOG.md'
19
+
20
+ # Specify which files should be added to the gem
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ f.match(%r{\A(?:test|spec|features)/})
24
+ end
25
+ end
26
+
27
+ spec.bindir = 'bin'
28
+ spec.executables = ['heathrow']
29
+ spec.require_paths = ['lib']
30
+
31
+ # Runtime dependencies - keep it simple!
32
+ spec.add_runtime_dependency 'rcurses', '>= 5.0'
33
+ spec.add_runtime_dependency 'sqlite3', '>= 1.4'
34
+ end
data/heathrowd.service ADDED
@@ -0,0 +1,21 @@
1
+ [Unit]
2
+ Description=Heathrow Daemon - Communication Hub Polling Service
3
+ After=network.target
4
+
5
+ [Service]
6
+ Type=simple
7
+ User=%i
8
+ ExecStart=/usr/local/bin/heathrowd start --daemon --interval 30
9
+ ExecStop=/usr/local/bin/heathrowd stop
10
+ Restart=always
11
+ RestartSec=10
12
+
13
+ # Security settings
14
+ NoNewPrivileges=yes
15
+ PrivateTmp=yes
16
+ ProtectSystem=strict
17
+ ProtectHome=yes
18
+ ReadWritePaths=%h/.heathrow
19
+
20
+ [Install]
21
+ WantedBy=multi-user.target
data/img/heathrow.svg ADDED
@@ -0,0 +1,95 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" style="stop-color:#1a1a2e"/>
5
+ <stop offset="100%" style="stop-color:#0f0f1a"/>
6
+ </linearGradient>
7
+ <linearGradient id="glow" x1="0%" y1="0%" x2="0%" y2="100%">
8
+ <stop offset="0%" style="stop-color:#e2b714"/>
9
+ <stop offset="100%" style="stop-color:#c49000"/>
10
+ </linearGradient>
11
+ </defs>
12
+
13
+ <style>
14
+ @keyframes pulse { 0%,100% { opacity: 0.5; } 50% { opacity: 1; } }
15
+ @keyframes spin { from { transform-origin: 128px 128px; transform: rotate(0deg); } to { transform-origin: 128px 128px; transform: rotate(360deg); } }
16
+ @keyframes dot-travel {
17
+ 0% { opacity: 0; }
18
+ 10% { opacity: 1; }
19
+ 90% { opacity: 1; }
20
+ 100% { opacity: 0; }
21
+ }
22
+ .hub-ring { animation: pulse 3s ease-in-out infinite; }
23
+ .outer-ring { animation: spin 30s linear infinite; }
24
+ .dot1 { animation: dot-travel 2.5s ease-in-out infinite; }
25
+ .dot2 { animation: dot-travel 2.5s ease-in-out 0.4s infinite; }
26
+ .dot3 { animation: dot-travel 2.5s ease-in-out 0.8s infinite; }
27
+ .dot4 { animation: dot-travel 2.5s ease-in-out 1.2s infinite; }
28
+ .dot5 { animation: dot-travel 2.5s ease-in-out 1.6s infinite; }
29
+ .dot6 { animation: dot-travel 2.5s ease-in-out 2.0s infinite; }
30
+ </style>
31
+
32
+ <!-- Background circle -->
33
+ <circle cx="128" cy="128" r="120" fill="url(#bg)" stroke="#e2b714" stroke-width="3"/>
34
+
35
+ <!-- Outer decorative ring (slow spin) -->
36
+ <circle class="outer-ring" cx="128" cy="128" r="95" fill="none" stroke="#e2b714" stroke-width="0.5" stroke-dasharray="8 12" opacity="0.3"/>
37
+
38
+ <!-- Hub ring (pulsing) -->
39
+ <circle class="hub-ring" cx="128" cy="128" r="38" fill="none" stroke="url(#glow)" stroke-width="3"/>
40
+ <!-- Hub center dot -->
41
+ <circle cx="128" cy="128" r="6" fill="#e2b714" opacity="0.9"/>
42
+
43
+ <!-- Spokes with traveling dots -->
44
+
45
+ <!-- Top: Email (gold) -->
46
+ <line x1="128" y1="90" x2="128" y2="52" stroke="#e2b714" stroke-width="1.5" opacity="0.4"/>
47
+ <circle class="dot1" cx="128" cy="70" r="2" fill="#e2b714"/>
48
+ <rect x="116" y="34" width="24" height="16" rx="2" fill="none" stroke="#e2b714" stroke-width="2"/>
49
+ <polyline points="116,36 128,44 140,36" fill="none" stroke="#e2b714" stroke-width="1.5"/>
50
+
51
+ <!-- Top-right: Chat (blue) -->
52
+ <line x1="155" y1="100" x2="185" y2="66" stroke="#39c" stroke-width="1.5" opacity="0.4"/>
53
+ <circle class="dot2" cx="170" cy="83" r="2" fill="#39c"/>
54
+ <rect x="178" y="48" width="24" height="17" rx="4" fill="none" stroke="#39c" stroke-width="2"/>
55
+ <circle cx="186" cy="57" r="1.5" fill="#39c"/>
56
+ <circle cx="190" cy="57" r="1.5" fill="#39c"/>
57
+ <circle cx="194" cy="57" r="1.5" fill="#39c"/>
58
+
59
+ <!-- Right: RSS (orange) -->
60
+ <line x1="166" y1="128" x2="206" y2="128" stroke="#f80" stroke-width="1.5" opacity="0.4"/>
61
+ <circle class="dot3" cx="186" cy="128" r="2" fill="#f80"/>
62
+ <circle cx="220" cy="128" r="3" fill="#f80"/>
63
+ <path d="M212,119 A14,14 0 0,1 228,135" fill="none" stroke="#f80" stroke-width="2.5"/>
64
+ <path d="M208,113 A22,22 0 0,1 234,139" fill="none" stroke="#f80" stroke-width="1.5"/>
65
+
66
+ <!-- Bottom-right: Discord (purple) -->
67
+ <line x1="155" y1="156" x2="184" y2="188" stroke="#77d" stroke-width="1.5" opacity="0.4"/>
68
+ <circle class="dot4" cx="170" cy="172" r="2" fill="#77d"/>
69
+ <text x="186" y="200" font-family="sans-serif" font-size="22" font-weight="bold" fill="#77d">#</text>
70
+
71
+ <!-- Bottom: Terminal (green) -->
72
+ <line x1="128" y1="166" x2="128" y2="204" stroke="#4c4" stroke-width="1.5" opacity="0.4"/>
73
+ <circle class="dot5" cx="128" cy="186" r="2" fill="#4c4"/>
74
+ <rect x="110" y="206" width="36" height="24" rx="3" fill="none" stroke="#4c4" stroke-width="2"/>
75
+ <text x="116" y="224" font-family="monospace" font-size="12" fill="#4c4">&gt;_</text>
76
+
77
+ <!-- Bottom-left: Phone (teal) -->
78
+ <line x1="101" y1="156" x2="72" y2="188" stroke="#5b5" stroke-width="1.5" opacity="0.4"/>
79
+ <circle class="dot6" cx="86" cy="172" r="2" fill="#5b5"/>
80
+ <rect x="58" y="188" width="18" height="28" rx="3" fill="none" stroke="#5b5" stroke-width="2"/>
81
+ <circle cx="67" cy="212" r="2" fill="#5b5"/>
82
+
83
+ <!-- Left: Forum (red) -->
84
+ <line x1="90" y1="128" x2="50" y2="128" stroke="#e55" stroke-width="1.5" opacity="0.4"/>
85
+ <circle cx="38" cy="128" r="12" fill="none" stroke="#e55" stroke-width="2"/>
86
+ <circle cx="34" cy="124" r="2" fill="#e55"/>
87
+ <circle cx="42" cy="124" r="2" fill="#e55"/>
88
+ <path d="M33,131 Q38,136 43,131" fill="none" stroke="#e55" stroke-width="1.5"/>
89
+
90
+ <!-- Top-left: Web (lavender) -->
91
+ <line x1="101" y1="100" x2="72" y2="68" stroke="#a8e" stroke-width="1.5" opacity="0.4"/>
92
+ <circle cx="60" cy="56" r="14" fill="none" stroke="#a8e" stroke-width="2"/>
93
+ <ellipse cx="60" cy="56" rx="6" ry="14" fill="none" stroke="#a8e" stroke-width="1"/>
94
+ <line x1="46" y1="56" x2="74" y2="56" stroke="#a8e" stroke-width="1"/>
95
+ </svg>
Binary file
data/img/sources.png ADDED
Binary file
@@ -0,0 +1,42 @@
1
+ module Heathrow
2
+ class AddressBook
3
+ attr_reader :aliases
4
+
5
+ def initialize(path = File.expand_path('~/setup/addressbook'))
6
+ @aliases = {}
7
+ parse(path) if File.exist?(path)
8
+ end
9
+
10
+ def parse(path)
11
+ File.readlines(path).each do |line|
12
+ next unless line =~ /^alias\s+(\S+)\s+(.+)/
13
+ @aliases[$1] = $2.strip
14
+ end
15
+ end
16
+
17
+ # Search aliases and addresses by query string
18
+ def lookup(query)
19
+ query_down = query.downcase
20
+ @aliases.select { |k, v| k.include?(query_down) || v.downcase.include?(query_down) }
21
+ end
22
+
23
+ # Expand an alias name to full address, or return input unchanged
24
+ def expand(name)
25
+ @aliases[name] || name
26
+ end
27
+
28
+ # Get all alias names
29
+ def names
30
+ @aliases.keys.sort
31
+ end
32
+
33
+ # Get completion candidates for a partial string
34
+ def complete(partial)
35
+ return [] if partial.nil? || partial.empty?
36
+ partial_down = partial.downcase
37
+ @aliases.select { |k, v|
38
+ k.start_with?(partial_down) || v.downcase.include?(partial_down)
39
+ }.map { |k, v| "#{k} → #{v}" }
40
+ end
41
+ end
42
+ end