Skip to repository content327 lines · 12.0 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T06:49:40.754Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
migrate.rs
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 ¬ifier,
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