A production-grade Text User Interface (TUI) application for blocking distracting websites on Linux and macOS. Built with Go and Bubble Tea.
- ✅ Cross-platform: Runs on Linux (Arch Linux) and macOS
- ✅ Safe /etc/hosts modification: Uses markers to isolate changes
- ✅ Wildcard support: Block patterns like
*.linkedin.* - ✅ Multiple durations: 5min, 15min, 1h, 4h, 6h, 8h
- ✅ Persistent state: Sessions survive app restarts
- ✅ Live countdown: Real-time timer display
- ✅ Multi-select delete: Remove multiple URLs at once
- ✅ Automatic unblocking: Blocks removed when timer expires
The application modifies /etc/hosts to block websites by redirecting them to 127.0.0.1 (localhost). All modifications are isolated between markers:
# BEGIN SELFCONTROL-TUI
127.0.0.1 linkedin.com
127.0.0.1 www.linkedin.com
::1 linkedin.com
::1 www.linkedin.com
# END SELFCONTROL-TUI
Safety guarantees:
- Never modifies existing lines in
/etc/hosts - Only removes lines added by this application
- Blocks both IPv4 and IPv6
When you add a wildcard pattern like *.linkedin.*, the application expands it to common variations:
*.example.*→example.com,www.example.com,example.net,www.example.net, etc.*.example.com→example.com,www.example.com,m.example.com,mobile.example.com, etc.
State is stored in $HOME/.config/selfcontrol-tui/state.json containing:
- List of blocked URLs
- Active session (if any) with end timestamp
When you reopen the TUI:
- Active sessions are restored with accurate countdown
- Expired sessions are automatically unblocked
- All URLs persist across restarts
The daemon (selfcontrol-daemon) runs in the background to automatically unblock websites when timers expire, even if the TUI is closed.
- Go 1.21 or later
- Root/sudo access (required for
/etc/hostsmodification)
# Clone the repository
git clone https://github.com/phil/selfcontrol
cd selfcontrol
# Download dependencies
go mod download
# Build the main application
go build -o selfcontrol cmd/selfcontrol/main.go
# Build the daemon (optional but recommended)
go build -o selfcontrol-daemon cmd/selfcontrol-daemon/main.go
# Install to system (optional)
sudo cp selfcontrol /usr/local/bin/
sudo cp selfcontrol-daemon /usr/local/bin/# Build both binaries
go build -o selfcontrol ./cmd/selfcontrol
go build -o selfcontrol-daemon ./cmd/selfcontrol-daemon# Run with sudo (required for /etc/hosts modification)
sudo ./selfcontrol
# Or if installed system-wide
sudo selfcontrolMain View:
a- Add URL or patternd- Delete URLs (multi-select mode)s- Start blocking sessionq- Quit
Add URL View:
- Type URL or pattern
Enter- Add URLEsc- Cancel
Delete Mode:
↑/↓orj/k- NavigateSpace- Toggle selectionEnter- Delete selected URLsEsc- Cancel
Select Duration:
↑/↓orj/k- NavigateEnter- Start session with selected durationEsc- Cancel
The daemon ensures websites are automatically unblocked when timers expire, even if the TUI is closed.
Create /etc/systemd/system/selfcontrol-daemon.service:
[Unit]
Description=SelfControl Daemon
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/selfcontrol-daemon
Restart=always
User=root
[Install]
WantedBy=multi-user.targetEnable and start:
sudo systemctl enable selfcontrol-daemon
sudo systemctl start selfcontrol-daemon
sudo systemctl status selfcontrol-daemonCreate /Library/LaunchDaemons/com.selfcontrol.daemon.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.selfcontrol.daemon</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/selfcontrol-daemon</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/var/log/selfcontrol-daemon.log</string>
<key>StandardOutPath</key>
<string>/var/log/selfcontrol-daemon.log</string>
</dict>
</plist>Load the daemon:
sudo cp selfcontrol-daemon /usr/local/bin/
sudo launchctl load /Library/LaunchDaemons/com.selfcontrol.daemon.plist
sudo launchctl start com.selfcontrol.daemonBubble Tea - Chosen for its:
- Robust terminal handling
- Clean, composable architecture (Elm-inspired)
- Active development and community
- Excellent documentation
- Built-in utilities (textinput, etc.)
selfcontrol/
├── cmd/
│ ├── selfcontrol/ # Main TUI application
│ │ └── main.go
│ └── selfcontrol-daemon/ # Background daemon
│ └── main.go
├── internal/
│ ├── blocker/ # /etc/hosts manipulation
│ │ └── blocker.go
│ ├── state/ # Persistence logic
│ │ └── state.go
│ ├── timer/ # Timer utilities
│ │ └── timer.go
│ └── ui/ # Bubble Tea UI
│ └── ui.go
├── go.mod
└── README.md
Location: $HOME/.config/selfcontrol-tui/state.json
Example:
{
"urls": [
"linkedin.com",
"*.reddit.*",
"twitter.com"
],
"active_session": {
"end_time": "2025-12-05T15:30:00Z",
"duration": "1 hour",
"start_time": "2025-12-05T14:30:00Z"
}
}The blocker expands wildcards intelligently:
-
*.domain.*- Expands to multiple TLDs:domain.com,www.domain.comdomain.net,www.domain.netdomain.org,domain.io, etc.
-
*.domain.com- Expands to common subdomains:domain.com,www.domain.comm.domain.com,mobile.domain.comapi.domain.com,app.domain.com, etc.
-
domain.com- Direct match:domain.com,www.domain.com
-
During active session:
- TUI updates countdown every second
- State persisted with
end_timetimestamp
-
When TUI is closed:
- Blocking remains active in
/etc/hosts - Daemon checks every 10 seconds for expired sessions
- Blocking remains active in
-
When TUI reopens:
- Loads state from disk
- Calculates remaining time from
end_time - Auto-unblocks if expired
-
Automatic unblocking:
- Daemon removes markers from
/etc/hosts - State cleared from disk
- No manual intervention needed
- Daemon removes markers from
1. Run: sudo selfcontrol
2. Press 'a'
3. Type: *.linkedin.*
4. Press Enter
5. Press 's'
6. Select duration (e.g., "1 hour")
7. Press Enter
Now linkedin.com, www.linkedin.com, and all subdomains are blocked for 1 hour.
1. Add: *.linkedin.*
2. Add: *.reddit.*
3. Add: twitter.com
4. Start session
If you started a session and closed the TUI, simply reopen it:
sudo selfcontrolYou'll see the remaining time and active blocking status.
Error: failed to write hosts file (are you running with sudo?)
Solution: Run with sudo:
sudo selfcontrolCheck daemon status:
Linux:
sudo systemctl status selfcontrol-daemon
journalctl -u selfcontrol-daemon -fmacOS:
sudo launchctl list | grep selfcontrol
tail -f /var/log/selfcontrol-daemon.logIf you need to manually remove blocking:
# Open hosts file
sudo nano /etc/hosts
# Remove lines between:
# BEGIN SELFCONTROL-TUI
# ... (remove these lines)
# END SELFCONTROL-TUI
# Save and exitOr use the blocker directly:
# In Go code
go run -exec sudo ./test-unblock.goTo completely reset the application:
# Remove state file
rm ~/.config/selfcontrol-tui/state.json
# Manually clean hosts file (if needed)
sudo nano /etc/hosts # Remove SelfControl sectiongo test ./...internal/state: JSON persistence, session managementinternal/blocker: Safe/etc/hostsmanipulation, wildcard expansioninternal/timer: Duration formatting, predefined durationsinternal/ui: Bubble Tea models, views, and update logic
- New duration: Edit
timer.PredefinedDurations() - New wildcard pattern: Edit
blocker.expandWildcards() - New view: Add mode to
ui.viewModeand implement handlers
- Root required: Modifying
/etc/hostsrequires root privileges - Safe isolation: Only modifies lines between markers
- No network access: Purely local file manipulation
- Transparent operation: All changes visible in
/etc/hosts
MIT License - See LICENSE file for details
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Submit a pull request
Built with:
- Bubble Tea - TUI framework
- Bubbles - TUI components
- Lipgloss - Terminal styling
Inspired by the original SelfControl for macOS.