import base64 import re import sys import urwid from collections import OrderedDict from functools import reduce from html.parser import HTMLParser from typing import List HASHTAG_PATTERN = re.compile(r'(?>> highlight_keys("[P]rint [V]iew", "blue") >>> [('blue', 'P'), 'rint ', ('blue', 'V'), 'iew'] """ def _gen(): highlighted = False for part in re.split("\\[|\\]", text): if part: if highlighted: yield (high_attr, part) if high_attr else part else: yield (low_attr, part) if low_attr else part highlighted = not highlighted return list(_gen()) def highlight_hashtags(line): hline = [] for p in re.split(HASHTAG_PATTERN, line): if p.startswith("#"): hline.append(("hashtag", p)) else: hline.append(p) return hline class LinkParser(HTMLParser): def reset(self): super().reset() self.links = [] def handle_starttag(self, tag, attrs): if tag == "a": href, title = None, None for name, value in attrs: if name == "href": href = value if name == "title": title = value if href: self.links.append((href, title)) def parse_content_links(content): """Parse tags from status's `content` and return them as a list of (href, title), where `title` may be None. """ parser = LinkParser() parser.feed(content) return parser.links[:] def copy_to_clipboard(screen: urwid.raw_display.Screen, text: str): """ copy text to clipboard using OSC 52 This escape sequence is documented here https://iterm2.com/documentation-escape-codes.html It has wide support - XTerm, Windows Terminal, Kitty, iTerm2, others. Some terminals may require a setting to be enabled in order to use OSC 52 clipboard functions. """ text_bytes = text.encode("utf-8") b64_bytes = base64.b64encode(text_bytes) b64_text = b64_bytes.decode("utf-8") screen.write(f"\033]52;c;{b64_text}\a") screen.flush() def get_max_toot_chars(instance, default=500): # Mastodon # https://docs.joinmastodon.org/entities/Instance/#max_characters max_toot_chars = deep_get(instance, ["configuration", "statuses", "max_characters"]) if isinstance(max_toot_chars, int): return max_toot_chars # Pleroma max_toot_chars = instance.get("max_toot_chars") if isinstance(max_toot_chars, int): return max_toot_chars return default def deep_get(adict: dict, path: List[str], default=None): return reduce( lambda d, key: d.get(key, default) if isinstance(d, dict) else default, path, adict ) class LRUCache(OrderedDict): """Dict with a limited size, ejecting LRUs as needed. Default max size = 10Mb""" def __init__(self, *args, cache_max_bytes: int = 1024 * 1024 * 10, **kwargs): assert cache_max_bytes > 0 self.total_value_size = 0 self.cache_max_bytes = cache_max_bytes super().__init__(*args, **kwargs) def __setitem__(self, key: str, value): if key in self: self.total_value_size -= sys.getsizeof(super().__getitem__(key).tobytes()) self.total_value_size += sys.getsizeof(value.tobytes()) super().__setitem__(key, value) super().move_to_end(key) while self.total_value_size > self.cache_max_bytes: old_key, value = next(iter(self.items())) sz = sys.getsizeof(value.tobytes()) super().__delitem__(old_key) self.total_value_size -= sz def __getitem__(self, key: str): val = super().__getitem__(key) super().move_to_end(key) return val