Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:49:40.754Z 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

migrate.rs

327 lines · 12.0 KB · rust
1use anyhow::{Context as _, Result};
2use editor::Editor;
3use fs::Fs;
4use gpui::WeakEntity;
5use migrator::{migrate_keymap, migrate_settings};
6use settings::{KeymapFile, Settings, SettingsStore};
7use util::ResultExt;
8use workspace::notifications::NotifyTaskExt;
9
10use std::sync::Arc;
11
12use gpui::{Entity, EventEmitter, Global, Task, TextStyle, TextStyleRefinement};
13use markdown::{Markdown, MarkdownElement, MarkdownStyle};
14use theme_settings::ThemeSettings;
15use ui::prelude::*;
16use workspace::item::ItemHandle;
17use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace};
18
19#[derive(Debug, Copy, Clone, PartialEq)]
20pub enum MigrationType {
21    Keymap,
22    Settings,
23}
24
25pub struct MigrationBanner {
26    workspace: WeakEntity<Workspace>,
27    migration_type: Option<MigrationType>,
28    should_migrate_task: Option<Task<()>>,
29    markdown: Option<Entity<Markdown>>,
30}
31
32pub enum MigrationEvent {
33    ContentChanged {
34        migration_type: MigrationType,
35        migrating_in_memory: bool,
36    },
37}
38
39pub struct MigrationNotification;
40
41impl EventEmitter<MigrationEvent> for MigrationNotification {}
42
43impl MigrationNotification {
44    pub fn try_global(cx: &App) -> Option<Entity<Self>> {
45        cx.try_global::<GlobalMigrationNotification>()
46            .map(|notifier| notifier.0.clone())
47    }
48
49    pub fn set_global(notifier: Entity<Self>, cx: &mut App) {
50        cx.set_global(GlobalMigrationNotification(notifier));
51    }
52}
53
54struct GlobalMigrationNotification(Entity<MigrationNotification>);
55
56impl Global for GlobalMigrationNotification {}
57
58impl MigrationBanner {
59    pub fn new(workspace: WeakEntity<Workspace>, cx: &mut Context<Self>) -> Self {
60        if let Some(notifier) = MigrationNotification::try_global(cx) {
61            cx.subscribe(
62                &notifier,
63                move |migrator_banner, _, event: &MigrationEvent, cx| {
64                    migrator_banner.handle_notification(event, cx);
65                },
66            )
67            .detach();
68        }
69        Self {
70            workspace,
71            migration_type: None,
72            should_migrate_task: None,
73            markdown: None,
74        }
75    }
76
77    fn handle_notification(&mut self, event: &MigrationEvent, cx: &mut Context<Self>) {
78        match event {
79            MigrationEvent::ContentChanged {
80                migration_type,
81                migrating_in_memory,
82            } => {
83                if *migrating_in_memory {
84                    self.migration_type = Some(*migration_type);
85                    self.show(cx);
86                } else {
87                    cx.emit(ToolbarItemEvent::ChangeLocation(
88                        ToolbarItemLocation::Hidden,
89                    ));
90                    self.reset(cx);
91                };
92            }
93        }
94    }
95
96    fn show(&mut self, cx: &mut Context<Self>) {
97        let (file_type, backup_file_name) = match self.migration_type {
98            Some(MigrationType::Keymap) => (
99                "keymap",
100                paths::keymap_backup_file()
101                    .file_name()
102                    .unwrap_or_default()
103                    .to_string_lossy()
104                    .into_owned(),
105            ),
106            Some(MigrationType::Settings) => (
107                "settings",
108                paths::settings_backup_file()
109                    .file_name()
110                    .unwrap_or_default()
111                    .to_string_lossy()
112                    .into_owned(),
113            ),
114            None => return,
115        };
116
117        let migration_text = format!(
118            "Your {} file uses deprecated settings which can be \
119            automatically updated. A backup will be saved to `{}`",
120            file_type, backup_file_name
121        );
122
123        self.markdown = Some(cx.new(|cx| Markdown::new(migration_text.into(), None, None, cx)));
124
125        cx.emit(ToolbarItemEvent::ChangeLocation(
126            ToolbarItemLocation::Secondary,
127        ));
128        cx.notify();
129    }
130
131    fn reset(&mut self, cx: &mut Context<Self>) {
132        self.should_migrate_task.take();
133        self.migration_type.take();
134        self.markdown.take();
135        cx.notify();
136    }
137}
138
139impl EventEmitter<ToolbarItemEvent> for MigrationBanner {}
140
141impl ToolbarItemView for MigrationBanner {
142    fn set_active_pane_item(
143        &mut self,
144        active_pane_item: Option<&dyn ItemHandle>,
145        window: &mut Window,
146        cx: &mut Context<Self>,
147    ) -> ToolbarItemLocation {
148        self.reset(cx);
149
150        let Some(target) = active_pane_item
151            .and_then(|item| item.act_as::<Editor>(cx))
152            .and_then(|editor| editor.update(cx, |editor, cx| editor.target_file_abs_path(cx)))
153        else {
154            return ToolbarItemLocation::Hidden;
155        };
156
157        if &target == paths::keymap_file() {
158            self.migration_type = Some(MigrationType::Keymap);
159            let fs = <dyn Fs>::global(cx);
160            let should_migrate = cx.background_spawn(should_migrate_keymap(fs));
161            self.should_migrate_task = Some(cx.spawn_in(window, async move |this, cx| {
162                if let Ok(true) = should_migrate.await {
163                    this.update(cx, |this, cx| {
164                        this.show(cx);
165                    })
166                    .log_err();
167                }
168            }));
169        } else if &target == paths::settings_file() {
170            self.migration_type = Some(MigrationType::Settings);
171            let fs = <dyn Fs>::global(cx);
172            let should_migrate = cx.background_spawn(should_migrate_settings(fs));
173            self.should_migrate_task = Some(cx.spawn_in(window, async move |this, cx| {
174                if let Ok(true) = should_migrate.await {
175                    this.update(cx, |this, cx| {
176                        this.show(cx);
177                    })
178                    .log_err();
179                }
180            }));
181        }
182
183        ToolbarItemLocation::Hidden
184    }
185}
186
187impl Render for MigrationBanner {
188    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
189        let migration_type = self.migration_type;
190        let settings = ThemeSettings::get_global(cx);
191        let ui_font_family = settings.ui_font.family.clone();
192        let line_height = settings.ui_font_size(cx) * 1.3;
193        h_flex()
194            .py_1()
195            .pl_2()
196            .pr_1()
197            .justify_between()
198            .bg(cx.theme().status().info_background.opacity(0.6))
199            .border_1()
200            .border_color(cx.theme().colors().border_variant)
201            .rounded_sm()
202            .child(
203                h_flex()
204                    .gap_2()
205                    .overflow_hidden()
206                    .child(
207                        Icon::new(IconName::Warning)
208                            .size(IconSize::XSmall)
209                            .color(Color::Warning),
210                    )
211                    .child(
212                        div()
213                            .overflow_hidden()
214                            .text_size(TextSize::Default.rems(cx))
215                            .max_h(2 * line_height)
216                            .when_some(self.markdown.as_ref(), |this, markdown| {
217                                this.child(
218                                    MarkdownElement::new(
219                                        markdown.clone(),
220                                        MarkdownStyle {
221                                            base_text_style: TextStyle {
222                                                color: cx.theme().colors().text,
223                                                font_family: ui_font_family,
224                                                ..Default::default()
225                                            },
226                                            inline_code: TextStyleRefinement {
227                                                background_color: Some(
228                                                    cx.theme().colors().background,
229                                                ),
230                                                ..Default::default()
231                                            },
232                                            ..Default::default()
233                                        },
234                                    )
235                                    .into_any_element(),
236                                )
237                            }),
238                    ),
239            )
240            .child(
241                Button::new("backup-and-migrate", "Backup and Update").on_click({
242                    let workspace = self.workspace.clone();
243                    move |_, window, cx| {
244                        let fs = <dyn Fs>::global(cx);
245                        let task = match migration_type {
246                            Some(MigrationType::Keymap) => {
247                                cx.background_spawn(write_keymap_migration(fs.clone()))
248                            }
249                            Some(MigrationType::Settings) => {
250                                cx.background_spawn(write_settings_migration(fs.clone()))
251                            }
252                            None => unreachable!(),
253                        };
254                        task.detach_and_notify_err(workspace.clone(), window, cx);
255                    }
256                }),
257            )
258            .into_any_element()
259    }
260}
261
262async fn should_migrate_keymap(fs: Arc<dyn Fs>) -> Result<bool> {
263    let old_text = KeymapFile::load_keymap_file(&fs).await?;
264    if let Ok(Some(_)) = migrate_keymap(&old_text) {
265        return Ok(true);
266    };
267    Ok(false)
268}
269
270async fn should_migrate_settings(fs: Arc<dyn Fs>) -> Result<bool> {
271    let old_text = SettingsStore::load_settings(&fs).await?;
272    if let Ok(Some(_)) = migrate_settings(&old_text) {
273        return Ok(true);
274    };
275    Ok(false)
276}
277
278async fn write_keymap_migration(fs: Arc<dyn Fs>) -> Result<()> {
279    let old_text = KeymapFile::load_keymap_file(&fs).await?;
280    let Ok(Some(new_text)) = migrate_keymap(&old_text) else {
281        return Ok(());
282    };
283    let keymap_path = paths::keymap_file().as_path();
284    if fs.is_file(keymap_path).await {
285        fs.atomic_write(paths::keymap_backup_file().to_path_buf(), old_text)
286            .await
287            .with_context(|| "Failed to create settings backup in home directory".to_string())?;
288        let resolved_path = fs
289            .canonicalize(keymap_path)
290            .await
291            .with_context(|| format!("Failed to canonicalize keymap path {:?}", keymap_path))?;
292        fs.atomic_write(resolved_path.clone(), new_text)
293            .await
294            .with_context(|| format!("Failed to write keymap to file {:?}", resolved_path))?;
295    } else {
296        fs.atomic_write(keymap_path.to_path_buf(), new_text)
297            .await
298            .with_context(|| format!("Failed to write keymap to file {:?}", keymap_path))?;
299    }
300    Ok(())
301}
302
303async fn write_settings_migration(fs: Arc<dyn Fs>) -> Result<()> {
304    let old_text = SettingsStore::load_settings(&fs).await?;
305    let Ok(Some(new_text)) = migrate_settings(&old_text) else {
306        return Ok(());
307    };
308    let settings_path = paths::settings_file().as_path();
309    if fs.is_file(settings_path).await {
310        fs.atomic_write(paths::settings_backup_file().to_path_buf(), old_text)
311            .await
312            .with_context(|| "Failed to create settings backup in home directory".to_string())?;
313        let resolved_path = fs
314            .canonicalize(settings_path)
315            .await
316            .with_context(|| format!("Failed to canonicalize settings path {:?}", settings_path))?;
317        fs.atomic_write(resolved_path.clone(), new_text)
318            .await
319            .with_context(|| format!("Failed to write settings to file {:?}", resolved_path))?;
320    } else {
321        fs.atomic_write(settings_path.to_path_buf(), new_text)
322            .await
323            .with_context(|| format!("Failed to write settings to file {:?}", settings_path))?;
324    }
325    Ok(())
326}
327
Served at tenant.openagents/omega Member data and write actions are omitted.