Initial commit: Bifrost accessible fediverse client

- Full ActivityPub support for Pleroma, GoToSocial, and Mastodon
- Screen reader optimized interface with PySide6
- Timeline switching with tabs and keyboard shortcuts (Ctrl+1-4)
- Threaded conversation navigation with expand/collapse
- Cross-platform desktop notifications via plyer
- Customizable sound pack system with audio feedback
- Complete keyboard navigation and accessibility features
- XDG Base Directory compliant configuration
- Multiple account support with OAuth authentication

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Storm Dragon
2025-07-20 03:39:47 -04:00
commit 460dfc52a5
31 changed files with 5320 additions and 0 deletions

145
src/config/settings.py Normal file
View File

@ -0,0 +1,145 @@
"""
Settings management with XDG Base Directory specification compliance
"""
import os
import configparser
from pathlib import Path
from typing import Any, Optional
class SettingsManager:
"""Manages application settings with XDG compliance"""
def __init__(self):
self.config_dir = self._get_config_dir()
self.data_dir = self._get_data_dir()
self.cache_dir = self._get_cache_dir()
# Ensure directories exist
self.config_dir.mkdir(parents=True, exist_ok=True)
self.data_dir.mkdir(parents=True, exist_ok=True)
self.cache_dir.mkdir(parents=True, exist_ok=True)
self.config_file = self.config_dir / "bifrost.conf"
self.config = configparser.ConfigParser()
self.load_settings()
def _get_config_dir(self) -> Path:
"""Get XDG config directory"""
xdg_config = os.getenv('XDG_CONFIG_HOME')
if xdg_config:
return Path(xdg_config) / "bifrost"
return Path.home() / ".config" / "bifrost"
def _get_data_dir(self) -> Path:
"""Get XDG data directory"""
xdg_data = os.getenv('XDG_DATA_HOME')
if xdg_data:
return Path(xdg_data) / "bifrost"
return Path.home() / ".local" / "share" / "bifrost"
def _get_cache_dir(self) -> Path:
"""Get XDG cache directory"""
xdg_cache = os.getenv('XDG_CACHE_HOME')
if xdg_cache:
return Path(xdg_cache) / "bifrost"
return Path.home() / ".cache" / "bifrost"
def load_settings(self):
"""Load settings from config file"""
if self.config_file.exists():
self.config.read(self.config_file)
else:
self.create_default_config()
def create_default_config(self):
"""Create default configuration"""
# General settings
self.config.add_section('general')
self.config.set('general', 'instance_url', '')
self.config.set('general', 'username', '')
self.config.set('general', 'current_sound_pack', 'default')
self.config.set('general', 'timeline_refresh_interval', '60')
self.config.set('general', 'auto_refresh_enabled', 'true')
# Sound settings
self.config.add_section('sounds')
self.config.set('sounds', 'enabled', 'true')
self.config.set('sounds', 'private_message_enabled', 'true')
self.config.set('sounds', 'private_message_volume', '0.8')
self.config.set('sounds', 'mention_enabled', 'true')
self.config.set('sounds', 'mention_volume', '1.0')
self.config.set('sounds', 'boost_enabled', 'true')
self.config.set('sounds', 'boost_volume', '0.7')
self.config.set('sounds', 'reply_enabled', 'true')
self.config.set('sounds', 'reply_volume', '0.8')
self.config.set('sounds', 'post_sent_enabled', 'true')
self.config.set('sounds', 'post_sent_volume', '0.9')
self.config.set('sounds', 'timeline_update_enabled', 'true')
self.config.set('sounds', 'timeline_update_volume', '0.5')
self.config.set('sounds', 'notification_enabled', 'true')
self.config.set('sounds', 'notification_volume', '0.8')
# Accessibility settings
self.config.add_section('accessibility')
self.config.set('accessibility', 'announce_thread_state', 'true')
self.config.set('accessibility', 'auto_expand_mentions', 'false')
self.config.set('accessibility', 'keyboard_navigation_wrap', 'true')
self.config.set('accessibility', 'focus_follows_mouse', 'false')
# Interface settings
self.config.add_section('interface')
self.config.set('interface', 'default_timeline', 'home')
self.config.set('interface', 'show_timestamps', 'true')
self.config.set('interface', 'compact_mode', 'false')
self.save_settings()
def save_settings(self):
"""Save settings to config file"""
with open(self.config_file, 'w') as f:
self.config.write(f)
def get(self, section: str, key: str, fallback: Any = None) -> Optional[str]:
"""Get a setting value"""
try:
return self.config.get(section, key)
except (configparser.NoSectionError, configparser.NoOptionError):
return fallback
def get_bool(self, section: str, key: str, fallback: bool = False) -> bool:
"""Get a boolean setting value"""
try:
return self.config.getboolean(section, key)
except (configparser.NoSectionError, configparser.NoOptionError, ValueError):
return fallback
def get_int(self, section: str, key: str, fallback: int = 0) -> int:
"""Get an integer setting value"""
try:
return self.config.getint(section, key)
except (configparser.NoSectionError, configparser.NoOptionError, ValueError):
return fallback
def get_float(self, section: str, key: str, fallback: float = 0.0) -> float:
"""Get a float setting value"""
try:
return self.config.getfloat(section, key)
except (configparser.NoSectionError, configparser.NoOptionError, ValueError):
return fallback
def set(self, section: str, key: str, value: Any):
"""Set a setting value"""
if not self.config.has_section(section):
self.config.add_section(section)
self.config.set(section, key, str(value))
def get_sounds_dir(self) -> Path:
"""Get the sounds directory path"""
return self.data_dir / "sounds"
def get_current_sound_pack_dir(self) -> Path:
"""Get the current sound pack directory"""
pack_name = self.get('general', 'current_sound_pack', 'default')
return self.get_sounds_dir() / pack_name