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