feature/tauri-v2
rs 459 lines 13.4 KB
Raw
1 #![cfg_attr(
2 all(not(debug_assertions), target_os = "windows"),
3 windows_subsystem = "windows"
4 )]
5
6 #[cfg(debug_assertions)]
7 const LOG_TARGETS: [tauri_plugin_log::TargetKind; 2] = [
8 tauri_plugin_log::TargetKind::Stdout,
9 tauri_plugin_log::TargetKind::Webview,
10 ];
11
12 #[cfg(not(debug_assertions))]
13 const LOG_TARGETS: [Target; 2] = [Target::Stdout, Target::LogDir];
14
15 use crate::encoding::convert_to_u16;
16 use encoding::BOM;
17 use log::{error, info, Level};
18 use std::collections::{self, HashMap};
19 use std::fs::File;
20 use std::io::Write;
21 use std::path::{Path, PathBuf};
22 use std::process::Command;
23 use std::{env, fs};
24 use tauri::plugin::TauriPlugin;
25 use tauri::{App, Manager, Wry};
26 use tauri_plugin_log::{RotationStrategy, Target};
27
28 mod encoding;
29
30 #[tauri::command]
31 fn open_in_explorer(path: &str) {
32 // FOR OTHER OS REFER - https://doc.rust-lang.org/std/env/consts/constant.OS.html
33 // REF - https://github.com/tauri-apps/tauri/issues/4062
34 // TARGET - WINDOWS
35 if env::consts::OS == "windows" {
36 Command::new("explorer")
37 .args(["/select,", path])
38 .spawn()
39 .unwrap();
40 }
41 }
42
43 #[tauri::command]
44 fn open_in_default(path: &str) {
45 if env::consts::OS == "windows" {
46 Command::new("powershell")
47 .args(["&", path])
48 .spawn()
49 .unwrap();
50 }
51 }
52
53 #[tauri::command]
54 fn open_terminal(path: &str) {
55 if env::consts::OS == "windows" {
56 // programs for windows: [cmd, powershell, wt]
57 // programs for ubuntu: [gnome-terminal]
58 // .args(["/C", "start", "wt"])
59 Command::new("cmd")
60 .args(["/C", "wt", "-d", path])
61 .spawn()
62 .unwrap();
63 } else {
64 Command::new("gnome-terminal")
65 .arg(format!("--working-directory={}", path).as_str())
66 .spawn()
67 .unwrap();
68 }
69 }
70
71 #[tauri::command]
72 fn is_file(path: &str) -> bool {
73 match fs::metadata(path) {
74 Ok(r) => r.is_file(),
75 Err(e) => {
76 error!("{}", e);
77 false
78 }
79 }
80 }
81
82 #[tauri::command]
83 fn is_folder(path: &str) -> bool {
84 match fs::metadata(path) {
85 Ok(r) => r.is_dir(),
86 Err(e) => {
87 error!("{}", e);
88 false
89 }
90 }
91 }
92
93 #[tauri::command]
94 fn attempt_file_access(app_handle: tauri::AppHandle, p: &str) {
95 // app_handle.fs_scope().allow_directory(p, true).unwrap();
96 }
97
98 #[tauri::command]
99 fn delete_file(path: &str, perm: bool) {
100 if perm {
101 if is_file(path) {
102 match fs::remove_file(path) {
103 Ok(_) => {
104 info!("{:?} sucessfully deleted.", PathBuf::from(path).file_name());
105 }
106 Err(err) => error!("Cannot remove {}. Error: {}.", path, err),
107 }
108 } else {
109 match fs::remove_dir_all(path) {
110 Ok(_) => info!("Path {} deleted sucessfully.", path),
111 Err(err) => error!("Cannot remove {}. Error: {}.", path, err),
112 }
113 }
114 } else {
115 match trash::delete(path) {
116 Ok(_) => {
117 info!(
118 "{:?} sucessfully moved to trash.",
119 PathBuf::from(path).file_name()
120 );
121 }
122 Err(err) => error!("Cannot remove {}. Error: {}.", path, err),
123 }
124 }
125 }
126
127 #[derive(Hash, Eq, PartialEq, Debug, serde::Serialize)]
128 struct FileData {
129 text: String,
130 encoding: String,
131 extension: String,
132 bom: bool,
133 spaces: usize,
134 }
135
136 #[tauri::command]
137 fn read_file(path: &str) -> FileData {
138 info!("Attempting to read file in {}.", path);
139
140 let mut bytes: Vec<u8> = vec![];
141 match fs::read(path) {
142 Ok(b) => {
143 bytes = b;
144 }
145 Err(err) => error!("Cannot read {}. Error: {}", path, err),
146 }
147
148 let ext = match Path::new(path).extension() {
149 Some(v) => v.to_str().unwrap(),
150 None => {
151 error!("No extension found for {}", path);
152 ""
153 }
154 };
155
156 let file_data: FileData;
157
158 // encode based on bom if present otherwise just default to utf8
159 if let Some(data) = encoding_rs::Encoding::for_bom(&bytes) {
160 let (text, encoding, _) = data.0.decode(&bytes);
161 let t = text.to_string();
162 let x = t.as_str();
163 let lines: Vec<&str> = x.lines().collect();
164 let spaces = detect_indent(&lines);
165 let space_count = match spaces {
166 Some(capture) => capture,
167 None => 0,
168 };
169
170 file_data = FileData {
171 text: text.to_string(),
172 encoding: encoding.name().to_string(),
173 extension: ext.to_string(),
174 bom: true,
175 spaces: space_count,
176 };
177 info!("File BOM found. Encoding with {}...", encoding.name());
178 } else {
179 let (text, encoding, _) = encoding_rs::UTF_8.decode(&bytes);
180 let t = text.to_string();
181 let x = t.as_str();
182 let lines: Vec<&str> = x.lines().collect();
183 let spaces = detect_indent(&lines);
184 let space_count = match spaces {
185 Some(capture) => capture,
186 None => 0,
187 };
188
189 file_data = FileData {
190 text: text.to_string(),
191 encoding: encoding.name().to_string(),
192 extension: ext.to_string(),
193 bom: false,
194 spaces: space_count,
195 };
196 info!(
197 "No file BOM found. Defaulting to {} encoding...",
198 encoding.name()
199 );
200 }
201 file_data
202 }
203
204 #[tauri::command]
205 fn write_file(path: &str, content: &str, enc: &str, has_bom: bool) {
206 info!("Attempting to write file to {}.", path);
207
208 let mut output = Vec::new();
209 if let Some(data) = encoding_rs::Encoding::for_label(enc.as_bytes()) {
210 let (bytes, _, _) = data.encode(content);
211 let mut c_bytes = bytes.to_vec();
212 let mut bom: Vec<u8> = Vec::new();
213 if has_bom {
214 info!("Encoding file to {} encoding...", enc);
215 if enc == "UTF-8" {
216 bom = b"\xEF\xBB\xBF".to_vec();
217 } else if enc == "UTF-16BE" {
218 c_bytes = convert_to_u16(content, BOM::BigEndian);
219 } else if enc == "UTF-16LE" {
220 c_bytes = convert_to_u16(content, BOM::LittleEndian);
221 }
222 }
223 output = [bom.as_slice(), c_bytes.as_slice()].concat();
224 }
225
226 if !PathBuf::from(path).exists() {
227 info!("{} not found. Creating new file...", path);
228 }
229
230 let file = File::create(path);
231 match file {
232 Ok(mut f) => {
233 f.write_all(&output).unwrap();
234 }
235 Err(err) => error!("Cannot write to {}. Error: {}", path, err),
236 }
237 }
238
239 #[tauri::command]
240 fn is_supported(path: &str) -> bool {
241 match infer::get_from_path(path) {
242 Ok(Some(info)) => {
243 let mut supported = false;
244 if info.mime_type().contains("text") {
245 supported = true;
246 } else {
247 info!("File type detected: {}. Must be binary.", info.mime_type());
248 }
249 supported
250 }
251 Ok(None) => true,
252 Err(e) => {
253 error!("{}", e);
254 false
255 }
256 }
257 }
258
259 fn detect_indent(lines: &[&str]) -> Option<usize> {
260 let mut indents: collections::HashMap<usize, usize> = collections::HashMap::new(); // # spaces indent -> # times seen
261 let mut last = 0; // # leading spaces in the last line we saw
262
263 for &text in lines.iter() {
264 let width = text.find(|c: char| c != ' ').unwrap_or_else(|| text.len());
265
266 let indent = (width as isize - last as isize).abs() as usize;
267 if indent > 1 {
268 *indents.entry(indent).or_insert(0) += 1;
269 }
270 last = width;
271 }
272
273 // find most frequent non-zero width difference
274 let mut max = 0;
275 let mut indent = None;
276 for (&width, &tally) in &indents {
277 if tally > max {
278 max = tally;
279 indent = Some(width);
280 }
281 }
282
283 indent
284 }
285
286 fn configure_log() -> TauriPlugin<Wry> {
287 tauri_plugin_log::Builder::default()
288 .format(move |out, message, record| {
289 let format = time::format_description::parse(
290 "[[[year]-[month]-[day]][[[hour]:[minute]:[second]]",
291 )
292 .unwrap();
293 let file_info = record
294 .file()
295 .map(|location| format!("::{}", location.split("\\").last().unwrap().to_owned()))
296 .unwrap_or("".to_string());
297 let line_info = record
298 .line()
299 .map(|line| format!(":{}", line))
300 .unwrap_or("".to_string());
301 out.finish(format_args!(
302 "{}[{}][{}{}{}] {}",
303 time::OffsetDateTime::now_utc().format(&format).unwrap(),
304 record.level(),
305 record.target(),
306 file_info,
307 line_info,
308 message
309 ))
310 })
311 // .targets(LOG_TARGETS)
312 .filter(|l| {
313 let mut filter = false;
314 if cfg!(debug_assertions) {
315 filter = !matches!(l.level(), Level::Trace);
316 } else if cfg!(not(debug_assertions)) {
317 filter = !matches!(l.level(), Level::Trace | Level::Debug);
318 }
319 filter
320 })
321 .rotation_strategy(RotationStrategy::KeepAll)
322 .build()
323 }
324
325 fn configure_log_path(app: &mut App) {
326 let app_log_dir = app.path().app_log_dir().unwrap();
327 let old_log_path = app_log_dir.join("svara.log");
328 if !Path::exists(&old_log_path) {
329 return;
330 }
331
332 let format = time::format_description::parse("[year]-[month]-[day]-[hour][minute]").unwrap();
333 let time = time::OffsetDateTime::now_utc().format(&format).unwrap();
334 let log_name = format!("svara_log-{}.log", time);
335
336 // changing the default log name to something more meaningful
337 let new_log_path = app_log_dir.join(log_name);
338 if let Err(e) = fs::rename(&old_log_path, &new_log_path) {
339 error!("Failed to rename log file: {}", e);
340 }
341 }
342
343 fn load_settings(app: &mut App) {
344 info!("Loading default settings:");
345
346 let default_settings = serde_json::json!(
347 {
348 "svara.theme": "Dark",
349 "editor.fontSize": 14,
350 "editor.fontFamily": "monospace",
351 "editor.lineHeight": 1.3,
352 "editor.tabSize": 4,
353 "editor.autosave": false,
354 "svara.showKeybinds": false,
355 "svara.useExternalTerminal": false,
356 "terminal.external.profile": "powershell",
357 "terminal.internal": {
358 "profile": "powershell",
359 "fontSize": "14",
360 "fontFamily": "Cascadia Mono",
361 "lineHeight": "1.2",
362 "cursorStyle": "bar",
363 "fontWeight": "normal",
364 }
365 }
366 );
367
368 let appdata_local = match app.path().local_data_dir() {
369 Ok(path) => path,
370 Err(err) => {
371 error!("Failed to get app local data directory");
372 return;
373 }
374 };
375
376 // Create the directory if it doesn't exist
377 if let Err(e) = fs::create_dir_all(&appdata_local) {
378 error!("Failed to create app data directory: {}", e);
379 return;
380 }
381
382 let settings_path = appdata_local.join("default_settings.json");
383
384 #[cfg(debug_assertions)]
385 if let Err(e) = fs::write(&settings_path, default_settings.to_string()) {
386 error!("Failed to write default settings: {}", e);
387 return;
388 }
389
390 if !settings_path.try_exists().unwrap_or(false) {
391 if let Err(e) = fs::write(&settings_path, default_settings.to_string()) {
392 error!("Failed to create default settings file: {}", e);
393 return;
394 }
395 info!(
396 "Default settings file not found. Created a new default settings file. Path: {:?}",
397 &settings_path
398 );
399 } else {
400 info!("Settings path: {:?}:", settings_path);
401 }
402
403 let mut defaults = HashMap::new();
404 for settings in default_settings.as_object().unwrap() {
405 defaults
406 .entry(settings.0.clone())
407 .or_insert_with(|| settings.1.clone());
408 }
409
410 let mut settings_store = match tauri_plugin_store::StoreBuilder::new(app.handle(), settings_path)
411 .defaults(defaults)
412 .build() {
413 Ok(store) => store,
414 Err(e) => {
415 error!("Failed to create settings store: {}", e);
416 return;
417 }
418 };
419
420 if let Err(e) = settings_store.reload() {
421 error!("Failed to load settings: {}", e);
422 }
423 }
424
425 fn main() {
426 let original = std::panic::take_hook();
427 std::panic::set_hook(Box::new(move |info| {
428 // log panics/crashes
429 original(info);
430 error!("[FATAL]: {:?}", info.to_string());
431 }));
432
433 tauri::Builder::default()
434 .plugin(tauri_plugin_opener::init())
435 .plugin(tauri_plugin_fs::init())
436 .invoke_handler(tauri::generate_handler![
437 open_in_explorer,
438 delete_file,
439 attempt_file_access,
440 is_file,
441 is_folder,
442 read_file,
443 write_file,
444 open_in_default,
445 is_supported,
446 open_terminal
447 ])
448 .plugin(tauri_plugin_fs::init())
449 .plugin(tauri_plugin_store::Builder::default().build())
450 .plugin(configure_log())
451 .plugin(tauri_plugin_pty::init())
452 .setup(|app| {
453 configure_log_path(app);
454 load_settings(app);
455 Ok(())
456 })
457 .run(tauri::generate_context!())
458 .expect("error while running tauri application");
459 }