use std::path::PathBuf;
use crate::ontology::*;
#[derive(Debug, Clone)]
pub struct FileFilter {
pub name: String,
pub extensions: Vec<String>,
}
impl FileFilter {
pub fn new(name: impl Into<String>, extensions: Vec<String>) -> Self {
Self {
name: name.into(),
extensions,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct OpenFileDialog {
pub title: String,
pub default_path: Option<PathBuf>,
pub filters: Vec<FileFilter>,
pub multiple: bool,
pub directory: bool,
}
impl OpenFileDialog {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
..Default::default()
}
}
pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
self.default_path = Some(path.into());
self
}
pub fn with_filter(mut self, filter: FileFilter) -> Self {
self.filters.push(filter);
self
}
pub fn with_multiple(mut self, multiple: bool) -> Self {
self.multiple = multiple;
self
}
pub fn with_directory(mut self, directory: bool) -> Self {
self.directory = directory;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct SaveFileDialog {
pub title: String,
pub default_name: String,
pub default_path: Option<PathBuf>,
pub filters: Vec<FileFilter>,
}
impl SaveFileDialog {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
..Default::default()
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.default_name = name.into();
self
}
pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
self.default_path = Some(path.into());
self
}
pub fn with_filter(mut self, filter: FileFilter) -> Self {
self.filters.push(filter);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageBoxResult {
Ok,
Cancel,
Yes,
No,
}
#[derive(Debug, Clone)]
pub struct MessageBox {
pub title: String,
pub message: String,
pub show_cancel: bool,
}
impl MessageBox {
pub fn new(title: impl Into<String>, message: impl Into<String>) -> Self {
Self {
title: title.into(),
message: message.into(),
show_cancel: false,
}
}
pub fn with_cancel(mut self) -> Self {
self.show_cancel = true;
self
}
}
pub trait DialogBackend {
fn open_file(&self, config: &OpenFileDialog) -> Vec<PathBuf>;
fn save_file(&self, config: &SaveFileDialog) -> Option<PathBuf>;
fn message_box(&self, config: &MessageBox) -> MessageBoxResult;
}
pub struct NullDialogBackend;
impl DialogBackend for NullDialogBackend {
fn open_file(&self, _config: &OpenFileDialog) -> Vec<PathBuf> {
Vec::new()
}
fn save_file(&self, _config: &SaveFileDialog) -> Option<PathBuf> {
None
}
fn message_box(&self, _config: &MessageBox) -> MessageBoxResult {
MessageBoxResult::Ok
}
}
impl Discoverable for NullDialogBackend {
fn schema(&self) -> WidgetSchema {
let mut schema = WidgetSchema::new(
"DialogBackend",
"Native file and message dialog system for open/save/alert workflows",
SemanticRole::Configuration,
);
schema.usage_hint = Some("backend.open_file(&OpenFileDialog::new(\"Open\"))".into());
schema.tags = vec![
"dialog".into(),
"file".into(),
"open".into(),
"save".into(),
"message".into(),
];
schema
}
fn capabilities(&self) -> Vec<AgentCapability> {
vec![AgentCapability::Clickable]
}
fn actions(&self) -> Vec<AgentAction> {
vec![
AgentAction::with_params(
"open_file",
"Show an open-file dialog",
vec![
ActionParam::optional(
"title",
"Dialog title",
ActionParamType::String,
serde_json::json!("Open"),
),
ActionParam::optional(
"multiple",
"Allow multiple selection",
ActionParamType::Boolean,
serde_json::json!(false),
),
ActionParam::optional(
"directory",
"Allow directory selection",
ActionParamType::Boolean,
serde_json::json!(false),
),
],
false,
),
AgentAction::with_params(
"save_file",
"Show a save-file dialog",
vec![
ActionParam::optional(
"title",
"Dialog title",
ActionParamType::String,
serde_json::json!("Save"),
),
ActionParam::optional(
"default_name",
"Default file name",
ActionParamType::String,
serde_json::json!(""),
),
],
false,
),
AgentAction::with_params(
"message_box",
"Show a message box alert",
vec![
ActionParam::required("title", "Message box title", ActionParamType::String),
ActionParam::required("message", "Message body", ActionParamType::String),
ActionParam::optional(
"show_cancel",
"Show cancel button",
ActionParamType::Boolean,
serde_json::json!(false),
),
],
false,
),
]
}
fn semantic_role(&self) -> SemanticRole {
SemanticRole::Configuration
}
fn agent_state(&self) -> serde_json::Value {
serde_json::json!({
"backend": "null",
"note": "Headless mode — dialogs return empty/cancelled results",
})
}
fn execute_action(
&mut self,
action: &str,
params: &serde_json::Value,
) -> Result<serde_json::Value, String> {
match action {
"open_file" => {
let title = params["title"].as_str().unwrap_or("Open");
let multiple = params["multiple"].as_bool().unwrap_or(false);
let directory = params["directory"].as_bool().unwrap_or(false);
let config = OpenFileDialog::new(title)
.with_multiple(multiple)
.with_directory(directory);
let result = self.open_file(&config);
let paths: Vec<String> = result.iter().map(|p| p.display().to_string()).collect();
Ok(serde_json::json!({ "paths": paths }))
}
"save_file" => {
let title = params["title"].as_str().unwrap_or("Save");
let name = params["default_name"].as_str().unwrap_or("");
let config = SaveFileDialog::new(title).with_name(name);
let result = self.save_file(&config);
Ok(serde_json::json!({ "path": result.map(|p| p.display().to_string()) }))
}
"message_box" => {
let title = params["title"].as_str().ok_or("missing title")?;
let message = params["message"].as_str().ok_or("missing message")?;
let mut config = MessageBox::new(title, message);
if params["show_cancel"].as_bool().unwrap_or(false) {
config = config.with_cancel();
}
let result = self.message_box(&config);
Ok(serde_json::json!({ "result": format!("{:?}", result) }))
}
_ => Err(format!("Unknown action: {action}")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn null_dialog_returns_empty() {
let backend = NullDialogBackend;
let dialog = OpenFileDialog::new("Open").with_multiple(true);
assert!(backend.open_file(&dialog).is_empty());
let save = SaveFileDialog::new("Save").with_name("output.txt");
assert!(backend.save_file(&save).is_none());
let msg = MessageBox::new("Alert", "Hello").with_cancel();
assert_eq!(backend.message_box(&msg), MessageBoxResult::Ok);
}
}