1use anyhow::Context;
2use serde::{Deserialize, Serialize};
3use std::collections::HashSet;
4use std::path::PathBuf;
5
6fn is_false(v: &bool) -> bool {
8 !*v
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
14#[serde(rename_all = "camelCase")]
15pub struct Settings {
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub default_provider: Option<String>,
18
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub default_model: Option<String>,
21
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub default_thinking_level: Option<String>,
24
25 #[serde(default, skip_serializing_if = "Vec::is_empty")]
26 pub tools: Vec<String>,
27
28 #[serde(default, skip_serializing_if = "Vec::is_empty")]
29 pub exclude_tools: Vec<String>,
30
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub theme: Option<String>,
33
34 #[serde(default, skip_serializing_if = "is_false")]
35 pub verbose: bool,
36
37 #[serde(
39 default,
40 skip_serializing_if = "Option::is_none",
41 rename = "hideThinkingBlock"
42 )]
43 pub hide_thinking: Option<bool>,
44
45 #[serde(
47 default,
48 skip_serializing_if = "Option::is_none",
49 rename = "collapseToolOutput"
50 )]
51 pub collapse_tool_output: Option<bool>,
52
53 #[serde(
55 default,
56 skip_serializing_if = "Option::is_none",
57 rename = "enabledModels"
58 )]
59 pub enabled_models: Option<Vec<String>>,
60
61 #[serde(
63 default,
64 skip_serializing_if = "Option::is_none",
65 rename = "autoCompact"
66 )]
67 pub auto_compact: Option<bool>,
68
69 #[serde(
71 default,
72 skip_serializing_if = "Option::is_none",
73 rename = "compactReserveTokens"
74 )]
75 pub compact_reserve_tokens: Option<u64>,
76
77 #[serde(
79 default,
80 skip_serializing_if = "Option::is_none",
81 rename = "compactKeepRecentTokens"
82 )]
83 pub compact_keep_recent_tokens: Option<u64>,
84
85 #[serde(skip)]
89 pub(crate) modified_fields: HashSet<String>,
90}
91
92impl Settings {
93 pub fn set_hide_thinking(&mut self, value: Option<bool>) {
97 self.hide_thinking = value;
98 self.modified_fields.insert("hideThinkingBlock".into());
99 }
100
101 pub fn set_collapse_tool_output(&mut self, value: Option<bool>) {
103 self.collapse_tool_output = value;
104 self.modified_fields.insert("collapseToolOutput".into());
105 }
106
107 pub fn set_default_thinking_level(&mut self, value: Option<String>) {
109 self.default_thinking_level = value;
110 self.modified_fields.insert("defaultThinkingLevel".into());
111 }
112
113 pub fn set_enabled_models(&mut self, value: Option<Vec<String>>) {
115 self.enabled_models = value;
116 self.modified_fields.insert("enabledModels".into());
117 }
118
119 pub fn set_auto_compact(&mut self, value: Option<bool>) {
121 self.auto_compact = value;
122 self.modified_fields.insert("autoCompact".into());
123 }
124
125 pub fn set_compact_reserve_tokens(&mut self, value: Option<u64>) {
127 self.compact_reserve_tokens = value;
128 self.modified_fields.insert("compactReserveTokens".into());
129 }
130
131 pub fn set_compact_keep_recent_tokens(&mut self, value: Option<u64>) {
133 self.compact_keep_recent_tokens = value;
134 self.modified_fields
135 .insert("compactKeepRecentTokens".into());
136 }
137
138 #[doc(hidden)]
141 pub fn mark_modified(&mut self, field: &str) {
142 self.modified_fields.insert(field.to_string());
143 }
144
145 pub fn load(cwd: &std::path::Path) -> anyhow::Result<Self> {
149 let global_path = Self::global_path()?;
150 Self::load_from(global_path, cwd)
151 }
152
153 pub fn load_from(
155 global_path: std::path::PathBuf,
156 cwd: &std::path::Path,
157 ) -> anyhow::Result<Self> {
158 let global = Self::load_file(&global_path)?;
159 let project = Self::load_file(&cwd.join(".rab").join("settings.json")).unwrap_or_default();
160 Ok(Self::merge(global, project))
161 }
162
163 fn global_path() -> anyhow::Result<PathBuf> {
164 let dir = directories::BaseDirs::new().context("Could not determine home directory")?;
165 Ok(dir
166 .home_dir()
167 .join(".rab")
168 .join("agent")
169 .join("settings.json"))
170 }
171
172 fn load_file(path: &std::path::Path) -> anyhow::Result<Settings> {
173 if !path.exists() {
174 return Ok(Settings::default());
175 }
176 let content = read_file_with_shared_lock(path)?;
178 serde_json::from_str(&content)
179 .with_context(|| format!("Failed to parse {}", path.display()))
180 }
181
182 fn merge(global: Settings, project: Settings) -> Self {
184 Self {
185 default_provider: project.default_provider.or(global.default_provider),
186 default_model: project.default_model.or(global.default_model),
187 default_thinking_level: project
188 .default_thinking_level
189 .or(global.default_thinking_level),
190 tools: if project.tools.is_empty() {
191 global.tools
192 } else {
193 project.tools
194 },
195 exclude_tools: if project.exclude_tools.is_empty() {
196 global.exclude_tools
197 } else {
198 project.exclude_tools
199 },
200 theme: project.theme.or(global.theme),
201 verbose: project.verbose || global.verbose,
202 hide_thinking: project.hide_thinking.or(global.hide_thinking),
203 collapse_tool_output: project.collapse_tool_output.or(global.collapse_tool_output),
204 enabled_models: project.enabled_models.or(global.enabled_models),
205 auto_compact: project.auto_compact.or(global.auto_compact),
206 compact_reserve_tokens: project
207 .compact_reserve_tokens
208 .or(global.compact_reserve_tokens),
209 compact_keep_recent_tokens: project
210 .compact_keep_recent_tokens
211 .or(global.compact_keep_recent_tokens),
212 modified_fields: HashSet::new(),
213 }
214 }
215
216 pub fn save(&mut self) -> anyhow::Result<()> {
231 if self.modified_fields.is_empty() {
232 return Ok(());
233 }
234 let path = Self::global_path()?;
235 self.save_to(path)
236 }
237
238 pub fn save_to(&mut self, path: std::path::PathBuf) -> anyhow::Result<()> {
241 if self.modified_fields.is_empty() {
242 return Ok(());
243 }
244
245 if let Some(parent) = path.parent() {
246 std::fs::create_dir_all(parent)?;
247 }
248
249 let self_value = serde_json::to_value(&*self)
250 .with_context(|| format!("Failed to serialize settings to {}", path.display()))?;
251 let content = compute_merged_content(&path, &self_value, &self.modified_fields)?;
252 atomic_write_with_lock(&path, &content)?;
253
254 self.modified_fields.clear();
256 Ok(())
257 }
258
259 pub fn reload(&mut self, cwd: &std::path::Path) -> anyhow::Result<()> {
262 let global_path = Self::global_path()?;
263 let global = Self::load_file(&global_path)?;
264 let project = Self::load_file(&cwd.join(".rab").join("settings.json")).unwrap_or_default();
265 let merged = Self::merge(global, project);
266 self.default_provider = merged.default_provider;
268 self.default_model = merged.default_model;
269 self.default_thinking_level = merged.default_thinking_level;
270 self.tools = merged.tools;
271 self.exclude_tools = merged.exclude_tools;
272 self.theme = merged.theme;
273 self.verbose = merged.verbose;
274 self.hide_thinking = merged.hide_thinking;
275 self.collapse_tool_output = merged.collapse_tool_output;
276 self.enabled_models = merged.enabled_models;
277 self.auto_compact = merged.auto_compact;
278 self.compact_reserve_tokens = merged.compact_reserve_tokens;
279 self.compact_keep_recent_tokens = merged.compact_keep_recent_tokens;
280 self.modified_fields.clear();
281 Ok(())
282 }
283
284 pub fn model(&self) -> &str {
286 self.default_model.as_deref().unwrap_or("deepseek-v4-flash")
287 }
288}
289
290fn read_file_with_shared_lock(path: &std::path::Path) -> anyhow::Result<String> {
295 let lock_path = path.with_extension("json.lock");
296 if let Ok(lock_file) = std::fs::OpenOptions::new()
297 .create(true)
298 .truncate(false)
299 .read(true)
300 .write(true)
301 .open(&lock_path)
302 {
303 #[cfg(unix)]
304 {
305 use std::os::unix::io::AsRawFd;
306 unsafe {
307 libc::flock(lock_file.as_raw_fd(), libc::LOCK_SH);
308 }
309 }
310 let content = std::fs::read_to_string(path)
311 .with_context(|| format!("Failed to read {}", path.display()))?;
312 #[cfg(unix)]
313 {
314 use std::os::unix::io::AsRawFd;
315 unsafe {
316 libc::flock(lock_file.as_raw_fd(), libc::LOCK_UN);
317 }
318 }
319 Ok(content)
320 } else {
321 std::fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display()))
323 }
324}
325
326fn compute_merged_content(
329 path: &std::path::Path,
330 self_value: &serde_json::Value,
331 modified_fields: &HashSet<String>,
332) -> anyhow::Result<String> {
333 let mut current: serde_json::Value = if path.exists() {
334 let content = std::fs::read_to_string(path)
335 .with_context(|| format!("Failed to read {}", path.display()))?;
336 serde_json::from_str(&content).unwrap_or(serde_json::Value::Object(serde_json::Map::new()))
337 } else {
338 serde_json::Value::Object(serde_json::Map::new())
339 };
340
341 if let (Some(current_obj), Some(self_obj)) = (current.as_object_mut(), self_value.as_object()) {
342 for key in modified_fields {
343 if let Some(value) = self_obj.get(key) {
344 current_obj.insert(key.clone(), value.clone());
345 } else {
346 current_obj.remove(key);
347 }
348 }
349 }
350
351 serde_json::to_string_pretty(¤t)
352 .with_context(|| format!("Failed to serialize settings to {}", path.display()))
353}
354
355fn atomic_write_with_lock(path: &std::path::Path, content: &str) -> anyhow::Result<()> {
357 if let Some(parent) = path.parent() {
358 std::fs::create_dir_all(parent)?;
359 }
360
361 let lock_path = path.with_extension("json.lock");
363 let lock_file = std::fs::OpenOptions::new()
364 .create(true)
365 .truncate(false)
366 .read(true)
367 .write(true)
368 .open(&lock_path)
369 .with_context(|| format!("Failed to open lock file {}", lock_path.display()))?;
370
371 #[cfg(unix)]
372 {
373 use std::os::unix::io::AsRawFd;
374 if unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_EX) } != 0 {
375 let err = std::io::Error::last_os_error();
376 anyhow::bail!("Failed to lock {}: {}", lock_path.display(), err);
377 }
378 }
379
380 let tmp_path = path.with_extension("json.tmp");
382 std::fs::write(&tmp_path, content)
383 .with_context(|| format!("Failed to write {}", tmp_path.display()))?;
384 std::fs::rename(&tmp_path, path).with_context(|| {
385 format!(
386 "Failed to rename {} to {}",
387 tmp_path.display(),
388 path.display()
389 )
390 })?;
391
392 if let Some(parent) = path.parent()
394 && let Ok(f) = std::fs::File::open(parent)
395 {
396 let _ = f.sync_all();
397 }
398
399 #[cfg(unix)]
401 {
402 use std::os::unix::io::AsRawFd;
403 unsafe {
404 libc::flock(lock_file.as_raw_fd(), libc::LOCK_UN);
405 }
406 }
407
408 Ok(())
409}
410
411#[cfg(test)]
414mod tests {
415 use super::*;
416 use std::fs;
417
418 fn tmp_path(name: &str) -> PathBuf {
420 std::env::temp_dir().join(format!("rab_settings_test_{}", name))
421 }
422
423 fn cleanup(path: &PathBuf) {
425 let _ = fs::remove_file(path);
426 let _ = fs::remove_file(path.with_extension("json.lock"));
427 let _ = fs::remove_file(path.with_extension("json.tmp"));
428 }
429
430 #[test]
431 fn test_save_and_load_roundtrip() {
432 let path = tmp_path("roundtrip.json");
433 cleanup(&path);
434
435 let mut settings = Settings::default();
436 settings.set_default_thinking_level(Some("high".into()));
437 assert_eq!(settings.modified_fields.len(), 1);
438 assert!(settings.modified_fields.contains("defaultThinkingLevel"));
439 settings.save_to(path.clone()).unwrap();
440 assert!(
441 settings.modified_fields.is_empty(),
442 "modified_fields should be cleared after save"
443 );
444
445 let content = fs::read_to_string(&path).unwrap();
446 let json: serde_json::Value = serde_json::from_str(&content).unwrap();
447 assert_eq!(json["defaultThinkingLevel"], "high");
448
449 let loaded = Settings::load_file(&path).unwrap();
450 assert_eq!(loaded.default_thinking_level.as_deref(), Some("high"));
451
452 cleanup(&path);
453 }
454
455 #[test]
456 fn test_save_multiple_fields_then_load() {
457 let path = tmp_path("multi.json");
458 cleanup(&path);
459
460 let mut settings = Settings::default();
461 settings.set_hide_thinking(Some(true));
462 settings.set_collapse_tool_output(Some(false));
463 settings.set_default_thinking_level(Some("medium".into()));
464 assert_eq!(settings.modified_fields.len(), 3);
465 settings.save_to(path.clone()).unwrap();
466
467 let loaded = Settings::load_file(&path).unwrap();
468 assert_eq!(loaded.hide_thinking, Some(true));
469 assert_eq!(loaded.collapse_tool_output, Some(false));
470 assert_eq!(loaded.default_thinking_level.as_deref(), Some("medium"));
471
472 cleanup(&path);
473 }
474
475 #[test]
476 fn test_incremental_save_preserves_existing_fields() {
477 let path = tmp_path("incremental.json");
478 cleanup(&path);
479
480 let mut s = Settings::default();
481 s.set_hide_thinking(Some(false));
482 s.save_to(path.clone()).unwrap();
483
484 let mut s2 = Settings::load_file(&path).unwrap();
485 assert_eq!(s2.hide_thinking, Some(false));
486 s2.set_default_thinking_level(Some("low".into()));
487 s2.save_to(path.clone()).unwrap();
488
489 let content = fs::read_to_string(&path).unwrap();
490 let json: serde_json::Value = serde_json::from_str(&content).unwrap();
491 assert_eq!(json["hideThinkingBlock"], false);
492 assert_eq!(json["defaultThinkingLevel"], "low");
493
494 let loaded = Settings::load_file(&path).unwrap();
495 assert_eq!(loaded.hide_thinking, Some(false));
496 assert_eq!(loaded.default_thinking_level.as_deref(), Some("low"));
497
498 cleanup(&path);
499 }
500
501 #[test]
502 fn test_unset_field_removed_from_file() {
503 let path = tmp_path("unset.json");
504 cleanup(&path);
505
506 let mut s = Settings::default();
507 s.set_default_thinking_level(Some("high".into()));
508 s.save_to(path.clone()).unwrap();
509
510 let mut s2 = Settings::load_file(&path).unwrap();
511 s2.set_default_thinking_level(None);
512 s2.save_to(path.clone()).unwrap();
513
514 let content = fs::read_to_string(&path).unwrap();
515 let json: serde_json::Value = serde_json::from_str(&content).unwrap();
516 assert!(
517 !json
518 .as_object()
519 .unwrap()
520 .contains_key("defaultThinkingLevel"),
521 "Field should be removed when set to None"
522 );
523
524 let loaded = Settings::load_file(&path).unwrap();
525 assert!(loaded.default_thinking_level.is_none());
526
527 cleanup(&path);
528 }
529
530 #[test]
531 fn test_hide_thinking_roundtrip() {
532 let path = tmp_path("hide.json");
533 cleanup(&path);
534
535 let mut s = Settings::default();
536 s.set_hide_thinking(Some(false));
537 s.save_to(path.clone()).unwrap();
538
539 let loaded = Settings::load_file(&path).unwrap();
540 assert_eq!(loaded.hide_thinking, Some(false));
541
542 let mut s2 = Settings::load_file(&path).unwrap();
543 s2.set_hide_thinking(Some(true));
544 s2.save_to(path.clone()).unwrap();
545
546 let loaded2 = Settings::load_file(&path).unwrap();
547 assert_eq!(loaded2.hide_thinking, Some(true));
548
549 cleanup(&path);
550 }
551
552 #[test]
553 fn test_merge_global_and_project() {
554 let mut global = Settings::default();
555 global.hide_thinking = Some(true);
556 global.default_thinking_level = Some("high".into());
557
558 let mut project = Settings::default();
559 project.hide_thinking = Some(false);
560
561 let merged = Settings::merge(global, project);
562 assert_eq!(merged.hide_thinking, Some(false));
563 assert_eq!(merged.default_thinking_level.as_deref(), Some("high"));
564 assert!(merged.modified_fields.is_empty());
565 }
566
567 #[test]
568 fn test_save_only_modified_fields() {
569 let path = tmp_path("modified_only.json");
570 cleanup(&path);
571
572 let initial = serde_json::json!({
573 "theme": "dark",
574 "defaultModel": "claude-sonnet",
575 "hideThinkingBlock": true
576 });
577 fs::write(&path, serde_json::to_string_pretty(&initial).unwrap()).unwrap();
578
579 let mut s = Settings::load_file(&path).unwrap();
580 assert_eq!(s.hide_thinking, Some(true));
581 assert_eq!(s.theme.as_deref(), Some("dark"));
582 assert_eq!(s.model(), "claude-sonnet");
583
584 s.set_default_thinking_level(Some("low".into()));
585 s.save_to(path.clone()).unwrap();
586
587 let content = fs::read_to_string(&path).unwrap();
588 let json: serde_json::Value = serde_json::from_str(&content).unwrap();
589 assert_eq!(
590 json["hideThinkingBlock"], true,
591 "hideThinkingBlock preserved"
592 );
593 assert_eq!(
594 json["defaultThinkingLevel"], "low",
595 "defaultThinkingLevel added"
596 );
597
598 cleanup(&path);
599 }
600
601 #[test]
602 fn test_clear_modified_fields_only_after_write() {
603 let path = tmp_path("clear_modified.json");
604 cleanup(&path);
605
606 let mut s = Settings::default();
607 s.set_default_thinking_level(Some("xhigh".into()));
608 s.set_hide_thinking(Some(false));
609 s.save_to(path.clone()).unwrap();
610 assert!(s.modified_fields.is_empty());
611
612 s.set_hide_thinking(Some(true));
613 assert_eq!(s.modified_fields.len(), 1);
614 assert!(s.modified_fields.contains("hideThinkingBlock"));
615 s.save_to(path.clone()).unwrap();
616 assert!(s.modified_fields.is_empty());
617
618 cleanup(&path);
619 }
620
621 #[test]
624 fn test_lock_file_created_and_lock_released() {
625 let path = tmp_path("lock_test.json");
626 cleanup(&path);
627
628 let mut s = Settings::default();
629 s.set_default_thinking_level(Some("high".into()));
630 s.save_to(path.clone()).unwrap();
631
632 let lock_path = path.with_extension("json.lock");
633 assert!(lock_path.exists(), "Lock file should exist after write");
634
635 #[cfg(unix)]
637 {
638 use std::os::unix::io::AsRawFd;
639 let lock_file = std::fs::OpenOptions::new()
640 .read(true)
641 .write(true)
642 .open(&lock_path)
643 .unwrap();
644 let result =
645 unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
646 assert_eq!(result, 0, "Lock must be released after write");
647 unsafe {
648 libc::flock(lock_file.as_raw_fd(), libc::LOCK_UN);
649 }
650 }
651
652 cleanup(&path);
653 }
654
655 #[test]
664 fn test_full_persistence_cycle() {
665 let path = tmp_path("full_cycle.json");
666 cleanup(&path);
667
668 {
670 let mut settings = Settings::default();
671 settings.set_hide_thinking(Some(false));
672 settings.set_default_thinking_level(Some("xhigh".into()));
673 settings.save_to(path.clone()).unwrap();
674 }
675
676 {
678 let loaded = Settings::load_file(&path).unwrap();
679 assert_eq!(loaded.hide_thinking, Some(false), "hide_thinking persists");
680 assert_eq!(
681 loaded.default_thinking_level.as_deref(),
682 Some("xhigh"),
683 "thinking level persists"
684 );
685 }
686
687 {
689 let mut settings = Settings::load_file(&path).unwrap();
690 settings.set_hide_thinking(Some(true));
691 settings.set_default_thinking_level(Some("low".into()));
692 settings.save_to(path.clone()).unwrap();
693 }
694
695 {
697 let loaded = Settings::load_file(&path).unwrap();
698 assert_eq!(loaded.hide_thinking, Some(true), "hide_thinking updated");
699 assert_eq!(
700 loaded.default_thinking_level.as_deref(),
701 Some("low"),
702 "thinking level updated"
703 );
704 }
705
706 {
708 let lock_path = path.with_extension("json.lock");
709 assert!(lock_path.exists(), "Lock file should exist");
710 #[cfg(unix)]
711 {
712 use std::os::unix::io::AsRawFd;
713 let lock_file = std::fs::OpenOptions::new()
714 .read(true)
715 .write(true)
716 .open(&lock_path)
717 .unwrap();
718 let result =
719 unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
720 assert_eq!(result, 0, "Lock must be released after save");
721 unsafe {
722 libc::flock(lock_file.as_raw_fd(), libc::LOCK_UN);
723 }
724 }
725 }
726
727 cleanup(&path);
728 }
729
730 #[test]
733 fn test_concurrent_writes_to_same_file() {
734 let path = tmp_path("concurrent.json");
735 cleanup(&path);
736
737 let mut s1 = Settings::default();
739 s1.set_hide_thinking(Some(true));
740 s1.set_default_thinking_level(Some("xhigh".into()));
741
742 let mut s2 = Settings::default();
743 s2.set_hide_thinking(Some(false));
744 s2.set_default_thinking_level(Some("low".into()));
745
746 s1.save_to(path.clone()).unwrap();
748 s2.save_to(path.clone()).unwrap();
749
750 let content = fs::read_to_string(&path).unwrap();
752 let json: serde_json::Value = serde_json::from_str(&content).unwrap();
753 assert!(json.is_object(), "File must be valid JSON, not corrupted");
754
755 assert_eq!(json["hideThinkingBlock"], false, "s2's hide_thinking");
757 assert_eq!(json["defaultThinkingLevel"], "low", "s2's thinking level");
758
759 cleanup(&path);
760 }
761
762 #[test]
764 fn test_lock_file_cleanup() {
765 let path = tmp_path("lock_cleanup.json");
766 cleanup(&path);
767
768 let mut s = Settings::default();
769 s.set_hide_thinking(Some(true));
770 s.save_to(path.clone()).unwrap();
771
772 let lock_path = path.with_extension("json.lock");
773 assert!(lock_path.exists(), "Lock file should exist");
774
775 let tmp_path = path.with_extension("json.tmp");
777 assert!(!tmp_path.exists(), "Temp file should be removed");
778
779 cleanup(&path);
780 }
781
782 #[test]
784 fn test_reload_preserves_unmodified() {
785 let path = tmp_path("reload_preserve.json");
786 cleanup(&path);
787
788 let initial = serde_json::json!({
790 "theme": "solarized",
791 "defaultModel": "deepseek-v4-pro",
792 "hideThinkingBlock": true
793 });
794 fs::write(&path, serde_json::to_string_pretty(&initial).unwrap()).unwrap();
795
796 let mut s = Settings::load_file(&path).unwrap();
798 s.set_default_thinking_level(Some("high".into()));
799 s.save_to(path.clone()).unwrap();
800
801 let content = fs::read_to_string(&path).unwrap();
803 let json: serde_json::Value = serde_json::from_str(&content).unwrap();
804 assert_eq!(json["theme"], "solarized", "theme preserved");
805 assert_eq!(json["defaultModel"], "deepseek-v4-pro", "model preserved");
806 assert_eq!(
807 json["hideThinkingBlock"], true,
808 "hideThinkingBlock preserved"
809 );
810 assert_eq!(json["defaultThinkingLevel"], "high", "thinking level added");
811
812 cleanup(&path);
813 }
814}