better link display and opening
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user