this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

sortal

+41 -158
+8
stack/sortal/CLAUDE.md
··· 24 24 - names: List of full names (primary name first) 25 25 - email: Email address 26 26 - icon: Avatar/icon URL 27 + - thumbnail: Path to a local thumbnail image file (stored in `thumbnails/` subdirectory) 27 28 - github: GitHub username 28 29 - twitter: Twitter/X username 29 30 - bluesky: Bluesky handle ··· 31 32 - orcid: ORCID identifier 32 33 - url: Personal/professional website 33 34 - atom_feeds: List of Atom/RSS feed URLs 35 + 36 + ## Thumbnails 37 + 38 + Contact thumbnails are stored locally in the XDG data directory: 39 + - Location: `$HOME/.local/share/{app_name}/thumbnails/` 40 + - Files are named: `{handle}.{jpg|png|gif}` 41 + - The `thumbnail` field contains a relative path like `thumbnails/{handle}.jpg`
+1
stack/sortal/README.md
··· 17 17 - `names`: List of full names with primary name first (required) 18 18 - `email`: Email address 19 19 - `icon`: Avatar/icon URL 20 + - `thumbnail`: Path to a local thumbnail image file 20 21 - `github`: GitHub username 21 22 - `twitter`: Twitter/X username 22 23 - `bluesky`: Bluesky handle
+16 -4
stack/sortal/lib/sortal.ml
··· 58 58 names : string list; 59 59 email : string option; 60 60 icon : string option; 61 + thumbnail : string option; 61 62 github : string option; 62 63 twitter : string option; 63 64 bluesky : string option; ··· 67 68 feeds : Feed.t list option; 68 69 } 69 70 70 - let make ~handle ~names ?email ?icon ?github ?twitter ?bluesky ?mastodon 71 + let make ~handle ~names ?email ?icon ?thumbnail ?github ?twitter ?bluesky ?mastodon 71 72 ?orcid ?url ?feeds () = 72 - { handle; names; email; icon; github; twitter; bluesky; mastodon; 73 + { handle; names; email; icon; thumbnail; github; twitter; bluesky; mastodon; 73 74 orcid; url; feeds } 74 75 75 76 let handle t = t.handle ··· 78 79 let primary_name = name 79 80 let email t = t.email 80 81 let icon t = t.icon 82 + let thumbnail t = t.thumbnail 81 83 let github t = t.github 82 84 let twitter t = t.twitter 83 85 let bluesky t = t.bluesky ··· 115 117 let open Jsont in 116 118 let open Jsont.Object in 117 119 let mem_opt f v ~enc = mem f v ~dec_absent:None ~enc_omit:Option.is_none ~enc in 118 - let make handle names email icon github twitter bluesky mastodon orcid url feeds = 119 - { handle; names; email; icon; github; twitter; bluesky; mastodon; 120 + let make handle names email icon thumbnail github twitter bluesky mastodon orcid url feeds = 121 + { handle; names; email; icon; thumbnail; github; twitter; bluesky; mastodon; 120 122 orcid; url; feeds } 121 123 in 122 124 map ~kind:"Contact" make ··· 124 126 |> mem "names" (list string) ~dec_absent:[] ~enc:names 125 127 |> mem_opt "email" (some string) ~enc:email 126 128 |> mem_opt "icon" (some string) ~enc:icon 129 + |> mem_opt "thumbnail" (some string) ~enc:thumbnail 127 130 |> mem_opt "github" (some string) ~enc:github 128 131 |> mem_opt "twitter" (some string) ~enc:twitter 129 132 |> mem_opt "bluesky" (some string) ~enc:bluesky ··· 170 173 | None -> ()); 171 174 (match t.icon with 172 175 | Some i -> pf ppf "%a: %a@," (styled `Bold string) "Icon" string i 176 + | None -> ()); 177 + (match t.thumbnail with 178 + | Some th -> pf ppf "%a: %a@," (styled `Bold string) "Thumbnail" string th 173 179 | None -> ()); 174 180 (match t.feeds with 175 181 | Some feeds when feeds <> [] -> ··· 229 235 ) entries 230 236 with 231 237 | _ -> [] 238 + 239 + let thumbnail_path t contact = 240 + match Contact.thumbnail contact with 241 + | None -> None 242 + | Some relative_path -> 243 + Some Eio.Path.(t.data_dir / relative_path) 232 244 233 245 let handle_of_name name = 234 246 let name = String.lowercase_ascii name in
+16 -1
stack/sortal/lib/sortal.mli
··· 86 86 social media handles, professional identifiers, and other contact information. *) 87 87 type t 88 88 89 - (** [make ~handle ~names ?email ?icon ?github ?twitter ?bluesky ?mastodon 89 + (** [make ~handle ~names ?email ?icon ?thumbnail ?github ?twitter ?bluesky ?mastodon 90 90 ?orcid ?url ?feeds ()] creates a new contact. 91 91 92 92 @param handle A unique identifier/username for this contact (required) 93 93 @param names A list of names for this contact, with the first being primary (required) 94 94 @param email Email address 95 95 @param icon URL to an avatar/icon image 96 + @param thumbnail Path to a local thumbnail image file 96 97 @param github GitHub username (without the [\@] prefix) 97 98 @param twitter Twitter/X username (without the [\@] prefix) 98 99 @param bluesky Bluesky handle ··· 106 107 names:string list -> 107 108 ?email:string -> 108 109 ?icon:string -> 110 + ?thumbnail:string -> 109 111 ?github:string -> 110 112 ?twitter:string -> 111 113 ?bluesky:string -> ··· 136 138 137 139 (** [icon t] returns the icon/avatar URL if available. *) 138 140 val icon : t -> string option 141 + 142 + (** [thumbnail t] returns the path to the local thumbnail image if available. 143 + This is a relative path from the Sortal data directory. *) 144 + val thumbnail : t -> string option 139 145 140 146 (** [github t] returns the GitHub username if available. *) 141 147 val github : t -> string option ··· 256 262 @return A list of all successfully loaded contacts 257 263 *) 258 264 val list : t -> Contact.t list 265 + 266 + (** [thumbnail_path t contact] returns the absolute filesystem path to the contact's thumbnail. 267 + 268 + Returns [None] if the contact has no thumbnail set, or [Some path] with 269 + the full path to the thumbnail file in Sortal's data directory. 270 + 271 + @param t The Sortal store 272 + @param contact The contact whose thumbnail path to retrieve *) 273 + val thumbnail_path : t -> Contact.t -> Eio.Fs.dir_ty Eio.Path.t option 259 274 260 275 (** {2 Searching} *) 261 276
-153
stack/sortal/scripts/import_yaml_contacts.py
··· 1 - #!/usr/bin/env python3 2 - """ 3 - Import YAML contacts from arod-dream to Sortal JSON format. 4 - 5 - This script reads contacts from ~/src/git/avsm/arod-dream/data/contacts/ 6 - and converts them to JSON files in the XDG data directory for sortal. 7 - """ 8 - 9 - import os 10 - import json 11 - import yaml 12 - from pathlib import Path 13 - 14 - def get_xdg_data_home(): 15 - """Get the XDG data home directory.""" 16 - xdg_data_home = os.environ.get('XDG_DATA_HOME') 17 - if xdg_data_home: 18 - return Path(xdg_data_home) 19 - return Path.home() / '.local' / 'share' 20 - 21 - def parse_contact_md(file_path): 22 - """Parse a markdown file with YAML front matter.""" 23 - with open(file_path, 'r') as f: 24 - content = f.read() 25 - 26 - # Extract YAML front matter between --- 27 - if content.startswith('---\n'): 28 - parts = content.split('---\n', 2) 29 - if len(parts) >= 2: 30 - yaml_content = parts[1] 31 - return yaml.safe_load(yaml_content) 32 - return None 33 - 34 - def detect_feed_type(url): 35 - """Detect feed type based on URL patterns.""" 36 - url_lower = url.lower() 37 - if 'json' in url_lower or url_lower.endswith('.json'): 38 - return 'json' 39 - elif 'rss' in url_lower or url_lower.endswith('.rss') or url_lower.endswith('.xml'): 40 - return 'rss' 41 - else: 42 - # Default to atom for most feeds 43 - return 'atom' 44 - 45 - def convert_to_sortal_format(yaml_data, handle): 46 - """Convert YAML contact data to Sortal JSON format.""" 47 - sortal_contact = { 48 - "handle": handle, 49 - "names": yaml_data.get("names", []) 50 - } 51 - 52 - # Optional fields 53 - if "email" in yaml_data: 54 - sortal_contact["email"] = yaml_data["email"] 55 - if "icon" in yaml_data: 56 - sortal_contact["icon"] = yaml_data["icon"] 57 - if "github" in yaml_data: 58 - sortal_contact["github"] = yaml_data["github"] 59 - if "twitter" in yaml_data: 60 - sortal_contact["twitter"] = yaml_data["twitter"] 61 - if "bluesky" in yaml_data: 62 - sortal_contact["bluesky"] = yaml_data["bluesky"] 63 - if "mastodon" in yaml_data: 64 - sortal_contact["mastodon"] = yaml_data["mastodon"] 65 - if "orcid" in yaml_data: 66 - sortal_contact["orcid"] = yaml_data["orcid"] 67 - if "url" in yaml_data: 68 - sortal_contact["url"] = yaml_data["url"] 69 - 70 - # Convert atom feeds to new feed structure 71 - if "atom" in yaml_data: 72 - atom_feeds = yaml_data["atom"] 73 - if atom_feeds: 74 - feeds = [] 75 - for feed_url in atom_feeds: 76 - feed_type = detect_feed_type(feed_url) 77 - feed_obj = { 78 - "type": feed_type, 79 - "url": feed_url 80 - } 81 - feeds.append(feed_obj) 82 - sortal_contact["feeds"] = feeds 83 - 84 - return sortal_contact 85 - 86 - def main(): 87 - # Source directory 88 - source_dir = Path.home() / 'src' / 'git' / 'avsm' / 'arod-dream' / 'data' / 'contacts' 89 - 90 - # Destination directory (XDG data home for sortal) 91 - xdg_data = get_xdg_data_home() 92 - dest_dir = xdg_data / 'sortal' 93 - dest_dir.mkdir(parents=True, exist_ok=True) 94 - 95 - print(f"Importing contacts from: {source_dir}") 96 - print(f"Output directory: {dest_dir}") 97 - print() 98 - 99 - if not source_dir.exists(): 100 - print(f"Error: Source directory does not exist: {source_dir}") 101 - return 1 102 - 103 - # Delete existing contacts to avoid old schema 104 - print("Clearing existing contacts...") 105 - for existing_file in dest_dir.glob('*.json'): 106 - existing_file.unlink() 107 - 108 - imported_count = 0 109 - error_count = 0 110 - total_feeds = 0 111 - 112 - # Process each .md file 113 - for md_file in sorted(source_dir.glob('*.md')): 114 - handle = md_file.stem 115 - 116 - try: 117 - yaml_data = parse_contact_md(md_file) 118 - if yaml_data is None: 119 - print(f"⚠ Skipping {handle}: No YAML front matter found") 120 - error_count += 1 121 - continue 122 - 123 - # Convert to Sortal format 124 - sortal_contact = convert_to_sortal_format(yaml_data, handle) 125 - 126 - # Write JSON file 127 - output_file = dest_dir / f"{handle}.json" 128 - with open(output_file, 'w') as f: 129 - json.dump(sortal_contact, f, indent=2, ensure_ascii=False) 130 - 131 - name = sortal_contact.get('names', [handle])[0] if sortal_contact.get('names') else handle 132 - feed_count = len(sortal_contact.get('feeds', [])) 133 - total_feeds += feed_count 134 - 135 - feed_info = f" ({feed_count} feed{'s' if feed_count != 1 else ''})" if feed_count > 0 else "" 136 - print(f"✓ Imported: {handle} ({name}){feed_info}") 137 - imported_count += 1 138 - 139 - except Exception as e: 140 - print(f"✗ Error importing {handle}: {e}") 141 - error_count += 1 142 - 143 - print() 144 - print(f"Import complete!") 145 - print(f" Successfully imported: {imported_count}") 146 - print(f" Total feeds: {total_feeds}") 147 - print(f" Errors: {error_count}") 148 - print(f" Output directory: {dest_dir}") 149 - 150 - return 0 if error_count == 0 else 1 151 - 152 - if __name__ == '__main__': 153 - exit(main())