diff --git a/PROFILES.md b/PROFILES.md new file mode 100644 index 0000000..2fd2927 --- /dev/null +++ b/PROFILES.md @@ -0,0 +1,115 @@ +# Go Note Go Profiles + +Profiles allow you to quickly switch between different Go Note Go configurations. Each profile stores a complete snapshot of all settings, plus a name and optional numeric shortcut. + +## Quick Start + +### Create Your First Profile + +Set up your settings the way you want them, then save as a profile with a shortcut: + +``` +:profile save roam 1 +``` + +This creates a profile named "roam" with shortcut `:1`. + +### Switch Between Profiles + +Use the numeric shortcuts you've configured: +``` +:1 # Switch to profile with shortcut 1 +:2 # Switch to profile with shortcut 2 +:3 # Switch to profile with shortcut 3 +:4 # Switch to profile with shortcut 4 +``` + +Or use the full command: +``` +:profile load roam +``` + +## Commands + +### Save a Profile +``` +:profile save # Save without shortcut +:profile save # Save with numeric shortcut +``` + +Examples: +``` +:profile save roam 1 # Save as "roam" with shortcut :1 +:profile save work 2 # Save as "work" with shortcut :2 +:profile save temp # Save as "temp" with no shortcut +``` + +### Load a Profile +``` +:profile load # Load by name +: # Load by shortcut (if configured) +``` + +Your current settings are automatically backed up to the `backup` profile before loading. + +### List Profiles +``` +:profile list +``` +Shows all saved profiles with their shortcuts (if any). + +Example output: `Profiles: backup, roam (:1), temp, work (:2)` + +### Current Profile +``` +:profile current +``` +Shows which profile is currently active. + +### Delete a Profile +``` +:profile delete +``` +Deletes a saved profile and removes its shortcut mapping. + +## Example Use Cases + +### Personal vs Work +``` +:profile save personal 1 # Personal Roam graph +:profile save work 2 # Work note-taking system +``` + +Then switch with `:1` or `:2` + +### Different Assistants +``` +:profile save assistant 3 # Connected to personal assistant +:profile save guest 4 # Safe settings for demos +``` + +### Different Upload Destinations +Switch between Roam, RemNote, Notion, etc. with a single command. + +## How It Works + +- Each profile stores all settings from `secure_settings.py` as JSON in Redis +- Profile metadata (name, shortcut) is stored with the settings +- Shortcuts are mapped dynamically in Redis (no hardcoded values) +- When you load a profile, current settings are backed up to `backup` +- The `backup` profile always contains your most recent settings + +## Safety + +- Your current settings are always backed up to `backup` before loading a new profile +- The secure_settings.py file is never modified - profiles only affect Redis settings +- You can always revert: `:profile load backup` +- Deleting a profile also removes its shortcut mapping + +## Configuration + +Profiles are completely user-configurable: +- Choose your own profile names +- Assign any numeric shortcuts you want +- No hardcoded profile names in the codebase +- Works for any Go Note Go user diff --git a/gonotego/command_center/commands.py b/gonotego/command_center/commands.py index 8f3ebea..d4b2431 100644 --- a/gonotego/command_center/commands.py +++ b/gonotego/command_center/commands.py @@ -8,6 +8,7 @@ from gonotego.command_center import assistant_commands # noqa: F401 from gonotego.command_center import custom_commands # noqa: F401 from gonotego.command_center import note_commands # noqa: F401 +from gonotego.command_center import profile_commands # noqa: F401 from gonotego.command_center import settings_commands # noqa: F401 from gonotego.command_center import system_commands # noqa: F401 from gonotego.command_center import twitter_commands # noqa: F401 diff --git a/gonotego/command_center/profile_commands.py b/gonotego/command_center/profile_commands.py new file mode 100644 index 0000000..2fb4ed6 --- /dev/null +++ b/gonotego/command_center/profile_commands.py @@ -0,0 +1,79 @@ +# Profile commands for switching between Go Note Go configurations. + +from gonotego.command_center import registry +from gonotego.command_center import system_commands +from gonotego.settings import profiles + +register_command = registry.register_command +say = system_commands.say + + +@register_command(r'p(\d)') +def load_profile_shortcut(shortcut): + """Load a profile using numeric shortcut (e.g., :p1, :p2, :p3).""" + # Check if this is a numeric shortcut + if not shortcut.isdigit(): + return # Not a profile shortcut + + profile_name = profiles.get_profile_by_shortcut(shortcut) + if profile_name is None: + say(f'Profile {shortcut} not found') + return # No profile mapped to this shortcut + + result = profiles.load_profile(profile_name) + + if result is None: + say(f'Profile {profile_name} not found.') + else: + say(f'Loaded profile: {profile_name}') + + +@register_command('profile save {} {}') +def save_profile_with_shortcut(profile_name, shortcut): + """Save current settings as a named profile with a shortcut.""" + profiles.save_profile(profile_name, shortcut=shortcut) + say(f'Saved profile: {profile_name} with shortcut :{shortcut}') + + +@register_command('profile save {}') +def save_profile(profile_name): + """Save current settings as a named profile.""" + profiles.save_profile(profile_name) + say(f'Saved profile: {profile_name}') + + +@register_command('profile load {}') +def load_profile(profile_name): + """Load a named profile.""" + result = profiles.load_profile(profile_name) + if result is None: + say(f'Profile not found: {profile_name}') + else: + say(f'Loaded profile: {profile_name}') + + +@register_command('profile list') +def list_profiles(): + """List all saved profiles with their shortcuts.""" + profile_names = profiles.list_profiles() + if not profile_names: + say('No profiles saved.') + else: + say(f'Profiles: {", ".join(profile_names)}') + + +@register_command('profile current') +def current_profile(): + """Show the currently active profile.""" + current = profiles.get_current_profile() + if current is None: + say('No profile currently active.') + else: + say(f'Current profile: {current}') + + +@register_command('profile delete {}') +def delete_profile(profile_name): + """Delete a saved profile.""" + profiles.delete_profile(profile_name) + say(f'Deleted profile: {profile_name}') diff --git a/gonotego/settings-server/package-lock.json b/gonotego/settings-server/package-lock.json index b81a6e0..2f4f435 100644 --- a/gonotego/settings-server/package-lock.json +++ b/gonotego/settings-server/package-lock.json @@ -2503,9 +2503,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001679", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001679.tgz", - "integrity": "sha512-j2YqID/YwpLnKzCmBOS4tlZdWprXm3ZmQLBH9ZBXFOhoxLA46fwyBvx6toCBWBmnuwUY/qB3kEU6gFx8qgCroA==", + "version": "1.0.30001713", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz", + "integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==", "dev": true, "funding": [ { @@ -2520,8 +2520,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/chalk": { "version": "4.1.2", diff --git a/gonotego/settings/profiles.py b/gonotego/settings/profiles.py new file mode 100644 index 0000000..a909322 --- /dev/null +++ b/gonotego/settings/profiles.py @@ -0,0 +1,184 @@ +"""Profile management for Go Note Go settings. + +Profiles allow switching between different configurations. +Each profile stores a complete snapshot of all settings plus metadata (name, shortcut). +""" +import json +from gonotego.settings import settings +from gonotego.settings import secure_settings +from gonotego.common import interprocess + +PROFILES_KEY = 'GoNoteGo:profiles' +PROFILE_SHORTCUTS_KEY = 'GoNoteGo:profile_shortcuts' +CURRENT_PROFILE_KEY = 'GoNoteGo:current_profile' + + +def get_redis_key(profile_name): + """Get Redis key for a specific profile.""" + return f'{PROFILES_KEY}:{profile_name}' + + +def get_all_settings(): + """Get all current settings as a dict.""" + settings_dict = {} + # Get all setting names from secure_settings + setting_names = [s for s in dir(secure_settings) if not s.startswith('_')] + for key in setting_names: + settings_dict[key] = settings.get(key) + return settings_dict + + +def save_profile(profile_name, shortcut=None): + backup_profile(profile_name) + return save_profile_raw(profile_name, shortcut=shortcut) + + +def save_profile_raw(profile_name, shortcut=None): + """Save current settings to a named profile. + + Args: + profile_name: Name of the profile + shortcut: Optional numeric shortcut (e.g., '1', '2', '3') + """ + r = interprocess.get_redis_client() + current_settings = get_all_settings() + + # Store profile data + profile_data = { + 'name': profile_name, + 'settings': current_settings, + } + return save_profile_data(profile_name, profile_data, shortcut=shortcut) + + +def save_profile_data(profile_name, profile_data, shortcut=None): + r = interprocess.get_redis_client() + current_settings = get_all_settings() + + if shortcut is not None: + profile_data['shortcut'] = shortcut + + profile_json = json.dumps(profile_data) + r.set(get_redis_key(profile_name), profile_json) + + # Update shortcuts mapping if shortcut is provided + if shortcut is not None: + shortcuts = get_shortcuts_mapping() + shortcuts[shortcut] = profile_name + r.set(PROFILE_SHORTCUTS_KEY, json.dumps(shortcuts)) + + return current_settings + + +def backup_profile(profile_name): + profile_data = get_profile_data(profile_name) + if profile_data is not None: + save_profile_data(f'{profile_name}.backup', profile_data) + + +def get_profile_data(profile_name): + r = interprocess.get_redis_client() + + profile_json = r.get(get_redis_key(profile_name)) + if profile_json is None: + return None + + profile_data = json.loads(profile_json) + return profile_data + + +def load_profile(profile_name): + """Load settings from a named profile. + + 1. Backs up current settings to 'backup' profile + 2. Loads all settings from the specified profile + 3. Sets current profile marker + """ + r = interprocess.get_redis_client() + + # First, backup current settings (without shortcut) + save_profile('backup', shortcut=None) + + # Load the requested profile + profile_data = get_profile_data(profile_name) + if profile_data is None: + return None + profile_settings = profile_data.get('settings', profile_data) # Backward compat + + # Clear all current settings and load profile settings + settings.clear_all() + for key, value in profile_settings.items(): + settings.set(key, value) + + # Mark this as the current profile + r.set(CURRENT_PROFILE_KEY, profile_name) + + return profile_settings + + +def get_current_profile(): + """Get the name of the currently active profile.""" + r = interprocess.get_redis_client() + profile_bytes = r.get(CURRENT_PROFILE_KEY) + if profile_bytes is None: + return None + return profile_bytes.decode('utf-8') + + +def get_shortcuts_mapping(): + """Get the mapping of shortcuts to profile names.""" + r = interprocess.get_redis_client() + shortcuts_json = r.get(PROFILE_SHORTCUTS_KEY) + if shortcuts_json is None: + return {} + return json.loads(shortcuts_json) + + +def get_profile_by_shortcut(shortcut): + """Get profile name for a given shortcut.""" + shortcuts = get_shortcuts_mapping() + return shortcuts.get(shortcut) + + +def list_profiles(): + """List all saved profile names with their shortcuts.""" + r = interprocess.get_redis_client() + profile_keys = r.keys(get_redis_key('*')) + + profiles_info = [] + shortcuts = get_shortcuts_mapping() + # Reverse mapping for lookup + name_to_shortcut = {v: k for k, v in shortcuts.items()} + + for key in profile_keys: + key_str = key.decode('utf-8') if isinstance(key, bytes) else key + # Extract profile name from key + if key_str.startswith(f'{PROFILES_KEY}:'): + profile_name = key_str[len(f'{PROFILES_KEY}:'):] + shortcut = name_to_shortcut.get(profile_name) + if shortcut: + profiles_info.append(f"{profile_name} ({shortcut})") + else: + profiles_info.append(profile_name) + + return sorted(profiles_info) + + +def delete_profile(profile_name): + """Delete a saved profile and remove its shortcut if any.""" + r = interprocess.get_redis_client() + + # Remove from shortcuts mapping + shortcuts = get_shortcuts_mapping() + shortcut_to_remove = None + for shortcut, name in shortcuts.items(): + if name == profile_name: + shortcut_to_remove = shortcut + break + + if shortcut_to_remove: + del shortcuts[shortcut_to_remove] + r.set(PROFILE_SHORTCUTS_KEY, json.dumps(shortcuts)) + + # Delete the profile + r.delete(get_redis_key(profile_name))