Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:03:51.971Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

settings_file.rs

284 lines · 9.7 KB · rust
1use crate::{settings_content::SettingsContent, settings_store::SettingsStore};
2use collections::HashSet;
3use fs::{Fs, PathEventKind};
4use futures::{StreamExt, channel::mpsc};
5use gpui::{App, BackgroundExecutor, ReadGlobal};
6use std::{path::PathBuf, sync::Arc, time::Duration};
7
8#[cfg(test)]
9mod tests {
10    use super::*;
11    use fs::FakeFs;
12
13    use gpui::TestAppContext;
14    use serde_json::json;
15    use std::path::Path;
16
17    #[gpui::test]
18    async fn test_watch_config_dir_reloads_tracked_file_on_rescan(cx: &mut TestAppContext) {
19        cx.executor().allow_parking();
20
21        let fs = FakeFs::new(cx.background_executor.clone());
22        let config_dir = PathBuf::from("/root/config");
23        let settings_path = PathBuf::from("/root/config/settings.json");
24
25        fs.insert_tree(
26            Path::new("/root"),
27            json!({
28                "config": {
29                    "settings.json": "A"
30                }
31            }),
32        )
33        .await;
34
35        let mut rx = watch_config_dir(
36            &cx.background_executor,
37            fs.clone(),
38            config_dir.clone(),
39            HashSet::from_iter([settings_path.clone()]),
40        );
41
42        assert_eq!(rx.next().await.as_deref(), Some("A"));
43        cx.run_until_parked();
44
45        fs.pause_events();
46        fs.insert_file(&settings_path, b"B".to_vec()).await;
47        fs.clear_buffered_events();
48
49        fs.emit_fs_event(&settings_path, Some(PathEventKind::Rescan));
50        fs.unpause_events_and_flush();
51        assert_eq!(rx.next().await.as_deref(), Some("B"));
52
53        fs.pause_events();
54        fs.insert_file(&settings_path, b"A".to_vec()).await;
55        fs.clear_buffered_events();
56
57        fs.emit_fs_event(&config_dir, Some(PathEventKind::Rescan));
58        fs.unpause_events_and_flush();
59        assert_eq!(rx.next().await.as_deref(), Some("A"));
60    }
61
62    #[gpui::test]
63    async fn test_watch_config_file_reloads_when_parent_dir_is_symlink(cx: &mut TestAppContext) {
64        cx.executor().allow_parking();
65        let fs = FakeFs::new(cx.background_executor.clone());
66        let config_settings_path = PathBuf::from("/root/.config/zed/settings.json");
67        let target_settings_path = PathBuf::from("/root/dotfiles/zed/settings.json");
68
69        fs.insert_tree(
70            Path::new("/root"),
71            json!({
72                ".config": {},
73                "dotfiles": {
74                    "zed": {
75                        "settings.json": "A"
76                    }
77                }
78            }),
79        )
80        .await;
81
82        fs.create_symlink(
83            Path::new("/root/.config/zed"),
84            PathBuf::from("/root/dotfiles/zed"),
85        )
86        .await
87        .unwrap();
88
89        let (mut rx, _task) =
90            watch_config_file(&cx.background_executor, fs.clone(), config_settings_path);
91        assert_eq!(rx.next().await.as_deref(), Some("A"));
92
93        fs.insert_file(&target_settings_path, b"B".to_vec()).await;
94        assert_eq!(rx.next().await.as_deref(), Some("B"));
95    }
96}
97
98pub const EMPTY_THEME_NAME: &str = "empty-theme";
99
100/// Settings for visual tests that use proper fonts instead of Courier.
101/// Uses Helvetica Neue for UI (sans-serif) and Menlo for code (monospace),
102/// which are available on all macOS systems.
103#[cfg(any(test, feature = "test-support"))]
104pub fn visual_test_settings() -> String {
105    let mut value =
106        crate::parse_json_with_comments::<serde_json::Value>(crate::default_settings().as_ref())
107            .unwrap();
108    util::merge_non_null_json_value_into(
109        serde_json::json!({
110            "ui_font_family": ".SystemUIFont",
111            "ui_font_features": {},
112            "ui_font_size": 14,
113            "ui_font_fallback": [],
114            "buffer_font_family": "Menlo",
115            "buffer_font_features": {},
116            "buffer_font_size": 14,
117            "buffer_font_fallbacks": [],
118            "theme": EMPTY_THEME_NAME,
119        }),
120        &mut value,
121    );
122    value.as_object_mut().unwrap().remove("languages");
123    serde_json::to_string(&value).unwrap()
124}
125
126#[cfg(any(test, feature = "test-support"))]
127pub fn test_settings() -> &'static str {
128    static CACHED: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
129        let mut value = crate::parse_json_with_comments::<serde_json::Value>(
130            crate::default_settings().as_ref(),
131        )
132        .unwrap();
133        #[cfg(not(target_os = "windows"))]
134        util::merge_non_null_json_value_into(
135            serde_json::json!({
136                "format_on_save": "on",
137                "ui_font_family": "Courier",
138                "ui_font_features": {},
139                "ui_font_size": 14,
140                "ui_font_fallback": [],
141                "buffer_font_family": "Courier",
142                "buffer_font_features": {},
143                "buffer_font_size": 14,
144                "buffer_font_fallbacks": [],
145                "theme": EMPTY_THEME_NAME,
146            }),
147            &mut value,
148        );
149        #[cfg(target_os = "windows")]
150        util::merge_non_null_json_value_into(
151            serde_json::json!({
152                "format_on_save": "on",
153                "ui_font_family": "Courier New",
154                "ui_font_features": {},
155                "ui_font_size": 14,
156                "ui_font_fallback": [],
157                "buffer_font_family": "Courier New",
158                "buffer_font_features": {},
159                "buffer_font_size": 14,
160                "buffer_font_fallbacks": [],
161                "theme": EMPTY_THEME_NAME,
162            }),
163            &mut value,
164        );
165        value.as_object_mut().unwrap().remove("languages");
166        serde_json::to_string(&value).unwrap()
167    });
168    &CACHED
169}
170
171pub fn watch_config_file(
172    executor: &BackgroundExecutor,
173    fs: Arc<dyn Fs>,
174    path: PathBuf,
175) -> (mpsc::UnboundedReceiver<String>, gpui::Task<()>) {
176    let (tx, rx) = mpsc::unbounded();
177    let task = executor.spawn(async move {
178        let path = fs.canonicalize(&path).await.unwrap_or_else(|_| path);
179        let (events, _) = fs.watch(&path, Duration::from_millis(100)).await;
180        futures::pin_mut!(events);
181
182        let contents = fs.load(&path).await.unwrap_or_default();
183        if tx.unbounded_send(contents).is_err() {
184            return;
185        }
186
187        loop {
188            if events.next().await.is_none() {
189                break;
190            }
191
192            if let Ok(contents) = fs.load(&path).await
193                && tx.unbounded_send(contents).is_err()
194            {
195                break;
196            }
197        }
198    });
199    (rx, task)
200}
201
202pub fn watch_config_dir(
203    executor: &BackgroundExecutor,
204    fs: Arc<dyn Fs>,
205    dir_path: PathBuf,
206    config_paths: HashSet<PathBuf>,
207) -> mpsc::UnboundedReceiver<String> {
208    let (tx, rx) = mpsc::unbounded();
209    executor
210        .spawn(async move {
211            for file_path in &config_paths {
212                if fs.metadata(file_path).await.is_ok_and(|v| v.is_some())
213                    && let Ok(contents) = fs.load(file_path).await
214                    && tx.unbounded_send(contents).is_err()
215                {
216                    return;
217                }
218            }
219
220            let (events, _) = fs.watch(&dir_path, Duration::from_millis(100)).await;
221            futures::pin_mut!(events);
222
223            while let Some(event_batch) = events.next().await {
224                for event in event_batch {
225                    if config_paths.contains(&event.path) {
226                        match event.kind {
227                            Some(PathEventKind::Removed) => {
228                                if tx.unbounded_send(String::new()).is_err() {
229                                    return;
230                                }
231                            }
232                            Some(PathEventKind::Created) | Some(PathEventKind::Changed) => {
233                                if let Ok(contents) = fs.load(&event.path).await
234                                    && tx.unbounded_send(contents).is_err()
235                                {
236                                    return;
237                                }
238                            }
239                            Some(PathEventKind::Rescan) => {
240                                for file_path in &config_paths {
241                                    if let Ok(contents) = fs.load(file_path).await
242                                        && tx.unbounded_send(contents).is_err()
243                                    {
244                                        return;
245                                    }
246                                }
247                            }
248                            _ => {}
249                        }
250                    } else if matches!(event.kind, Some(PathEventKind::Rescan))
251                        && event.path == dir_path
252                    {
253                        for file_path in &config_paths {
254                            if let Ok(contents) = fs.load(file_path).await
255                                && tx.unbounded_send(contents).is_err()
256                            {
257                                return;
258                            }
259                        }
260                    }
261                }
262            }
263        })
264        .detach();
265
266    rx
267}
268
269pub fn update_settings_file(
270    fs: Arc<dyn Fs>,
271    cx: &App,
272    update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
273) {
274    SettingsStore::global(cx).update_settings_file(fs, update)
275}
276
277pub fn update_settings_file_with_completion(
278    fs: Arc<dyn Fs>,
279    cx: &App,
280    update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
281) -> futures::channel::oneshot::Receiver<anyhow::Result<()>> {
282    SettingsStore::global(cx).update_settings_file_with_completion(fs, update)
283}
284
Served at tenant.openagents/omega Member data and write actions are omitted.