# Plugin System CupidFM can be extended with plugins written in **CupidScript**, a lightweight scripting language embedded in the file manager. ## Overview Plugins allow you to: - Hook into key events - Respond to directory/selection changes - Create custom file operations - Extend CupidFM with new features - Automate repetitive tasks ## Quick Start ### 1. Create Plugin Directory ```bash mkdir -p ~/.cupidfm/plugins ``` ### 2. Write Your First Plugin Create `~/.cupidfm/plugins/hello.cs`: ```cs // hello.cs - Simple greeting plugin fn on_load() { fm.notify("Hello Plugin loaded!"); } fn on_key(key) { if (key == "h") { fm.notify("You pressed 'h'!"); return true; // Consume the key } return false; // Let CupidFM handle it } ``` ### 3. Restart CupidFM ```bash ./cupidfm ``` You'll see "Hello Plugin loaded!" on startup. Press `h` to see the notification! ## Plugin Locations CupidFM searches for plugins in: ### User Plugins (Always Loaded) 1. `~/.cupidfm/plugins/` 2. `~/.cupidfm/plugin/` ### Local Plugins (Require Environment Variable) Set `CUPIDFM_LOAD_LOCAL_PLUGINS=1` to enable: 3. `./cupidfm/plugins/` 4. `./cupidfm/plugin/` 5. `./plugins/` 6. `./plugin/` **Security:** Local plugins are disabled by default to prevent accidentally running untrusted code. ```bash # Enable local plugin loading export CUPIDFM_LOAD_LOCAL_PLUGINS=1 ./cupidfm ``` ## Plugin Hooks Plugins can implement these hooks: ### `on_load()` Called when CupidFM starts and the plugin is loaded. ```cs fn on_load() { fm.notify("Plugin initialized"); // Setup code here } ``` ### `on_key(key)` Called when a key is pressed. Return `true` to consume the key, `false` to pass it through. ```cs fn on_key(key) { if (key == "g") { fm.notify("Custom 'g' handler"); return true; // Key handled } return false; // Let CupidFM process it } ``` **Key format:** - Regular keys: `"a"`, `"b"`, `"1"` - Control keys: `"^C"`, `"^S"`, `"^X"` - Special keys: `"KEY_UP"`, `"KEY_DOWN"`, `"F1"` ### `on_dir_change(new_cwd, old_cwd)` Called when the current directory changes. ```cs fn on_dir_change(new_cwd, old_cwd) { fm.notify("Entered: " + new_cwd); } ``` ### `on_selection_change(new_name, old_name)` Called when the selected file/directory changes. ```cs fn on_selection_change(new_name, old_name) { if (ends_with(new_name, ".txt")) { fm.notify("Text file selected"); } } ``` ## CupidFM API (`fm.*`) This page contains a quick overview of the plugin API. The canonical, complete reference lives in `../CUPIDFM_CUPIDSCRIPT_API.md`. ### UI Functions | Function | Description | |----------|-------------| | `fm.notify(msg)` | Show a notification in the status bar | | `fm.status(msg)` | Alias for `fm.notify` | | `fm.popup(title, msg)` | Show a modal popup dialog | | `fm.console_print(msg)` / `fm.console(msg)` | Write a line to CupidFM’s in-app console | | `fm.prompt(title, initial)` | Prompt for text input; returns `string` or `nil` | | `fm.confirm(title, msg)` | Prompt yes/no; returns `bool` | | `fm.menu(title, items)` | Prompt menu; returns selected index or `-1` | | `fm.prompt_async(title, initial, cb)` | Async prompt (callback-based) | | `fm.confirm_async(title, msg, cb)` | Async confirm (callback-based) | | `fm.menu_async(title, items, cb)` | Async menu (callback-based) | #### `fm.notify(message)` Show a notification in the status bar. ```cs fm.notify("Operation complete!"); ``` #### `fm.popup(title, message)` Show a modal popup dialog. ```cs fm.popup("Warning", "Are you sure?"); ``` #### `fm.prompt(title, default_value)` Show an input prompt and return the user's input. ```cs let filename = fm.prompt("Enter filename:", "newfile.txt"); if (filename) { fm.notify("You entered: " + filename); } ``` ### Context, Navigation, and File Ops | Function | Description | |----------|-------------| | `fm.cwd()` | Current directory (left pane) | | `fm.selected_name()` | Currently selected name (or `""`) | | `fm.selected_path()` | Absolute path to selected entry | | `fm.selected_paths()` | List of selected paths (multi-select) | | `fm.entries()` | Visible directory listing as a list of maps | | `fm.cursor()` | Current cursor index (or `-1`) | | `fm.count()` | Count of visible entries | | `fm.pane()` | `"directory"` or `"preview"` | | `fm.search_active()` | Whether search UI is active | | `fm.search_query()` | Current search query text | | `fm.set_search(query)` | Set search query from script | | `fm.clear_search()` | Clear search query | | `fm.bind(key, func_name)` | Bind a key to a function | | `fm.key_name(code)` | Convert integer keycode to string name | | `fm.key_code(name)` | Convert key name string to integer code | | `fm.reload()` | Reload the directory listing / UI | | `fm.exit()` | Exit CupidFM | | `fm.cd(path)` | Change directory | | `fm.select(name)` | Select entry by name | | `fm.select_index(i)` | Select entry by index | | `fm.open_selected()` | Open the current selection | | `fm.enter_dir()` | Enter selected directory | | `fm.parent_dir()` | Go to parent directory | | `fm.copy(path, dst_dir)` | Copy file/dir | | `fm.move(path, dst_dir)` | Move file/dir | | `fm.rename(path, new_name)` | Rename file/dir | | `fm.delete(path)` | Delete file/dir | | `fm.mkdir(name_or_path)` | Create a directory | | `fm.touch(name_or_path)` | Create an empty file | | `fm.undo()` / `fm.redo()` | Undo/redo last file operation | | `fm.each_selected(fn_or_name)` | Iterate selected paths | ### Editor Functions | Function | Description | |----------|-------------| | `fm.editor_active()` | True if the built-in editor is open | | `fm.editor_get_path()` | Current editor file path (or `nil`) | | `fm.editor_line_count()` | Total lines in editor | | `fm.editor_get_cursor()` | Cursor `{line, col}` (1-indexed) | | `fm.editor_set_cursor(line, col)` | Set cursor (1-indexed) | | `fm.editor_get_selection()` | Selection bounds map (1-indexed) | | `fm.editor_get_content()` | Whole buffer as string | | `fm.editor_get_line(n)` / `fm.editor_get_lines(start, end)` | Read lines | | `fm.editor_insert_text(text)` | Insert at cursor | | `fm.editor_replace_text(...)` | Replace range | | `fm.editor_delete_range(...)` | Delete range | | `fm.editor_uppercase_selection()` | Uppercase selection | | `fm.editor_save()` | Save current editor file | | `fm.editor_save_as(path)` | Save to new path (updates editor path) | | `fm.editor_reload()` | Reload file from disk (may prompt discard) | | `fm.editor_close()` | Close editor (may prompt discard) | | `fm.editor_set_readonly(readonly)` | Enable/disable read-only mode | #### `fm.editor_get_path()` Get the currently open editor file path. ```cs let path = fm.editor_get_path(); if (path) { fm.notify("Editing: " + path); } ``` #### `fm.editor_get_content()` Get entire editor content. ```cs let content = fm.editor_get_content(); let lines = len(split(content, "\n")); fm.notify("File has " + str(lines) + " lines"); ``` #### `fm.editor_insert_text(text)` Insert text at cursor position. ```cs fm.editor_insert_text("// TODO: "); ``` #### `fm.editor_get_cursor()` Get cursor position `{line, col}` (1-indexed). ```cs let pos = fm.editor_get_cursor(); fm.notify("Cursor at line " + str(pos[0])); ``` #### `fm.editor_set_cursor(line, column)` Set cursor position. ```cs fm.editor_set_cursor(1, 1); // Move to start (1-indexed) ``` #### `fm.editor_save()` Save current editor content. ```cs fm.editor_save(); fm.notify("File saved!"); ``` #### `fm.editor_active()` Check if editor is open. ```cs if (fm.editor_active()) { fm.notify("Editor is active"); } ``` ### Legacy API Names (Deprecated) Some older docs and plugins may use legacy names. Prefer the new names above: | Legacy | Use Instead | |--------|-------------| | `fm.get_cwd()` | `fm.cwd()` | | `fm.set_cwd(path)` | `fm.cd(path)` | | `fm.refresh()` | `fm.reload()` | | `fm.get_selected()` | `fm.selected_name()` | | `fm.get_selected_path()` | `fm.selected_path()` | | `fm.get_files()` | `fm.entries()` | | `fm.editor_is_open()` | `fm.editor_active()` | | `fm.editor_insert(text)` | `fm.editor_insert_text(text)` | ## Example Plugins ### Example 1: Auto-Logger Log every directory you visit: ```cs // logger.cs fn on_load() { fm.notify("Directory logger active"); } fn on_dir_change(new_cwd, old_cwd) { let logfile = "/tmp/cupidfm-history.log"; let timestamp = str(time()); let entry = timestamp + " | " + new_cwd + "\n"; // Append to log file (using CupidScript I/O) file_append(logfile, entry); } ``` ### Example 2: Quick Notes Press `n` to quickly create a note: ```cs // quick-notes.cs fn on_key(key) { if (key == "n") { let note = fm.prompt("Quick note:", ""); if (note) { let filename = "note-" + str(time()) + ".txt"; let path = fm.cwd() + "/" + filename; file_write(path, note); fm.reload(); fm.notify("Note saved: " + filename); } return true; } return false; } ``` ### Example 3: Git Status Display Show git status when entering directories: ```cs // git-status.cs fn on_dir_change(new_cwd, old_cwd) { let git_dir = new_cwd + "/.git"; if (file_exists(git_dir)) { // Run git status (using system command) let result = exec("git -C " + new_cwd + " status --short"); if (result) { fm.popup("Git Status", result); } } } ``` ### Example 4: Bulk Rename Press `Ctrl+B` to rename multiple files: ```cs // bulk-rename.cs fn on_key(key) { if (key == "^B") { let pattern = fm.prompt("Find pattern:", ""); if (!pattern) return true; let replacement = fm.prompt("Replace with:", ""); if (!replacement) return true; let entries = fm.entries(); let count = 0; if (entries != nil) { for e in entries { if (e["is_dir"]) continue; let name = e["name"]; if (contains(name, pattern)) { let new_name = replace(name, pattern, replacement); if (fm.rename(name, new_name)) { count = count + 1; } } } } fm.reload(); fm.notify("Renamed " + str(count) + " files"); return true; } return false; } ``` ### Example 5: Markdown Preview Auto-convert Markdown to HTML: ```cs // md-preview.cs fn on_selection_change(new_name, old_name) { if (ends_with(new_name, ".md")) { let md_path = fm.selected_path(); let html_path = replace(md_path, ".md", ".html"); // Convert using pandoc (if installed) let cmd = "pandoc -o " + html_path + " " + md_path; exec(cmd); fm.notify("Converted to HTML"); } } ``` ## CupidScript Language Features ### Variables and Types ```cs let x = 10; let name = "CupidFM"; let list = [1, 2, 3]; let map = {"key": "value"}; ``` ### Functions ```cs fn greet(name) { return "Hello, " + name; } let msg = greet("User"); ``` ### Control Flow ```cs if (condition) { // ... } else { // ... } while (x < 10) { x = x + 1; } for item in list { print(item); } ``` ### String Functions ```cs len(str) // Length split(str, delim) // Split into list contains(str, sub) // Check substring replace(str, old, new) // Replace starts_with(str, prefix) ends_with(str, suffix) ``` ### File I/O ```cs file_read(path) // Read file file_write(path, content) // Write file file_append(path, content) // Append file_exists(path) // Check exists ``` ### System Commands ```cs exec(command) // Run shell command, return output ``` ## Debugging Plugins ### Print to Log ```cs fn on_load() { print("Plugin loaded"); // Prints to log.txt print("Debug value: " + str(variable)); } ``` ### Check Errors ```cs fn on_key(key) { try { // Your code risky_operation(); } catch (err) { fm.popup("Error", err); } return false; } ``` ### Test with Minimal Plugin ```cs fn on_load() { fm.popup("Test", "Plugin works!"); } ``` ## Best Practices 1. **Handle errors** - Use try/catch for file operations 2. **Return key state** - Always return true/false in `on_key()` 3. **Avoid heavy operations** - Don't block in hooks 4. **Use notifications** - Keep users informed 5. **Test incrementally** - Start simple, add features ## Troubleshooting ### Plugin Not Loading 1. Check file is in `~/.cupidfm/plugins/` 2. Ensure file has `.cs` extension 3. Check syntax errors in `log.txt` 4. Verify local plugins are enabled (if needed) ### Hook Not Called 1. Verify function name: `on_load`, `on_key`, etc. 2. Check function signature matches exactly 3. Look for errors in `log.txt` ### API Function Fails 1. Check if editor is open (for editor functions) 2. Verify paths are absolute 3. Check return values for `nil` --- **Navigate:** [← Syntax Highlighting](Syntax-Highlighting.md) | [CupidScript API Reference →](../CUPIDFM_CUPIDSCRIPT_API.md)