Skip to content

Instantly share code, notes, and snippets.

@gingerbeardman
Created January 12, 2026 21:06
Show Gist options
  • Select an option

  • Save gingerbeardman/a81df96cd0b4c7a397b04711cafeb287 to your computer and use it in GitHub Desktop.

Select an option

Save gingerbeardman/a81df96cd0b4c7a397b04711cafeb287 to your computer and use it in GitHub Desktop.
HTTP Server Control as an menubar app using xbar
#!/usr/bin/env python3
# <xbar.title>HTTP Server Control</xbar.title>
# <xbar.version>3.0.0</xbar.version>
# <xbar.author>Matt Sephton</xbar.author>
# <xbar.author.github>gingerbeardman</xbar.author.github>
# <xbar.desc>Start and stop a Python HTTP servers from a specific directory</xbar.desc>
# <xbar.dependencies>python3</xbar.dependencies>
import os
import subprocess
import sys
import signal
import http.server
import time
import json
import hashlib
# Configuration
CONFIG_FILE = os.path.expanduser("~/Library/Application Support/xbar/plugins/.xbar_httpd_config")
PID_FILE = "/tmp/xbar_httpd_pids.json" # Now stores multiple PIDs
LOG_DIR = "/tmp/xbar_httpd_logs"
DEFAULT_SERVER_DIR = os.path.expanduser("~/Sites")
DEFAULT_SERVER_PORT = 8000
DEFAULT_ENABLE_LOG = True
def get_path_id(path):
"""Generate a short unique ID for a path"""
return hashlib.md5(path.encode()).hexdigest()[:8]
def get_all_server_dirs():
"""Get all SERVER_DIR entries from config"""
if not os.path.exists(CONFIG_FILE):
return []
dirs = []
with open(CONFIG_FILE, 'r') as f:
for line in f:
line = line.strip()
# All SERVER_DIR lines are treated equally (no active/commented distinction)
if line.startswith('SERVER_DIR='):
path = os.path.expanduser(line.split('=', 1)[1].strip())
dirs.append(path)
return dirs
def load_config():
"""Load configuration from file or create default"""
if os.path.exists(CONFIG_FILE):
config = {}
with open(CONFIG_FILE, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
if '=' in line:
key, value = line.split('=', 1)
config[key.strip()] = value.strip()
base_port = int(config.get('SERVER_PORT', DEFAULT_SERVER_PORT))
enable_log = config.get('ENABLE_LOG', str(DEFAULT_ENABLE_LOG)).lower() in ('true', '1', 'yes')
else:
# Create default config file
base_port = DEFAULT_SERVER_PORT
enable_log = DEFAULT_ENABLE_LOG
with open(CONFIG_FILE, 'w') as f:
f.write(f"# xbar HTTP Server Configuration\n")
f.write(f"# Multiple SERVER_DIR entries - each gets its own server on sequential ports\n")
f.write(f"# First directory uses SERVER_PORT, second uses SERVER_PORT+1, etc.\n")
f.write(f"SERVER_DIR={DEFAULT_SERVER_DIR}\n")
f.write(f"SERVER_PORT={base_port}\n")
f.write(f"ENABLE_LOG={enable_log}\n")
return base_port, enable_log
BASE_PORT, ENABLE_LOG = load_config()
def load_pids():
"""Load PID mapping from JSON file"""
if not os.path.exists(PID_FILE):
return {}
try:
with open(PID_FILE, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, ValueError):
return {}
def save_pids(pids):
"""Save PID mapping to JSON file"""
with open(PID_FILE, 'w') as f:
json.dump(pids, f, indent=2)
def get_server_info(path):
"""Get server info (PID, port) for a path, return None if not running"""
pids = load_pids()
path_id = get_path_id(path)
if path_id in pids:
pid = pids[path_id]['pid']
port = pids[path_id]['port']
# Verify process is still running
try:
os.kill(pid, 0)
return {'pid': pid, 'port': port}
except OSError:
# Process is dead, clean up
del pids[path_id]
save_pids(pids)
return None
def is_server_running_for_path(path):
"""Check if server for specific path is running"""
return get_server_info(path) is not None
def get_assigned_port(path, all_dirs):
"""Get the assigned port for a directory based on its index"""
try:
index = all_dirs.index(path)
return BASE_PORT + index
except ValueError:
return BASE_PORT
class NoCacheHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
"""Custom HTTP request handler that disables caching"""
def end_headers(self):
self.send_cache_control_headers()
http.server.SimpleHTTPRequestHandler.end_headers(self)
def send_cache_control_headers(self):
"""Send headers to disable caching"""
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
self.send_header("Pragma", "no-cache")
self.send_header("Expires", "0")
def start_server_for_folder(folder_path, port):
"""Start HTTP server for a specific folder on a specific port"""
if not os.path.exists(folder_path):
print(f"Error: Directory {folder_path} does not exist")
return False
# Check if already running
if is_server_running_for_path(folder_path):
print(f"Server already running for {folder_path}")
return False
path_id = get_path_id(folder_path)
# Create a unique server script for this folder
server_script = f"/tmp/xbar_httpd_server_{path_id}.py"
with open(server_script, 'w') as f:
f.write(f'''#!/usr/bin/env python3
import http.server
import socketserver
import os
class NoCacheHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
"""Custom HTTP request handler that disables caching"""
def end_headers(self):
self.send_cache_control_headers()
http.server.SimpleHTTPRequestHandler.end_headers(self)
def send_cache_control_headers(self):
"""Send headers to disable caching"""
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
self.send_header("Pragma", "no-cache")
self.send_header("Expires", "0")
os.chdir("{folder_path}")
PORT = {port}
class ReusableTCPServer(socketserver.TCPServer):
allow_reuse_address = True
with ReusableTCPServer(("", PORT), NoCacheHTTPRequestHandler) as httpd:
print(f"Serving at port {{PORT}} with caching disabled")
httpd.serve_forever()
''')
# Ensure log directory exists
if ENABLE_LOG and not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
# Determine where to redirect output
if ENABLE_LOG:
log_file_path = os.path.join(LOG_DIR, f"server_{path_id}.log")
log_file = open(log_file_path, 'w')
log_file.write(f"{'='*60}\n")
log_file.write(f"Server started at {subprocess.check_output(['date']).decode().strip()}\n")
log_file.write(f"Port: {port}, Directory: {folder_path}\n")
log_file.write(f"Caching: Disabled\n")
log_file.write(f"{'='*60}\n")
log_file.flush()
stdout_dest = log_file
stderr_dest = log_file
else:
stdout_dest = subprocess.DEVNULL
stderr_dest = subprocess.DEVNULL
# Start server in background
process = subprocess.Popen(
[sys.executable, server_script],
stdout=stdout_dest,
stderr=stderr_dest,
start_new_session=True
)
# Save PID and port to JSON
pids = load_pids()
pids[path_id] = {
'pid': process.pid,
'port': port,
'path': folder_path
}
save_pids(pids)
print(f"Server started for {folder_path} on port {port}")
return True
def stop_server_for_folder(folder_path):
"""Stop HTTP server for a specific folder"""
server_info = get_server_info(folder_path)
if not server_info:
print(f"Server is not running for {folder_path}")
return False
try:
pid = server_info['pid']
os.kill(pid, signal.SIGTERM)
# Remove from PID file
pids = load_pids()
path_id = get_path_id(folder_path)
if path_id in pids:
del pids[path_id]
save_pids(pids)
print(f"Server stopped for {folder_path}")
return True
except OSError as e:
print(f"Error stopping server: {e}")
# Clean up PID file anyway
pids = load_pids()
path_id = get_path_id(folder_path)
if path_id in pids:
del pids[path_id]
save_pids(pids)
return False
def toggle_server(folder_path, port):
"""Toggle server on/off for a folder"""
if is_server_running_for_path(folder_path):
stop_server_for_folder(folder_path)
else:
start_server_for_folder(folder_path, port)
def view_log(folder_path=None):
"""Open the log file(s) in the default text editor"""
if folder_path:
path_id = get_path_id(folder_path)
log_file_path = os.path.join(LOG_DIR, f"server_{path_id}.log")
if os.path.exists(log_file_path):
subprocess.run(['open', '-t', log_file_path])
else:
print(f"Log file does not exist for {folder_path}")
else:
# Open all logs
if os.path.exists(LOG_DIR):
subprocess.run(['open', LOG_DIR])
else:
print("No logs exist")
def clear_logs():
"""Clear all log files"""
if os.path.exists(LOG_DIR):
for filename in os.listdir(LOG_DIR):
file_path = os.path.join(LOG_DIR, filename)
if os.path.isfile(file_path):
os.remove(file_path)
print("All logs cleared successfully")
else:
print("Log directory does not exist")
def add_server_dir(new_path):
"""Add a new server directory to the config file"""
new_path = os.path.expanduser(new_path)
if not os.path.exists(new_path):
print(f"Error: Directory {new_path} does not exist")
return
# Check if path already exists in config
all_dirs = get_all_server_dirs()
if new_path in all_dirs:
print(f"Directory already in config: {new_path}")
return
# Add to config file
with open(CONFIG_FILE, 'a') as f:
f.write(f"SERVER_DIR={new_path}\n")
print(f"Added directory: {new_path}")
def main():
# Handle commands
if len(sys.argv) > 1:
command = sys.argv[1]
if command == "toggle":
if len(sys.argv) > 3:
folder_path = sys.argv[2]
port = int(sys.argv[3])
toggle_server(folder_path, port)
elif command == "open":
if len(sys.argv) > 2:
port = int(sys.argv[2])
subprocess.run(['open', f'http://localhost:{port}'])
elif command == "view_log":
if len(sys.argv) > 2:
folder_path = sys.argv[2]
view_log(folder_path)
else:
view_log()
elif command == "clear_logs":
clear_logs()
elif command == "add_dir":
if len(sys.argv) > 2:
new_path = sys.argv[2]
add_server_dir(new_path)
return
# Display menu
all_dirs = get_all_server_dirs()
# Count running servers
running_count = sum(1 for path in all_dirs if is_server_running_for_path(path))
# Menu bar icon
if running_count > 0:
print(f"● {running_count}")
else:
print("○")
print("---")
# Show each directory with toggle capability at top level
if len(all_dirs) > 0:
for i, path in enumerate(all_dirs):
port = get_assigned_port(path, all_dirs)
server_info = get_server_info(path)
is_running = server_info is not None
# Check if directory exists
dir_exists = os.path.exists(path)
# Get a nice display name (last component of path)
display_name = os.path.basename(path.rstrip('/')) or path
# Status indicator
if not dir_exists:
status_icon = "⚠️ "
elif is_running:
status_icon = "● "
else:
status_icon = "○ "
# Create toggle menu item (top level) - disable if directory doesn't exist
if dir_exists:
print(f"{status_icon} {display_name} | bash='{sys.executable}' param1='{__file__}' param2='toggle' param3='{path}' param4='{port}' terminal=false refresh=true")
else:
print(f"{status_icon} {display_name}")
# Port number
print(f"--:{port}")
if is_running:
print(f"--Open in Browser… | bash='{sys.executable}' param1='{__file__}' param2='open' param3='{port}' terminal=false")
if ENABLE_LOG:
path_id = get_path_id(path)
log_file_path = os.path.join(LOG_DIR, f"server_{path_id}.log")
if os.path.exists(log_file_path):
log_size = os.path.getsize(log_file_path)
log_size_kb = log_size / 1024
print(f"--View Log ({log_size_kb:.1f} KB) | bash='{sys.executable}' param1='{__file__}' param2='view_log' param3='{path}' terminal=false")
# Visual separator before path
print("-----")
# Full path with existence indicator
if not dir_exists:
print(f"--{path} | size=10 color=red")
else:
print(f"--{path} | size=10")
else:
print("No directories configured")
print("--Add a directory in config file")
# Log options (only if we have servers and logging enabled)
if ENABLE_LOG and os.path.exists(LOG_DIR) and len(all_dirs) > 0:
print("---")
print(f"View All Logs… | bash='{sys.executable}' param1='{__file__}' param2='view_log' terminal=false")
print(f"Clear All Logs | bash='{sys.executable}' param1='{__file__}' param2='clear_logs' terminal=false refresh=true")
print("---")
# print(f"Base Port: {BASE_PORT}")
print(f"Edit Config… | bash='open' param1='{CONFIG_FILE}' terminal=false")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment