better link display and opening
This commit is contained in:
@@ -82,6 +82,13 @@ class ContentDisplayConfig(BaseModel):
|
|||||||
default_view_mode: Literal["markdown", "html"] = "markdown"
|
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):
|
class ThemeConfig(BaseModel):
|
||||||
"""Theme/appearance settings."""
|
"""Theme/appearance settings."""
|
||||||
|
|
||||||
@@ -96,6 +103,7 @@ class MaildirGTDConfig(BaseModel):
|
|||||||
default_factory=EnvelopeDisplayConfig
|
default_factory=EnvelopeDisplayConfig
|
||||||
)
|
)
|
||||||
content_display: ContentDisplayConfig = Field(default_factory=ContentDisplayConfig)
|
content_display: ContentDisplayConfig = Field(default_factory=ContentDisplayConfig)
|
||||||
|
link_panel: LinkPanelConfig = Field(default_factory=LinkPanelConfig)
|
||||||
keybindings: KeybindingsConfig = Field(default_factory=KeybindingsConfig)
|
keybindings: KeybindingsConfig = Field(default_factory=KeybindingsConfig)
|
||||||
theme: ThemeConfig = Field(default_factory=ThemeConfig)
|
theme: ThemeConfig = Field(default_factory=ThemeConfig)
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ from textual.containers import Container, Vertical
|
|||||||
from textual.screen import ModalScreen
|
from textual.screen import ModalScreen
|
||||||
from textual.widgets import Label, ListView, ListItem, Static
|
from textual.widgets import Label, ListView, ListItem, Static
|
||||||
|
|
||||||
|
from src.maildir_gtd.config import get_config
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LinkItem:
|
class LinkItem:
|
||||||
@@ -77,7 +79,13 @@ class LinkItem:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _shorten_url(url: str, domain: str, path: str, max_len: int) -> str:
|
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
|
# Special handling for common sites
|
||||||
path = path.strip("/")
|
path = path.strip("/")
|
||||||
|
|
||||||
@@ -116,23 +124,43 @@ class LinkItem:
|
|||||||
icon = "#" if type_ == "issues" else "MR!"
|
icon = "#" if type_ == "issues" else "MR!"
|
||||||
return f"{domain} > {repo} {icon}{num}"
|
return f"{domain} > {repo} {icon}{num}"
|
||||||
|
|
||||||
# Generic shortening
|
# Generic shortening - keep URL readable
|
||||||
if len(url) <= max_len:
|
if len(url) <= max_len:
|
||||||
return url
|
return url
|
||||||
|
|
||||||
# Truncate path intelligently
|
# Build shortened path, keeping as many segments as fit
|
||||||
path_parts = [p for p in path.split("/") if p]
|
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]}"
|
short_path = f"{path_parts[0]}/.../{path_parts[-1]}"
|
||||||
elif path_parts:
|
result = f"{domain} > {short_path}"
|
||||||
short_path = "/".join(path_parts)
|
if len(result) <= max_len:
|
||||||
else:
|
return result
|
||||||
short_path = ""
|
|
||||||
|
|
||||||
result = f"{domain}"
|
# Just last segment
|
||||||
if short_path:
|
result = f"{domain} > .../{path_parts[-1]}"
|
||||||
result += f" > {short_path}"
|
if len(result) <= max_len:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Truncate with ellipsis as last resort
|
||||||
|
result = f"{domain} > {path_parts[-1]}"
|
||||||
if len(result) > max_len:
|
if len(result) > max_len:
|
||||||
result = result[: max_len - 3] + "..."
|
result = result[: max_len - 3] + "..."
|
||||||
|
|
||||||
@@ -221,36 +249,48 @@ def _assign_mnemonics(links: List[LinkItem]) -> None:
|
|||||||
used_mnemonics: set = set()
|
used_mnemonics: set = set()
|
||||||
|
|
||||||
# Characters to use for mnemonics (easily typeable)
|
# Characters to use for mnemonics (easily typeable)
|
||||||
# Exclude h,j,k,l to avoid conflicts with navigation keys
|
# Exclude keys used by app/panel bindings: h,j,k,l (navigation), q (quit),
|
||||||
available_chars = "asdfgqwertyuiopzxcvbnm"
|
# 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:
|
for link in links:
|
||||||
mnemonic = None
|
mnemonic = None
|
||||||
|
|
||||||
# Try first letter of domain
|
# Try first letter of label (prioritize link text over domain)
|
||||||
if link.domain:
|
if link.label:
|
||||||
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:
|
|
||||||
first = link.label[0].lower()
|
first = link.label[0].lower()
|
||||||
if first in available_chars and first not in used_mnemonics:
|
if first in available_chars and first not in used_mnemonics:
|
||||||
mnemonic = first
|
mnemonic = first
|
||||||
used_mnemonics.add(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
|
# Try first two letters combined logic
|
||||||
if not mnemonic:
|
if not mnemonic:
|
||||||
# Try domain + label initials
|
# Try label word initials
|
||||||
candidates = []
|
candidates = []
|
||||||
if link.domain and len(link.domain) > 1:
|
|
||||||
candidates.append(link.domain[:2].lower())
|
|
||||||
if link.label:
|
if link.label:
|
||||||
words = link.label.split()
|
words = link.label.split()
|
||||||
if len(words) >= 2:
|
if len(words) >= 2:
|
||||||
candidates.append((words[0][0] + words[1][0]).lower())
|
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:
|
for candidate in candidates:
|
||||||
if len(candidate) == 2 and candidate not in used_mnemonics:
|
if len(candidate) == 2 and candidate not in used_mnemonics:
|
||||||
@@ -444,7 +484,11 @@ class LinkPanel(ModalScreen):
|
|||||||
try:
|
try:
|
||||||
webbrowser.open(link.url)
|
webbrowser.open(link.url)
|
||||||
self.app.notify(f"Opened: {link.short_display}", title="Link Opened")
|
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:
|
except Exception as e:
|
||||||
self.app.notify(f"Failed to open link: {e}", severity="error")
|
self.app.notify(f"Failed to open link: {e}", severity="error")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user