diff --git a/inbox/captures.json b/inbox/captures.json new file mode 100644 index 0000000..50cfb68 --- /dev/null +++ b/inbox/captures.json @@ -0,0 +1,26 @@ +[ + { + "id": 1, + "content": "Build a voice memo transcription tool", + "type": "idea", + "tags": [], + "captured": "2026-01-30T23:28:05.330819", + "processed": false + }, + { + "id": 2, + "content": "Set up automatic backups", + "type": "task", + "tags": [], + "captured": "2026-01-30T23:28:05.356440", + "processed": false + }, + { + "id": 3, + "content": "This is a random thought", + "type": "note", + "tags": [], + "captured": "2026-01-30T23:28:05.381974", + "processed": false + } +] \ No newline at end of file diff --git a/tools/capture.py b/tools/capture.py new file mode 100755 index 0000000..1f6ffc4 --- /dev/null +++ b/tools/capture.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +""" +capture - Quick thought capture + +Quickly capture thoughts, tasks, ideas, and links. +They go to inbox for later processing. +""" + +import json +import sys +from datetime import datetime +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +CAPTURE_FILE = WORKSPACE / "inbox" / "captures.json" + +TYPES = { + 'thought': 'šŸ’­', + 'task': 'āœ…', + 'idea': 'šŸ’”', + 'link': 'šŸ”—', + 'note': 'šŸ“', + 'quote': 'šŸ’¬', + 'question': 'ā“', +} + +def load_captures() -> list: + """Load captured items.""" + CAPTURE_FILE.parent.mkdir(parents=True, exist_ok=True) + if CAPTURE_FILE.exists(): + with open(CAPTURE_FILE) as f: + return json.load(f) + return [] + +def save_captures(captures: list): + """Save captures.""" + with open(CAPTURE_FILE, 'w') as f: + json.dump(captures, f, indent=2) + +def capture(content: str, capture_type: str = 'note', tags: list = None): + """Capture something quickly.""" + captures = load_captures() + + item = { + 'id': len(captures) + 1, + 'content': content, + 'type': capture_type if capture_type in TYPES else 'note', + 'tags': tags or [], + 'captured': datetime.now().isoformat(), + 'processed': False, + } + + captures.append(item) + save_captures(captures) + + emoji = TYPES.get(capture_type, 'šŸ“') + print(f"{emoji} Captured #{item['id']}: {content[:50]}...") + +def list_captures(show_all: bool = False, type_filter: str = None): + """List captured items.""" + captures = load_captures() + + if not show_all: + captures = [c for c in captures if not c.get('processed')] + + if type_filter: + captures = [c for c in captures if c['type'] == type_filter] + + if not captures: + print("šŸ“­ Inbox empty") + return + + print(f"\nšŸ“„ Inbox ({len(captures)} items)") + print("=" * 50) + + for item in captures: + emoji = TYPES.get(item['type'], 'šŸ“') + status = "āœ“" if item.get('processed') else " " + content = item['content'][:60] + if len(item['content']) > 60: + content += "..." + + print(f"[{status}] #{item['id']:3} {emoji} {content}") + + if item.get('tags'): + tags = ' '.join(f"#{t}" for t in item['tags']) + print(f" {tags}") + + print() + +def process_item(item_id: int): + """Mark an item as processed.""" + captures = load_captures() + + for item in captures: + if item['id'] == item_id: + item['processed'] = True + save_captures(captures) + print(f"āœ“ Processed #{item_id}") + return + + print(f"Item not found: #{item_id}") + +def clear_processed(): + """Remove processed items.""" + captures = load_captures() + unprocessed = [c for c in captures if not c.get('processed')] + removed = len(captures) - len(unprocessed) + + save_captures(unprocessed) + print(f"āœ“ Removed {removed} processed items") + +def export_markdown(): + """Export unprocessed items as markdown.""" + captures = load_captures() + unprocessed = [c for c in captures if not c.get('processed')] + + if not unprocessed: + print("Nothing to export") + return + + # Group by type + by_type = {} + for item in unprocessed: + t = item['type'] + if t not in by_type: + by_type[t] = [] + by_type[t].append(item) + + print("# Inbox Export") + print(f"*Exported: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n") + + for type_name, items in sorted(by_type.items()): + emoji = TYPES.get(type_name, 'šŸ“') + print(f"## {emoji} {type_name.title()}s\n") + for item in items: + tags = ' '.join(f"`#{t}`" for t in item.get('tags', [])) + print(f"- {item['content']} {tags}") + print() + +def main(): + if len(sys.argv) < 2: + print("Usage:") + print(" capture - Quick capture") + print(" capture -t task - Capture as task") + print(" capture -t idea - Capture as idea") + print(" capture -t link - Capture a link") + print(" capture list - Show inbox") + print(" capture list --all - Show all (incl. processed)") + print(" capture done - Mark as processed") + print(" capture clear - Clear processed items") + print(" capture export - Export as markdown") + print("") + print(f"Types: {', '.join(TYPES.keys())}") + list_captures() + return + + # Parse args + if sys.argv[1] == 'list': + show_all = '--all' in sys.argv + list_captures(show_all=show_all) + elif sys.argv[1] == 'done' and len(sys.argv) > 2: + process_item(int(sys.argv[2])) + elif sys.argv[1] == 'clear': + clear_processed() + elif sys.argv[1] == 'export': + export_markdown() + elif sys.argv[1] == '-t' and len(sys.argv) > 3: + capture_type = sys.argv[2] + content = ' '.join(sys.argv[3:]) + capture(content, capture_type) + else: + content = ' '.join(sys.argv[1:]) + capture(content) + +if __name__ == "__main__": + main() diff --git a/ws b/ws index 30cce5f..7cf24e6 100755 --- a/ws +++ b/ws @@ -28,6 +28,7 @@ COMMANDS = { 'focus': ('tools/focus.py', 'Pomodoro focus timer'), 'habits': ('tools/habits.py', 'Habit tracker with streaks'), 'track': ('tools/track.py', 'Time tracking'), + 'cap': ('tools/capture.py', 'Quick thought capture'), # Projects 'news': ('projects/news-feed/main.py', 'RSS news reader'),