Importers
An importer takes files dropped on the launcher window: the plugin claims them by extension, or decides for itself, and the launcher sends them to its import call.
An importer in full
The template claims .exmod files and copies each into its data folder:
json
"launcher": {
"program": { "windows": "bin/example-helper.exe", "linux": "bin/example-helper" },
"sections": [{ "id": "items", "title": "Example", "icon": "package" }],
"imports": ["exmod"]
}rust
fn import(&self, host: &mut Host, paths: &[PathBuf]) -> Result<Vec<String>, String> {
std::fs::create_dir_all(self.items_dir()).map_err(|e| e.to_string())?;
let mut lines = Vec::new();
for (i, path) in paths.iter().enumerate() {
if !is_item(path) {
lines.push(format!("Skipped {}: not a .{EXTENSION} file", path.display()));
continue;
}
let name = path.file_name().and_then(|n| n.to_str())
.ok_or_else(|| format!("{} has no name", path.display()))?;
// fills the progress card, 0.0 to 1.0
host.progress(i as f32 / paths.len() as f32, &format!("Importing {name}"));
std::fs::copy(path, self.items_dir().join(name)).map_err(|e| format!("{name}: {e}"))?;
lines.push(format!("Imported {name}"));
}
Ok(lines) // one line per file
}text
launcher {"id":3,"method":"import","params":{"paths":["/home/you/Downloads/okayama.exmod","/home/you/Downloads/suzuka.exmod"]}}
plugin {"method":"progress","params":{"done":0.0,"text":"Importing okayama.exmod"}}
plugin {"method":"progress","params":{"done":0.5,"text":"Importing suzuka.exmod"}}
plugin {"id":3,"result":["Imported okayama.exmod","Imported suzuka.exmod"]}- Files routed to your plugin arrive together in one
importcall, as absolute paths. importhas 300 seconds; eachprogressresets the wait.- A
pickbutton gets files from the file picker instead, and the template passes them to the same function.
How a drop is routed
- The launcher claims addons first: a folder with
swofty.json, a.swoftyaddonfile (or a legacy.swoftymod), or a zip that holds an addon. Those install as addons. - It checks every plugin with
importsorclaimsthat is on in the active profile, in registry order. A plugin that's off isn't run and can't claim anything. A plugin withimportsclaims a file when its extension matches, ignoring case. A plugin withclaimsdecides for itself. - A claimed file goes to that plugin's
import. An unclaimed one opens a dialog that names it, with Go to Simplified Importer and Read the addon docs.
claims
Matching extensions can't tell a mod's zip from any other zip, and can't claim a folder. To decide for yourself, set claims to true:
json
"launcher": {
"program": { "windows": "bin/importer.exe", "linux": "bin/importer" },
"imports": ["zip"],
"claims": true
}The launcher then runs your program on its own for each drop, and reads one line:
text
$ bin/importer claims /home/you/Downloads/okayama.zip
yesrust
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
if let [verb, path] = args.as_slice() && verb == "claims" {
// yes, maybe or no; anything else falls back to imports
println!("{}", if looks_like_a_mod(std::path::Path::new(path)) { "yes" } else { "no" });
return;
}
// …the protocol loop
}- The answer is
yes,maybeorno. Amaybewins only when no other plugin saysyesormaybe. - Answer within 5 seconds, without the protocol: there is no
helloand no profile. The working directory is the addon folder, and stdin and stderr are closed. - If your program fails, stalls or prints anything else, the launcher falls back to
imports. - A Rust helper that uses
swofty_addon::helper::run_helperanswers from itsImporter::claimsfor you (The Rust side).