diff --git a/README.md b/README.md index 37cab24..b0a6c64 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,28 @@ bash scripts/install-kwin-script.sh Create your config files in `~/.config/shortcut-sage/`: +**config.toml**: Global settings (optional, uses defaults if not present) +```toml +[privacy] +# Enable window title capture for better context-aware suggestions +# Recommended: true for best results, false if privacy is a concern +capture_window_titles = false + +# Optionally redact titles from logs (reduces audit trail quality) +redact_titles_from_logs = true + +[engine] +buffer_size_seconds = 3 +default_cooldown_seconds = 300 +max_suggestions = 3 + +[overlay] +position = "top-left" +opacity = 0.9 +``` + +See [config.toml](config.toml) for all available options. + **shortcuts.yaml**: Define your shortcuts ```yaml version: "1.0" @@ -152,9 +174,12 @@ See [implementation-plan.md](implementation-plan.md) for full roadmap. - **No keylogging**: Only symbolic events (window focus, desktop switch) - **Local processing**: No cloud, no telemetry -- **Redacted by default**: Window titles not logged +- **Flexible data capture**: Window titles configurable via config.toml for improved suggestions +- **Optional redaction**: Separate control for logging vs. suggestion logic (if privacy preferred) - **Open source**: Audit the code yourself +> **Note**: For best results, enable `capture_window_titles = true` in config.toml. This allows more targeted, context-aware suggestions. Privacy controls are available but may reduce suggestion quality. + ## Contributing Contributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md) first. diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..8b5f04a --- /dev/null +++ b/config.toml @@ -0,0 +1,63 @@ +# Shortcut Sage Configuration +# Place this file in your config directory (default: ~/.config/shortcut-sage/) + +[general] +# Enable debug logging (verbose output) +debug = false + +[privacy] +# Capture and use window titles (captions) in suggestion logic +# - true: Full window titles sent to daemon (e.g., "Mozilla Firefox - Stack Overflow") +# RECOMMENDED for better, more context-aware suggestions +# - false: Only application resource class sent (e.g., "firefox") +# Limits suggestion accuracy for privacy +# +# Window titles enable more targeted suggestions based on: +# - Document names (suggest Kate shortcuts when editing *.py files) +# - Web page context (suggest browser shortcuts on specific sites) +# - More nuanced workflow understanding +capture_window_titles = false + +# Redact window titles from persistent logs (even if capture_window_titles = true) +# Set to false if you want full audit trails for debugging/improvement +redact_titles_from_logs = true + +[engine] +# Ring buffer size in seconds (how far back to look for patterns) +buffer_size_seconds = 3 + +# Default cooldown in seconds (can be overridden per-rule in rules.yaml) +default_cooldown_seconds = 300 + +# Maximum number of suggestions to show simultaneously +max_suggestions = 3 + +[overlay] +# Show the suggestion overlay +enabled = true + +# Overlay position (top-left, top-right, bottom-left, bottom-right) +position = "top-left" + +# Overlay offset from screen edge in pixels +offset_x = 20 +offset_y = 20 + +# Auto-hide overlay after N seconds (0 = never auto-hide) +auto_hide_seconds = 0 + +# Overlay transparency (0.0 = invisible, 1.0 = opaque) +opacity = 0.9 + +[logging] +# Log format: "json" or "ndjson" +format = "ndjson" + +# Log rotation: maximum file size in MB before rotation +max_log_size_mb = 10 + +# Number of rotated log files to keep +max_log_files = 5 + +# Log level: "debug", "info", "warning", "error" +level = "info" diff --git a/kwin/event-monitor.js b/kwin/event-monitor.js index c2b83e7..ef8336e 100644 --- a/kwin/event-monitor.js +++ b/kwin/event-monitor.js @@ -1,125 +1,176 @@ -/* - * Shortcut Sage Event Monitor - KWin Script - * Monitors KDE Plasma events and sends them to Shortcut Sage daemon +/** + * Shortcut Sage - KWin Event Monitor + * + * Monitors desktop events and sends them to the Shortcut Sage daemon via DBus. + * + * Events monitored: + * - Desktop/workspace switches + * - Window focus changes (including window titles for better context) + * - Show desktop state changes + * + * Dev shortcut: Meta+Shift+S sends a test event + * + * Window titles are sent to improve suggestion accuracy. The daemon's + * config.toml controls whether titles are used for suggestions and/or + * logged. Configure privacy.capture_window_titles to balance accuracy + * vs. privacy based on your preferences. */ -// Configuration -const DAEMON_SERVICE = "org.shortcutsage.Daemon"; -const DAEMON_PATH = "/org/shortcutsage/Daemon"; +// DBus connection to Shortcut Sage daemon +const BUS_NAME = "org.shortcutsage.Daemon"; +const OBJECT_PATH = "/org/shortcutsage/Daemon"; +const INTERFACE = "org.shortcutsage.Daemon"; -// Initialize DBus interface -function initDBus() { - try { - var dbusInterface = workspace.knownInterfaces[DAEMON_SERVICE]; - if (dbusInterface) { - print("Found Shortcut Sage daemon interface"); - return true; - } else { - print("Shortcut Sage daemon not available"); - return false; - } - } catch (error) { - print("Failed to connect to Shortcut Sage daemon: " + error); - return false; +// Logging configuration +const DEBUG = false; // Set to true for verbose logging during development +const LOG_PREFIX = "[ShortcutSage]"; + +// Helper function for logging +function log(message) { + if (DEBUG) { + console.log(LOG_PREFIX + " " + message); } } -// Function to send event to daemon via DBus +function logError(message) { + console.error(LOG_PREFIX + " ERROR: " + message); +} + +// Initialize the script +log("Initializing KWin Event Monitor"); + +/** + * Send an event to the daemon via DBus + * @param {string} type - Event type (e.g., "window_focus", "desktop_switch") + * @param {string} action - Action name (e.g., "show_desktop", "tile_left") + * @param {Object} metadata - Additional metadata (optional) + */ function sendEvent(type, action, metadata) { - // Using DBus to call the daemon's SendEvent method - callDBus( - DAEMON_SERVICE, - DAEMON_PATH, - DAEMON_SERVICE, - "SendEvent", - JSON.stringify({ + try { + // Build event object + const event = { timestamp: new Date().toISOString(), type: type, action: action, metadata: metadata || {} - }) - ); -} + }; -// Monitor workspace events -function setupEventListeners() { - // Desktop switch events - workspace.clientDesktopChanged.connect(function(client, desktop) { - sendEvent("desktop_switch", "switch_desktop", { - window: client ? client.caption : "unknown", - desktop: desktop - }); - }); - - // Window focus events - workspace.clientActivated.connect(function(client) { - if (client) { - sendEvent("window_focus", "window_focus", { - window: client.caption, - app: client.resourceClass ? client.resourceClass.toString() : "unknown" - }); - } - }); - - // Screen edge activation (overview, etc.) - workspace.screenEdgeActivated.connect(function(edge, desktop) { - var action = "unknown"; - if (edge === 0) action = "overview"; // Top edge usually shows overview - else if (edge === 2) action = "application_launcher"; // Bottom edge - else action = "screen_edge"; - - sendEvent("desktop_state", action, { - edge: edge, - desktop: desktop - }); - }); - - // Window geometry changes (for tiling, maximizing, etc.) - workspace.clientStepUserMovedResized.connect(function(client, step) { - if (client && step) { - var action = "window_move"; - if (client.maximizedHorizontally && client.maximizedVertically) { - action = "maximize"; - } else if (!client.maximizedHorizontally && !client.maximizedVertically) { - action = "window_move"; - } - - sendEvent("window_state", action, { - window: client.caption, - maximized: client.maximizedHorizontally && client.maximizedVertically - }); - } - }); + const eventJson = JSON.stringify(event); + log("Sending event: " + eventJson); + + // Call DBus method + callDBus( + BUS_NAME, + OBJECT_PATH, + INTERFACE, + "SendEvent", + eventJson + ); + } catch (error) { + logError("Failed to send event: " + error); + } } -// Register a test shortcut for development -function setupTestShortcut() { - registerShortcut( - "Shortcut Sage Test", - "Test shortcut for Shortcut Sage development", - "Ctrl+Alt+S", - function() { - sendEvent("test", "test_shortcut", { - source: "kwin_script" - }); - } - ); +/** + * Ping the daemon to check if it's alive + */ +function pingDaemon() { + try { + const result = callDBus( + BUS_NAME, + OBJECT_PATH, + INTERFACE, + "Ping" + ); + log("Ping result: " + result); + return result === "pong"; + } catch (error) { + logError("Daemon not responding to ping: " + error); + return false; + } } -// Initialize when script loads -function init() { - print("Shortcut Sage KWin script initializing..."); - - if (initDBus()) { - setupEventListeners(); - setupTestShortcut(); - print("Shortcut Sage KWin script initialized successfully"); - } else { - print("Shortcut Sage KWin script initialized in fallback mode - daemon not available"); - // Still set up events but with fallback behavior if needed - setupTestShortcut(); +// Track previous state to detect changes +// These variables must be mutable (using `let`) because they are updated +// in event handlers to compare with new states +let previousDesktop = workspace.currentDesktop; +let showingDesktop = workspace.showingDesktop; + +/** + * Monitor desktop/workspace switches + */ +workspace.currentDesktopChanged.connect(function(desktop, client) { + if (desktop !== previousDesktop) { + log("Desktop switched: " + previousDesktop + " -> " + desktop); + sendEvent( + "desktop_switch", + "switch_desktop", + { + from: previousDesktop, + to: desktop + } + ); + previousDesktop = desktop; + } +}); + +/** + * Monitor "Show Desktop" state changes + */ +workspace.showingDesktopChanged.connect(function(showing) { + if (showing !== showingDesktop) { + log("Show desktop changed: " + showing); + const action = showing ? "show_desktop" : "hide_desktop"; + sendEvent( + "show_desktop", + action, + { showing: showing } + ); + showingDesktop = showing; + } +}); + +/** + * Monitor active window (focus) changes + */ +workspace.clientActivated.connect(function(client) { + if (client) { + log("Window activated: " + client.caption); + sendEvent( + "window_focus", + "window_activated", + { + caption: client.caption, + resourceClass: client.resourceClass || "unknown" + } + ); + } +}); + +/** + * Dev shortcut: Meta+Shift+S to send a test event + */ +registerShortcut( + "ShortcutSage: Test Event", + "ShortcutSage: Send Test Event (Meta+Shift+S)", + "Meta+Shift+S", + function() { + log("Test event triggered"); + sendEvent( + "test", + "test_event", + { source: "dev_shortcut" } + ); } +); + +// Ping daemon on startup to verify connection +log("Pinging daemon..."); +if (pingDaemon()) { + log("Successfully connected to daemon"); +} else { + logError("Could not connect to daemon - is it running?"); + logError("Start the daemon with: shortcut-sage daemon "); } -// Run initialization -init(); \ No newline at end of file +log("KWin Event Monitor initialized successfully"); diff --git a/kwin/metadata.json b/kwin/metadata.json new file mode 100644 index 0000000..22dd12c --- /dev/null +++ b/kwin/metadata.json @@ -0,0 +1,20 @@ +{ + "KPlugin": { + "Name": "Shortcut Sage Event Monitor", + "Description": "Monitors desktop events and sends them to Shortcut Sage daemon for context-aware keyboard shortcut suggestions", + "Authors": [ + { + "Name": "Coldaine", + "Email": "pmaclyman@gmail.com" + } + ], + "Category": "Accessibility", + "License": "MIT", + "Version": "0.1.0", + "Website": "https://github.com/Coldaine/ShortcutSage", + "Id": "shortcut-sage-event-monitor", + "EnabledByDefault": true + }, + "X-Plasma-API": "javascript", + "X-Plasma-MainScript": "event-monitor.js" +} diff --git a/scripts/install-kwin-script.sh b/scripts/install-kwin-script.sh new file mode 100644 index 0000000..44a757c --- /dev/null +++ b/scripts/install-kwin-script.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# +# Installation script for Shortcut Sage KWin Event Monitor +# +# This script installs the KWin script that monitors desktop events +# and sends them to the Shortcut Sage daemon via DBus. +# + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +KWIN_DIR="$PROJECT_ROOT/kwin" + +# KWin scripts directory +KWIN_SCRIPTS_DIR="$HOME/.local/share/kwin/scripts" +SCRIPT_NAME="shortcut-sage-event-monitor" +INSTALL_DIR="$KWIN_SCRIPTS_DIR/$SCRIPT_NAME" + +echo "=== Shortcut Sage KWin Script Installer ===" +echo "" + +# Check if KDE Plasma is running +if [ -z "$KDE_SESSION_VERSION" ]; then + echo -e "${YELLOW}Warning: KDE Plasma not detected. This script is designed for KDE Plasma.${NC}" + echo "Continue anyway? (y/N)" + read -r response + if [[ ! "$response" =~ ^[Yy]$ ]]; then + echo "Installation aborted." + exit 1 + fi +fi + +# Check if source files exist +if [ ! -f "$KWIN_DIR/event-monitor.js" ]; then + echo -e "${RED}Error: event-monitor.js not found in $KWIN_DIR${NC}" + exit 1 +fi + +if [ ! -f "$KWIN_DIR/metadata.json" ]; then + echo -e "${RED}Error: metadata.json not found in $KWIN_DIR${NC}" + exit 1 +fi + +# Create KWin scripts directory if it doesn't exist +echo "Creating KWin scripts directory..." +mkdir -p "$KWIN_SCRIPTS_DIR" + +# Create installation directory +echo "Installing KWin script to $INSTALL_DIR..." +mkdir -p "$INSTALL_DIR" + +# Copy files +cp "$KWIN_DIR/event-monitor.js" "$INSTALL_DIR/" +cp "$KWIN_DIR/metadata.json" "$INSTALL_DIR/" + +# Check if script is already enabled +if kreadconfig5 --file kwinrc --group Plugins --key "$SCRIPT_NAME"Enabled 2>/dev/null | grep -q "true"; then + echo -e "${GREEN}Script is already enabled${NC}" +else + # Enable the script + echo "Enabling KWin script..." + kwriteconfig5 --file kwinrc --group Plugins --key "${SCRIPT_NAME}Enabled" true +fi + +# Reload KWin scripts +echo "Reloading KWin scripts..." +if command -v qdbus &> /dev/null; then + qdbus org.kde.KWin /KWin reconfigure 2>/dev/null || true +elif command -v qdbus-qt5 &> /dev/null; then + qdbus-qt5 org.kde.KWin /KWin reconfigure 2>/dev/null || true +else + echo -e "${YELLOW}Warning: qdbus not found. Please restart KWin manually or log out/in.${NC}" +fi + +echo "" +echo -e "${GREEN}✓ Installation complete!${NC}" +echo "" +echo "The KWin event monitor is now installed and enabled." +echo "" +echo "To verify installation:" +echo " 1. Open KDE System Settings → Window Management → KWin Scripts" +echo " 2. Look for 'Shortcut Sage Event Monitor' in the list" +echo "" +echo "To test the script:" +echo " 1. Start the daemon: shortcut-sage daemon ~/.config/shortcut-sage" +echo " 2. Press Meta+Shift+S to send a test event" +echo " 3. Check daemon logs for the test event" +echo "" +echo "To uninstall:" +echo " rm -rf $INSTALL_DIR" +echo " kwriteconfig5 --file kwinrc --group Plugins --key ${SCRIPT_NAME}Enabled false" +echo ""