Files
luk/src/mail/config.py
2025-12-18 14:00:54 -05:00

180 lines
5.4 KiB
Python

"""Configuration system for Mail email reader using Pydantic."""
import logging
import os
from pathlib import Path
from typing import Literal, Optional
import toml
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
class TaskBackendConfig(BaseModel):
"""Configuration for task management backend."""
backend: Literal["taskwarrior", "dstask"] = "taskwarrior"
taskwarrior_path: str = "task"
dstask_path: str = Field(
default_factory=lambda: str(Path.home() / ".local" / "bin" / "dstask")
)
class EnvelopeDisplayConfig(BaseModel):
"""Configuration for envelope list item rendering."""
# Sender display
max_sender_length: int = 25
# Date/time display
date_format: str = "%m/%d"
time_format: str = "%H:%M"
show_date: bool = True
show_time: bool = True
# Grouping
group_by: Literal["relative", "absolute"] = "relative"
# relative: "Today", "Yesterday", "This Week", etc.
# absolute: "December 2025", "November 2025", etc.
# Layout
lines: Literal[2, 3] = 2
# 2: sender/date on line 1, subject on line 2
# 3: sender/date on line 1, subject on line 2, preview on line 3
show_checkbox: bool = True
show_preview: bool = False # Only used when lines=3
# NerdFont icons for status
icon_unread: str = "\uf0e0" # nf-fa-envelope (filled)
icon_read: str = "\uf2b6" # nf-fa-envelope_open (open)
icon_flagged: str = "\uf024" # nf-fa-flag
icon_attachment: str = "\uf0c6" # nf-fa-paperclip
class KeybindingsConfig(BaseModel):
"""Keybinding customization."""
next_message: str = "j"
prev_message: str = "k"
delete: str = "#"
archive: str = "e"
open_by_id: str = "o"
quit: str = "q"
toggle_header: str = "h"
create_task: str = "t"
reload: str = "%"
toggle_sort: str = "s"
toggle_selection: str = "space"
clear_selection: str = "escape"
scroll_page_down: str = "pagedown"
scroll_page_down: str = "space"
scroll_page_up: str = "b"
toggle_main_content: str = "w"
open_links: str = "l"
toggle_view_mode: str = "m"
class ContentDisplayConfig(BaseModel):
"""Configuration for message content display."""
# View mode: "markdown" for pretty rendering, "html" for raw/plain display
default_view_mode: Literal["markdown", "html"] = "markdown"
class LinkPanelConfig(BaseModel):
"""Configuration for the link panel."""
# Whether to close the panel after opening a link
close_on_open: bool = False
class MailOperationsConfig(BaseModel):
"""Configuration for mail operations."""
# Folder to move messages to when archiving
archive_folder: str = "Archive"
class ThemeConfig(BaseModel):
"""Theme/appearance settings."""
theme_name: str = "monokai"
class MailAppConfig(BaseModel):
"""Main configuration for Mail email reader."""
task: TaskBackendConfig = Field(default_factory=TaskBackendConfig)
envelope_display: EnvelopeDisplayConfig = Field(
default_factory=EnvelopeDisplayConfig
)
content_display: ContentDisplayConfig = Field(default_factory=ContentDisplayConfig)
link_panel: LinkPanelConfig = Field(default_factory=LinkPanelConfig)
mail: MailOperationsConfig = Field(default_factory=MailOperationsConfig)
keybindings: KeybindingsConfig = Field(default_factory=KeybindingsConfig)
theme: ThemeConfig = Field(default_factory=ThemeConfig)
@classmethod
def get_config_path(cls) -> Path:
"""Get the path to the config file."""
# Check environment variable first
env_path = os.getenv("LUK_MAIL_CONFIG")
if env_path:
return Path(env_path)
# Default to ~/.config/luk/mail.toml
return Path.home() / ".config" / "luk" / "mail.toml"
@classmethod
def load(cls, config_path: Optional[Path] = None) -> "MailAppConfig":
"""Load config from TOML file with defaults for missing values."""
if config_path is None:
config_path = cls.get_config_path()
if config_path.exists():
try:
with open(config_path, "r") as f:
data = toml.load(f)
logger.info(f"Loaded config from {config_path}")
return cls.model_validate(data)
except Exception as e:
logger.warning(f"Error loading config from {config_path}: {e}")
logger.warning("Using default configuration")
return cls()
else:
logger.info(f"No config file at {config_path}, using defaults")
return cls()
def save(self, config_path: Optional[Path] = None) -> None:
"""Save current config to TOML file."""
if config_path is None:
config_path = self.get_config_path()
# Ensure parent directory exists
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, "w") as f:
toml.dump(self.model_dump(), f)
logger.info(f"Saved config to {config_path}")
# Global config instance (lazy-loaded)
_config: Optional[MailAppConfig] = None
def get_config() -> MailAppConfig:
"""Get the global config instance, loading it if necessary."""
global _config
if _config is None:
_config = MailAppConfig.load()
return _config
def reload_config() -> MailAppConfig:
"""Force reload of the config from disk."""
global _config
_config = MailAppConfig.load()
return _config