better link display and opening

This commit is contained in:
Bendt
2025-12-18 11:57:29 -05:00
parent e08f552386
commit 82fbc31683
3 changed files with 78 additions and 26 deletions

BIN
.coverage

Binary file not shown.

View File

@@ -82,6 +82,13 @@ class ContentDisplayConfig(BaseModel):
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 ThemeConfig(BaseModel):
"""Theme/appearance settings."""
@@ -96,6 +103,7 @@ class MaildirGTDConfig(BaseModel):
default_factory=EnvelopeDisplayConfig
)
content_display: ContentDisplayConfig = Field(default_factory=ContentDisplayConfig)
link_panel: LinkPanelConfig = Field(default_factory=LinkPanelConfig)
keybindings: KeybindingsConfig = Field(default_factory=KeybindingsConfig)
theme: ThemeConfig = Field(default_factory=ThemeConfig)

View File

@@ -13,6 +13,8 @@ from textual.containers import Container, Vertical
from textual.screen import ModalScreen
from textual.widgets import Label, ListView, ListItem, Static
from src.maildir_gtd.config import get_config
@dataclass
class LinkItem:
@@ -77,7 +79,13 @@ class LinkItem:
@staticmethod
def _shorten_url(url: str, domain: str, path: str, max_len: int) -> str:
"""Create a shortened, readable version of the URL."""
"""Create a shortened, readable version of the URL.
Intelligently shortens URLs by:
- Special handling for known sites (GitHub, Google Docs, Jira, GitLab)
- Keeping first and last path segments, eliding middle only if needed
- Adapting to available width
"""
# Special handling for common sites
path = path.strip("/")
@@ -116,23 +124,43 @@ class LinkItem:
icon = "#" if type_ == "issues" else "MR!"
return f"{domain} > {repo} {icon}{num}"
# Generic shortening
# Generic shortening - keep URL readable
if len(url) <= max_len:
return url
# Truncate path intelligently
# Build shortened path, keeping as many segments as fit
path_parts = [p for p in path.split("/") if p]
if len(path_parts) > 2:
if not path_parts:
return domain
# Try to fit the full path first
full_path = "/".join(path_parts)
result = f"{domain} > {full_path}"
if len(result) <= max_len:
return result
# Keep first segment + last two segments if possible
if len(path_parts) >= 3:
short_path = f"{path_parts[0]}/.../{path_parts[-2]}/{path_parts[-1]}"
result = f"{domain} > {short_path}"
if len(result) <= max_len:
return result
# Keep first + last segment
if len(path_parts) >= 2:
short_path = f"{path_parts[0]}/.../{path_parts[-1]}"
elif path_parts:
short_path = "/".join(path_parts)
else:
short_path = ""
result = f"{domain} > {short_path}"
if len(result) <= max_len:
return result
result = f"{domain}"
if short_path:
result += f" > {short_path}"
# Just last segment
result = f"{domain} > .../{path_parts[-1]}"
if len(result) <= max_len:
return result
# Truncate with ellipsis as last resort
result = f"{domain} > {path_parts[-1]}"
if len(result) > max_len:
result = result[: max_len - 3] + "..."
@@ -221,36 +249,48 @@ def _assign_mnemonics(links: List[LinkItem]) -> None:
used_mnemonics: set = set()
# Characters to use for mnemonics (easily typeable)
# Exclude h,j,k,l to avoid conflicts with navigation keys
available_chars = "asdfgqwertyuiopzxcvbnm"
# Exclude keys used by app/panel bindings: h,j,k,l (navigation), q (quit),
# b (page up), e (archive), o (open), s (sort), t (task), w (toggle), x (select)
reserved_keys = set("hjklqbeostwx")
available_chars = "".join(
c for c in "asdfgqwertyuiopzxcvbnm" if c not in reserved_keys
)
for link in links:
mnemonic = None
# Try first letter of domain
if link.domain:
first = link.domain[0].lower()
if first in available_chars and first not in used_mnemonics:
mnemonic = first
used_mnemonics.add(first)
# Try first letter of label
if not mnemonic and link.label:
# Try first letter of label (prioritize link text over domain)
if link.label:
first = link.label[0].lower()
if first in available_chars and first not in used_mnemonics:
mnemonic = first
used_mnemonics.add(first)
# Try first letter of domain as fallback
if not mnemonic and link.domain:
first = link.domain[0].lower()
if first in available_chars and first not in used_mnemonics:
mnemonic = first
used_mnemonics.add(first)
# Try other letters from label
if not mnemonic and link.label:
for char in link.label.lower():
if char in available_chars and char not in used_mnemonics:
mnemonic = char
used_mnemonics.add(char)
break
# Try first two letters combined logic
if not mnemonic:
# Try domain + label initials
# Try label word initials
candidates = []
if link.domain and len(link.domain) > 1:
candidates.append(link.domain[:2].lower())
if link.label:
words = link.label.split()
if len(words) >= 2:
candidates.append((words[0][0] + words[1][0]).lower())
if link.domain and len(link.domain) > 1:
candidates.append(link.domain[:2].lower())
for candidate in candidates:
if len(candidate) == 2 and candidate not in used_mnemonics:
@@ -444,7 +484,11 @@ class LinkPanel(ModalScreen):
try:
webbrowser.open(link.url)
self.app.notify(f"Opened: {link.short_display}", title="Link Opened")
self.dismiss()
# Only dismiss if configured to close on open
config = get_config()
if config.link_panel.close_on_open:
self.dismiss()
except Exception as e:
self.app.notify(f"Failed to open link: {e}", severity="error")