From 82fbc31683a3c909f4682e08b547d18922aa25d2 Mon Sep 17 00:00:00 2001 From: Bendt Date: Thu, 18 Dec 2025 11:57:29 -0500 Subject: [PATCH] better link display and opening --- .coverage | Bin 69632 -> 69632 bytes src/maildir_gtd/config.py | 8 +++ src/maildir_gtd/screens/LinkPanel.py | 96 +++++++++++++++++++-------- 3 files changed, 78 insertions(+), 26 deletions(-) diff --git a/.coverage b/.coverage index fd69aec57ec97954e7c296a9681a0a06bf6d8582..4ffaba4b5bb8abd8397a9ffff4f51aa8c2eacc1c 100644 GIT binary patch delta 173 zcmZozz|ydQWdmCSW6xyv1|{bdULRgdp4U7}cxLiUin20t zHgYNcs@r+rj)B2|fq|I;4p_h}76yj~28PM!dt{gz7$?8!sT7qGWMSkq6RkQeH(C9N S&F}x`Q#QT1x4E*ny$Jx-t}#>q delta 165 zcmZozz|ydQWdmCSpi~Az? z2JRKybGfs)J-J?R-RC;NwV$hpD~`*JOOi{F^Ec;f&b^#dHVX=9aBlADlwel26J=rK ztW*3|_ij5s0|O7k0R{+g08>Dcg=6yB9vMc7$*+4VMI;4*s!c?zPRmVJKVtLy|M`@g LcQ;q|wl@I)HGVNU diff --git a/src/maildir_gtd/config.py b/src/maildir_gtd/config.py index cc47a53..8b67190 100644 --- a/src/maildir_gtd/config.py +++ b/src/maildir_gtd/config.py @@ -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) diff --git a/src/maildir_gtd/screens/LinkPanel.py b/src/maildir_gtd/screens/LinkPanel.py index 323dfe6..3cff5f7 100644 --- a/src/maildir_gtd/screens/LinkPanel.py +++ b/src/maildir_gtd/screens/LinkPanel.py @@ -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")