ravenrs_platform_native/
files.rs1use std::io;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7#[derive(Debug)]
23pub struct SaveDialog {
24 dialog: rfd::FileDialog,
25}
26
27impl SaveDialog {
28 pub fn new(file_name: &str) -> Self {
30 Self { dialog: rfd::FileDialog::new().set_file_name(file_name) }
31 }
32
33 pub fn title(mut self, title: &str) -> Self {
35 self.dialog = self.dialog.set_title(title);
36 self
37 }
38
39 pub fn filter(mut self, name: &str, extensions: &[&str]) -> Self {
41 self.dialog = self.dialog.add_filter(name, extensions);
42 self
43 }
44
45 pub fn folder(mut self, folder: &Path) -> Self {
47 self.dialog = self.dialog.set_directory(folder);
48 self
49 }
50
51 pub fn ask(self) -> Option<PathBuf> {
53 self.dialog.save_file()
54 }
55}
56
57#[derive(Debug, Default)]
72pub struct OpenDialog {
73 dialog: rfd::FileDialog,
74}
75
76impl OpenDialog {
77 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn title(mut self, title: &str) -> Self {
84 self.dialog = self.dialog.set_title(title);
85 self
86 }
87
88 pub fn filter(mut self, name: &str, extensions: &[&str]) -> Self {
90 self.dialog = self.dialog.add_filter(name, extensions);
91 self
92 }
93
94 pub fn folder(mut self, folder: &Path) -> Self {
96 self.dialog = self.dialog.set_directory(folder);
97 self
98 }
99
100 pub fn ask(self) -> Option<PathBuf> {
102 self.dialog.pick_file()
103 }
104}
105
106pub(crate) const MOST_DROPPED_BYTES: u64 = 1024 * 1024 * 1024;
108
109pub(crate) fn read_picked(path: &Path, most_bytes: Option<u64>) -> Option<ravenrs_widgets::PickedFile> {
112 let name = path.file_name()?.to_string_lossy().into_owned();
113 let size = std::fs::metadata(path).ok()?.len();
114 if most_bytes.is_some_and(|most| size > most) {
115 return Some(ravenrs_widgets::PickedFile { name, size, bytes: Vec::new().into() });
116 }
117 match std::fs::read(path) {
118 Ok(bytes) => Some(ravenrs_widgets::PickedFile::new(name, bytes)),
119 Err(e) => {
120 tracing::warn!(error = %e, "could not read the chosen file");
121 None
122 }
123 }
124}
125
126#[derive(Debug, Default)]
128pub(crate) struct SystemFilePicker;
129
130impl ravenrs_widgets::FilePickerProvider for SystemFilePicker {
131 fn pick(
132 &self,
133 request: &ravenrs_widgets::FileRequest,
134 done: Box<dyn FnOnce(Vec<ravenrs_widgets::PickedFile>)>,
135 ) {
136 let mut dialog = rfd::FileDialog::new();
137 if !request.extensions.is_empty() {
138 let kinds: Vec<&str> = request.extensions.iter().map(String::as_str).collect();
139 dialog = dialog.add_filter(kinds.join(", "), &kinds);
140 }
141 let paths = if request.multiple {
142 dialog.pick_files().unwrap_or_default()
143 } else {
144 dialog.pick_file().into_iter().collect()
145 };
146 done(paths.iter().filter_map(|p| read_picked(p, request.most_bytes)).collect());
147 }
148
149 fn save(&self, name: &str, bytes: &[u8]) {
150 let Some(path) = SaveDialog::new(name).ask() else { return };
151 if let Err(e) = std::fs::write(path, bytes) {
152 tracing::warn!(error = %e, "could not save the file");
153 }
154 }
155}
156
157pub fn open_with_system(path: &Path) -> io::Result<()> {
160 let mut command = if cfg!(target_os = "macos") {
161 Command::new("open")
162 } else if cfg!(target_os = "windows") {
163 let mut start = Command::new("cmd");
165 start.args(["/C", "start", ""]);
166 start
167 } else {
168 Command::new("xdg-open")
169 };
170 command.arg(path).spawn().map(|_| ())
171}
172
173pub fn open_url(url: &str) -> io::Result<()> {
179 if !(url.starts_with("https://") || url.starts_with("http://")) {
180 return Err(io::Error::new(io::ErrorKind::InvalidInput, "not a web address"));
181 }
182 if cfg!(target_os = "windows") {
183 Command::new("rundll32").args(["url.dll,FileProtocolHandler", url]).spawn().map(|_| ())
184 } else {
185 open_with_system(Path::new(url))
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn only_web_addresses_are_opened() {
195 for url in ["file:///etc/passwd", "/bin/sh", "--help", "javascript:alert(1)", ""] {
196 assert_eq!(open_url(url).map_err(|e| e.kind()), Err(io::ErrorKind::InvalidInput), "{url}");
197 }
198 }
199}