2 Commits
Author SHA1 Message Date
reya b560ea4116 clean up 2026-10-22 05:18:35 +07:00
reya 14b66d530c Remove migration plan and one-shot scripts 2026-10-21 21:51:10 +07:00
28 changed files with 87 additions and 809 deletions
+4 -3
View File
@@ -16,6 +16,7 @@ use gpui::{
retain_all, svg, white,
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{Panel, PanelEvent};
use gpui_kit::component::input::{Input, InputEvent, InputState, Textarea, TextareaState};
@@ -1295,7 +1296,7 @@ impl ChatPanel {
.child(
h_flex()
.gap_1()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.child(Avatar::new(avatar).seed(profile.avatar_seed()).small())
.child(name.clone()),
),
@@ -1334,7 +1335,7 @@ impl ChatPanel {
.child(
div()
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.line_height(relative(1.25))
.child(SharedString::from(url.to_string())),
)
@@ -1364,7 +1365,7 @@ impl ChatPanel {
.child(
div()
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.line_height(relative(1.25))
.child(SharedString::from(url.0.to_string())),
)
+2 -6
View File
@@ -211,11 +211,7 @@ impl RenderOnce for MessageRow {
.text_xs()
.text_color(cx.theme().muted_foreground)
.when_some(author, |this, author| {
this.child(
div()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(author),
)
this.child(div().font_semibold().child(author))
})
.when_some(timestamp, |this, timestamp| {
this.child(timestamp)
@@ -299,7 +295,7 @@ impl RenderOnce for ReplyPreview {
.border_color(cx.theme().list_active)
.text_xs()
.refine_style(&style)
.child(div().font_weight(gpui::FontWeight::SEMIBOLD).child(author))
.child(div().font_semibold().child(author))
.child(div().w_full().text_ellipsis().line_clamp(1).child(preview))
.hover(|this| this.bg(cx.theme().secondary))
.when_some(on_click, |this, on_click| this.on_click(on_click))
+3 -2
View File
@@ -2,6 +2,7 @@ use gpui::{
AnyElement, App, IntoElement, ParentElement, SharedString, Styled, WeakEntity, Window, div, px,
relative,
};
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::{ActiveTheme, Sizable, WindowExt, h_flex, v_flex};
use nip29::{Group, GroupMetadata};
use ui::Avatar;
@@ -137,7 +138,7 @@ fn header(group: &Group, cx: &App) -> AnyElement {
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.truncate()
.child(group.display_name()),
)
@@ -162,7 +163,7 @@ fn section(label: &'static str, content: AnyElement, cx: &App) -> AnyElement {
.child(
div()
.text_xs()
.font_weight(gpui::FontWeight::MEDIUM)
.font_medium()
.text_color(cx.theme().muted_foreground)
.child(label),
)
+2 -1
View File
@@ -11,6 +11,7 @@ use gpui::{
list, px,
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariant, ButtonVariants};
use gpui_kit::component::dialog::DialogButtonProps;
use gpui_kit::component::dock::{Panel, PanelEvent};
@@ -1126,7 +1127,7 @@ impl GroupPanel {
.flex_shrink_0()
.max_w(px(140.))
.truncate()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.text_color(cx.theme().foreground)
.child(author),
)
+2 -1
View File
@@ -7,6 +7,7 @@ use gpui::{
WeakEntity, div,
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenu, PopupMenuItem};
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex};
use nip29::{Activity, Group, Reaction};
@@ -232,7 +233,7 @@ pub(crate) fn render_activity(
.min_w_0()
.truncate()
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.child(subject.name()),
)
.child(
+3 -2
View File
@@ -10,6 +10,7 @@ use gpui::{
App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement,
SharedString, Styled, Subscription, Task, Window, div, relative,
};
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::Button;
use gpui_kit::component::notification::{Notification, NotificationType};
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
@@ -743,7 +744,7 @@ impl DeviceRegistry {
.text_xs()
.child(
div()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from("From:")),
@@ -773,7 +774,7 @@ impl DeviceRegistry {
.text_xs()
.child(
div()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from("Code:")),
@@ -60,7 +60,6 @@ pub fn open(window: &mut Window, cx: &mut App) {
.icon(IconName::ArrowRight)
.label("Join")
.primary()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click({
let join = join.clone();
move |event, window, cx| {
-1
View File
@@ -450,7 +450,6 @@ impl Render for NewChat {
.icon(IconName::ArrowRight)
.label("Start")
.primary()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click(cx.listener(move |this, _event, window, cx| {
if this.start_chat(window, cx) {
window.close_dialog(cx);
+2 -2
View File
@@ -7,6 +7,7 @@ use gpui::{
};
use gpui_base::{Popup, Select};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle};
use gpui_kit::component::form::field;
@@ -641,7 +642,6 @@ impl Render for NewGroup {
"Create Group"
})
.primary()
.font_weight(gpui::FontWeight::SEMIBOLD)
.loading(self.creating)
.disabled(self.creating || self.uploading)
.on_click(cx.listener(|this, _event, _window, cx| this.create(cx))),
@@ -658,7 +658,7 @@ fn field_label(text: &'static str, cx: &App) -> Div {
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.child(text)
}
+2 -4
View File
@@ -8,6 +8,7 @@ use gpui::{
PathPromptOptions, Render, SharedString, Styled, Task, Window, div, retain_all,
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::DockPlacement;
use gpui_kit::component::input::{Input, InputState, Textarea, TextareaState};
@@ -486,7 +487,6 @@ impl Render for ProfileDialog {
.icon(IconName::SquarePen)
.label("Update")
.primary()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click(cx.listener(move |this, _ev, window, cx| {
this.toggle_edit(window, cx);
})),
@@ -498,7 +498,6 @@ impl Render for ProfileDialog {
.icon(IconName::MessageSquare)
.label("Message")
.primary()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click(cx.listener(move |this, _ev, window, cx| {
this.message(window, cx);
})),
@@ -519,7 +518,6 @@ impl Render for ProfileDialog {
.icon(IconName::CircleCheck)
.label("Save")
.primary()
.font_weight(gpui::FontWeight::SEMIBOLD)
.loading(self.updating)
.disabled(self.updating)
.on_click(cx.listener(move |this, _ev, window, cx| {
@@ -535,7 +533,7 @@ fn label(label: &'static str, cx: &App) -> Div {
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.child(label)
}
-1
View File
@@ -286,7 +286,6 @@ impl RelayManager {
.icon(IconName::ArrowRight)
.label("Update")
.primary()
.font_weight(gpui::FontWeight::SEMIBOLD)
.loading(updating)
.disabled(updating)
.on_click(cx.listener(move |this, _ev, window, cx| {
-1
View File
@@ -115,7 +115,6 @@ impl Render for RestoreEncryption {
.icon(IconName::ArrowRight)
.label("Restore")
.primary()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click(cx.listener(move |this, _, window, cx| {
this.restore(window, cx);
})),
+2 -1
View File
@@ -8,6 +8,7 @@ use gpui::{
SharedString, Styled, Subscription, Task, Window, div, px, relative, uniform_list,
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::spinner::Spinner;
use gpui_kit::component::{ActiveTheme, Disableable, Icon, Sizable, WindowExt, h_flex, v_flex};
@@ -376,7 +377,7 @@ impl Render for Screening {
)
.child(
div()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.line_height(relative(1.25))
.child(profile.name()),
),
+4 -7
View File
@@ -1,5 +1,6 @@
use std::sync::Arc;
use ::settings::{AppSettings, AppearanceMode};
use anyhow::Error;
use auto_update::AutoUpdater;
use chat::{ChatEvent, ChatRegistry};
@@ -11,7 +12,7 @@ use gpui::{
use gpui_kit::component::button::Button;
use gpui_kit::component::dock::{DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
use gpui_kit::component::notification::{Notification, NotificationType};
use gpui_kit::component::{ActiveTheme, WindowExt, v_flex};
use gpui_kit::component::{ActiveTheme, Theme, WindowExt, v_flex};
use nip4e::{DeviceEvent, DeviceRegistry};
use nip29::{GroupId, GroupKey};
use nostr_sdk::prelude::*;
@@ -88,13 +89,9 @@ impl Workspace {
let workspace = cx.entity().downgrade();
let mut subscriptions = smallvec![];
// Follow OS appearance changes only when the user has not pinned a
// mode; the native-theme watcher drives runtime changes either way.
// The `::settings` path is the external crate; the crate-local
// `settings` module is the preferences dialog.
if ::settings::AppSettings::get_appearance_mode(cx) == ::settings::AppearanceMode::System {
if AppSettings::get_appearance_mode(cx) == AppearanceMode::System {
subscriptions.push(cx.observe_window_appearance(window, |_this, window, cx| {
gpui_kit::component::Theme::sync_system_appearance(Some(window), cx);
Theme::sync_system_appearance(Some(window), cx);
}));
}
+3 -3
View File
@@ -4,6 +4,7 @@ use gpui::{
IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div,
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{Panel, PanelEvent};
use gpui_kit::component::input::{Input, InputState};
@@ -153,7 +154,7 @@ impl Render for BackupPanel {
.child(
div()
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from("Public Key:")),
)
@@ -166,7 +167,7 @@ impl Render for BackupPanel {
.child(
div()
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from("Secret Key:")),
)
@@ -184,7 +185,6 @@ impl Render for BackupPanel {
})
.primary()
.small()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click(cx.listener(move |this, _ev, _window, cx| {
this.copy_secret(cx);
})),
+3 -2
View File
@@ -10,6 +10,7 @@ use gpui::{
};
use gpui_base::{VirtualListScrollHandle, v_virtual_list};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
use gpui_kit::component::input::{Input, InputEvent, InputState};
@@ -269,7 +270,7 @@ impl BrowsePanel {
.child(
div()
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.line_height(relative(1.2))
.child("Public Groups"),
)
@@ -402,7 +403,7 @@ impl BrowsePanel {
div()
.text_xs()
.truncate()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.line_height(relative(1.2))
.child(name),
)
+2 -2
View File
@@ -8,6 +8,7 @@ use gpui::{
Window, div, rems, retain_all,
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{Panel, PanelEvent};
use gpui_kit::component::input::{Input, InputEvent, InputState};
@@ -325,7 +326,7 @@ impl Render for ContactListPanel {
.child(
div()
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.text_color(cx.theme().muted_foreground)
.child("New contact:"),
)
@@ -376,7 +377,6 @@ impl Render for ContactListPanel {
.icon(IconName::CircleCheck)
.label("Update")
.primary()
.font_weight(gpui::FontWeight::SEMIBOLD)
.loading(self.updating)
.disabled(self.updating)
.on_click(cx.listener(move |this, _ev, window, cx| {
+2 -1
View File
@@ -10,6 +10,7 @@ use gpui::{
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window, div, svg,
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants};
@@ -424,7 +425,7 @@ impl GreeterPanel {
div()
.text_color(cx.theme().muted_foreground)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.child("Find new friends"),
)
.child(
+3 -3
View File
@@ -165,7 +165,7 @@ impl Sidebar {
.text_xs()
.truncate()
.line_height(relative(1.2))
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.child(name),
)
.child(
@@ -250,7 +250,7 @@ impl Sidebar {
.text_color(cx.theme().muted_foreground),
)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.text_color(cx.theme().muted_foreground)
.on_click(cx.listener(move |this, _event, _window, cx| {
match section {
@@ -286,7 +286,7 @@ impl Sidebar {
let nav = Nav::new(public_key.to_hex())
.label(person.name())
.text_xs()
.font_weight(gpui::FontWeight::MEDIUM)
.font_medium()
.clickable(true)
.when(!hide_avatar, |this| {
this.prefix(
+2 -1
View File
@@ -4,6 +4,7 @@ use gpui::{
AnyElement, App, AppContext, Context, IntoElement, ParentElement, SharedString, Styled, Task,
Window, div,
};
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockPlacement, PanelHandle};
use gpui_kit::component::notification::Notification;
@@ -215,7 +216,7 @@ impl Sidebar {
.child("Selected")
.child(
div()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.child(SharedString::from(self.selected_contacts.len().to_string())),
),
)
+8 -6
View File
@@ -8,7 +8,7 @@ use gpui::{
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
use gpui_kit::component::{ActiveTheme, Icon, h_flex, v_flex};
use nip29::GroupsRegistry;
use settings::AppSettings;
@@ -65,19 +65,20 @@ impl Sidebar {
.child(
v_flex()
.px_2()
.gap_1()
.child(
Nav::new("browse")
.prefix(Icon::new(IconName::Compass).small())
.prefix(Icon::new(IconName::Compass))
.label("Browse")
.text_sm()
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(Command::ShowBrowse), cx)
}),
)
.child(
Nav::new("reqs")
.prefix(Icon::new(IconName::UserRoundPlus).small())
.prefix(Icon::new(IconName::UserRoundPlus))
.label("Requests")
.text_sm()
.when(request_count > 0, |this| {
this.suffix(
h_flex()
@@ -116,8 +117,9 @@ impl Sidebar {
)
.child(
Nav::new("contacts")
.prefix(Icon::new(IconName::Book).small())
.prefix(Icon::new(IconName::Book))
.label("Contacts")
.text_sm()
.when(contacts_open, |this| this.bg(cx.theme().list_active))
.on_click(cx.listener(|this, _event, window, cx| {
this.toggle_contacts(window, cx);
@@ -129,7 +131,7 @@ impl Sidebar {
div()
.px_4()
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.text_color(cx.theme().muted_foreground)
.child(list_title),
)
+2 -1
View File
@@ -9,6 +9,7 @@ use gpui::{
IntoElement, ObjectFit, ParentElement, Render, SharedString, Styled, StyledImage, Subscription,
Task, UniformListScrollHandle, WeakEntity, Window, div, img, retain_all,
};
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::dock::{DockArea, Panel, PanelEvent};
use gpui_kit::component::input::{InputEvent, InputState};
use gpui_kit::component::notification::Notification;
@@ -367,7 +368,7 @@ impl Render for Sidebar {
.when(cx.theme().shadow, |this| this.shadow_md())
.rounded_full()
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.text_color(cx.theme().muted_foreground)
.child(Spinner::new().small().color(cx.theme().primary))
.child("Getting messages..."),
@@ -38,7 +38,6 @@ pub(super) fn render_signed_out(_window: &mut Window, cx: &mut App) -> AnyElemen
.active(gpui::white().opacity(0.8)),
)
.large()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click(|_, window, cx| {
import::open(window, cx);
}),
+36 -35
View File
@@ -8,6 +8,7 @@ use gpui::{
SharedString, Styled, WeakEntity, Window, div, px,
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::dialog::DialogButtonProps;
use gpui_kit::component::dock::{ClosePanel, DockArea, DockPlacement, PanelHandle};
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
@@ -59,6 +60,30 @@ pub(crate) fn open_room(
}
}
/// A row shown in the sidebar's lists.
pub(super) enum SidebarRow {
Section(ListSection),
Room {
room: Entity<Room>,
},
Group {
group: Entity<Group>,
},
Contact {
public_key: PublicKey,
},
/// A profile found by global search.
Profile {
public_key: PublicKey,
},
Message {
hit: MessageHit,
},
/// A non-interactive label, such as a search results section.
Heading(&'static str),
Empty(&'static str),
}
impl Sidebar {
pub(super) fn render_rows(
&self,
@@ -100,7 +125,7 @@ impl Sidebar {
.text_color(cx.theme().muted_foreground),
)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.text_color(cx.theme().muted_foreground)
.on_click(cx.listener(move |this, _event, _window, cx| {
match section {
@@ -122,7 +147,7 @@ impl Sidebar {
Nav::new(label)
.label(label)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_semibold()
.text_color(cx.theme().muted_foreground)
.into_any_element()
}
@@ -160,7 +185,7 @@ impl Sidebar {
let nav = Nav::new(index)
.label(name)
.text_xs()
.text_sm()
.when(!hide_avatar, |this| {
this.prefix(
Avatar::new(room.read(cx).display_image(cx))
@@ -185,7 +210,7 @@ impl Sidebar {
.when(!spam, |this| {
this.child(
div()
.font_weight(gpui::FontWeight::NORMAL)
.font_normal()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(created_at),
@@ -230,7 +255,7 @@ impl Sidebar {
Nav::new(index)
.label(person.name())
.text_xs()
.text_sm()
.when(!hide_avatar, |this| {
this.prefix(
Avatar::new(person.avatar())
@@ -254,7 +279,7 @@ impl Sidebar {
Nav::new(index)
.label(person.name())
.text_xs()
.text_sm()
.when(!hide_avatar, |this| {
this.prefix(
Avatar::new(person.avatar())
@@ -287,7 +312,7 @@ impl Sidebar {
let nav = Nav::new(index)
.label(name)
.text_xs()
.text_sm()
.when(!hide_avatar, |this| {
this.prefix(
Avatar::new(group.read(cx).display_image())
@@ -298,7 +323,7 @@ impl Sidebar {
.when_some(suffix, |this, suffix| {
this.suffix(
div()
.font_weight(gpui::FontWeight::NORMAL)
.font_normal()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(suffix),
@@ -360,8 +385,8 @@ impl Sidebar {
Nav::new(index)
.label(content)
.text_xs()
.font_weight(gpui::FontWeight::MEDIUM)
.text_sm()
.font_medium()
.when(!hide_avatar, |this| {
this.prefix(
Avatar::new(person.avatar())
@@ -371,7 +396,7 @@ impl Sidebar {
})
.suffix(
div()
.font_weight(gpui::FontWeight::NORMAL)
.font_normal()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(detail),
@@ -386,27 +411,3 @@ impl Sidebar {
.collect()
}
}
/// A row shown in the sidebar's lists.
pub(super) enum SidebarRow {
Section(ListSection),
Room {
room: Entity<Room>,
},
Group {
group: Entity<Group>,
},
Contact {
public_key: PublicKey,
},
/// A profile found by global search.
Profile {
public_key: PublicKey,
},
Message {
hit: MessageHit,
},
/// A non-interactive label, such as a search results section.
Heading(&'static str),
Empty(&'static str),
}
-371
View File
@@ -1,371 +0,0 @@
# Migrate the UI to GPUI-Kit, then native-theme
> **Status: Phase A and Phase B are implemented** (on the working tree,
> uncommitted). The app builds against `gpui-kit` (resolved 0.7.3 /
> gpui-pre 0.3.9), every UI element comes from the kit, and `crates/theme` is
> deleted.
>
> **Phase B** adopted the OS theme through `native-theme-gpui` 0.6.1 (see
> below). `desktop` reads `SystemTheme::from_system()` at boot — before the
> window opens — and calls `apply_system_theme`, then honors the persisted
> appearance mode on top of it; a `watch`-feature subscription re-applies the
> OS theme at runtime via `native_theme::watch::on_theme_change`. Where the
> OS cannot be read, the boot logs a warning and the kit palette remains.
> The optional preset picker was skipped, matching the signed reference
> (its adoption removed its theme pickers — the connector owns fonts,
> radius and geometry). Web is untouched: `native-theme` is a desktop-only
> dependency and the wasm32 failure set is byte-for-byte the pre-existing
> baseline.
>
> **Follow-up (also on the working tree):** the last custom modules in
> `crates/ui` are gone — `motion`, `typography`, `layout` and `styled` were
> deleted and every call site now reads gpui-kit tokens directly (`text_xs` /
> `text_sm`, `cx.theme().radius` / `radius_lg`, `gap_2`, `Separator`). The
> dock wears the previous skin again, ported from the `signed` reference
> implementation onto the kit dock engine (`workspace::dock`: pill tabs, the
> 44px bar that doubles as the title bar, window controls, collapse buttons,
> chromeless sidebar group). The avatar with the pixel fallback is back
> (`ui::Avatar`): user image when set, deterministic pixel art from the
> profile/group seed otherwise, also as the image-load fallback. Desktop
> traffic lights recenter for the 44px bar (`workspace::TAB_BAR_HEIGHT`).
> Icons are vendored in `assets/icons/` (signed's set, filled from the kit
> bundle where coop needs more) and regression-tested in
> `crates/assets/tests`.
Plan to move coop's custom UI elements onto [GPUI-Kit](https://gpui-kit.com)
(`gpui-kit` 0.7.1), delete the custom theme implementation (`crates/theme`),
and then integrate [`native-theme`](https://github.com/tiborgats/native-theme)
through its official `native-theme-gpui` connector.
> **Revision.** This supersedes the earlier approach in this file (depend on
> the `native-theme` core crate and hand-write a `ResolvedTheme` → coop token
> mapping). Going through GPUI-Kit first makes that mapping unnecessary: the
> connector already projects native themes onto gpui-component's `Theme`, and
> every call site reads `cx.theme()` from gpui-component directly.
>
> **Revision 2.** Full replacement, no hybrids: no custom UI element is kept
> or maintained after Phase A. Earlier hybrid positions in this file (keep
> `ui::message` where it fits, possibly keep nav composites, keep an icon
> mapping shim) are dropped — kit elements are adopted outright, chat-specific
> composition moves into `chat_ui`, and the only coop-owned visuals left are
> brand SVG assets rendered through the kit `Icon`.
## 1. What the git history shows
- `516eb0e` — *move gpui-components to ui crate*: the UI crate **was** the
upstream component library, vendored into `crates/ui`. Call sites used
library components and its theme vocabulary
(`cx.theme().elevated_surface_background`, `surface_background`,
`border_variant`, …).
- `0c1c23f` — *migrate to gpui-base (#51)*: the vendored library was deleted
and replaced by the `gpui-base` crate plus hand-written wrappers (Button,
Switch, Dialog, Input, Dock, Menu, Notification, …) and the custom
`crates/theme`.
"Migrate like before" therefore means: go back to the upstream component
library — this time as a dependency (`gpui-kit`), not vendored — and let it
own the theme.
## 2. Strategy
```mermaid
graph LR
A[Phase A: GPUI-Kit] --> B[Phase B: native-theme-gpui]
A1[A1 Bootstrap<br/>init + open_window + Root] --> A2[A2 Component swap<br/>~20 custom modules]
A2 --> A3[A3 Theme removal<br/>cx.theme = gpui-component]
A3 --> B1[B1 Connector<br/>apply_system_theme]
B1 --> B2[B2 Appearance + presets<br/>settings wiring]
```
**Phase A** replaces the custom elements with `gpui_kit::component`
components and moves all color reads onto gpui-component's `Theme`
(`ThemeColor`, 139 semantic fields). The app runs on gpui-component's
built-in light/dark palettes at the end of Phase A; `crates/theme` is gone.
At the end of Phase A `crates/ui` contains no UI elements: every element
comes from the kit, and only app utilities survive there (§4.4).
**Phase B** adds `native-theme-gpui`, which installs native OS themes (or any
of 16 bundled presets) into that same `Theme`. No call site changes — the
connector is verified against exactly the versions Phase A pins.
### Target dependency set (exact versions)
| Crate | Version | Note |
|---|---|---|
| `gpui-kit` | 0.7.1 | facade: `gpui_kit::*` = GPUI (`gpui-pre`), `gpui_kit::component` (gpui-component 0.7.1), `gpui_kit::base` (gpui-base 0.7.1), `gpui_kit::assets` |
| `gpui-pre` | =0.3.8 | already pinned; gpui-kit 0.7.1 requires it |
| `native-theme-gpui` | 0.6 | Phase B; verified against gpui-kit 0.7.1 / gpui-pre 0.3.8; needs Rust ≥ 1.95 (toolchain is 1.98) |
Do not jump to `gpui-kit` 0.8: `native-theme-gpui` 0.6.1 pins 0.7.1, and a
gpui-component patch release has broken the connector before.
## 3. Current custom UI inventory
`crates/ui` is ~8,700 lines across 34 files. Consumers import it at ~138
sites (`workspace` 14 files, `chat_ui`, `nip29_ui`, `nip4e`, `settings`,
`desktop`, `web`); theme tokens are read at ~340 sites across the same
crates.
| Custom module | Lines | GPUI-Kit replacement |
|---|---|---|
| `menu/` (popup, dropdown, item) | 983 | `menu::{PopupMenu, PopupMenuItem, DropdownMenu, ContextMenu}` |
| `dock/` (mod, panel) | 1,032 | `dock` (entity-backed DockArea / panels) |
| `motion.rs` | 668 | keep — animation utility, not an element; no theme dependency |
| `notification.rs` | 594 | `notification::Notification` + `WindowExt::push_notification`; placement via `Theme.notification` |
| `button.rs` | 581 | `button::{Button, ButtonGroup, Toggle, DropdownButton}` |
| `message.rs` | 553 | `message::{Message, Bubble, MessageGroup, MessageScroller}` (avatar/header/content/footer slots); author grouping, date separators and receipts move into `chat_ui` as app code |
| `avatar.rs` | 445 | `avatar::Avatar` |
| `root.rs` | 430 | framework `Root` via `gpui_kit::open_window` (owns dialogs, notifications, tooltip/menu layers, focus traps) |
| `dialog.rs` | 394 | `WindowExt::open_dialog` / `AlertDialog`; custom footer buttons → Dialog's footer API |
| `input/` | ~290 | `input::{Input, InputState, Textarea, NumberInput}`; delete the custom styling (its `input_bg`/`selection`/`caret` reads die with it) |
| `icon.rs` | 274 | kit `Icon` + `IconName` (Lucide) at every call site; the module and the coop enum are deleted (see §4.3) |
| `kbd.rs` | 270 | `Kbd` |
| `tab/` | 385 | `tab::{Tab, TabBar, Tabs}` |
| `title_bar.rs` | 218 | `TitleBar` (keep the client-side-decoration constants) |
| `popover.rs` | 170 | `popover::Popover` |
| `styled.rs` | 161 | `h_flex`/`v_flex`/`StyledExt` from the kit; drop coop-specific helpers as their users migrate |
| `switch.rs` | 157 | `switch::Switch` |
| `markdown.rs` | 146 | `text::TextView::markdown` (code styling via `Theme.highlight_theme`) |
| `nav.rs` + `nav_item.rs` | 268 | `sidebar::{Sidebar, SidebarHeader, SidebarGroup, SidebarMenu, SidebarMenuItem, SidebarFooter}`; the nav layout is adapted to the kit structure, not preserved |
| `group_box.rs` | 126 | `group_box::GroupBox` |
| `scroll/` | ~124 | `scroll::{Scrollbar, Scrollable}` |
| `resizable/` | 31 | `resizable::{h_resizable, v_resizable, resizable_panel}` |
| `tooltip.rs` | 34 | `.tooltip()` |
| `indicator.rs` | 59 | `marker::Marker` / `badge::Badge`; the custom module is deleted either way |
| `window_ext.rs` | 90 | `gpui_kit::component::WindowExt` |
## 4. Phase A — GPUI-Kit migration
### 4.1 Bootstrap
- `desktop/main.rs` and `web/lib.rs`: `gpui_kit::init(cx)` first, then open
the window with `gpui_kit::open_window(opts, cx, |window, cx| …)`, which
installs the framework `Root` (dialog, sheet, notification and tooltip
layers). Delete `ui::Root` and its layers; `workspace::init` stays as the
window's content view.
- Register a combined `AssetSource` at app construction: coop's existing
per-platform source for `assets/brand` and images, with
`gpui_kit::assets::Assets` as the fallback for the component icon bundle
(embedded on desktop; CDN-on-demand on wasm).
- `TitlebarOptions` / client-side decoration setup stays coop's
(`CLIENT_SIDE_DECORATION_*`, `TABBAR_HEIGHT` move to `ui`).
- Keep `bind_keys` **before** `set_menus` (guide rule); coop already does.
### 4.2 Component slices (each PR compiles)
Replace one family at a time; consumers update in the same PR:
1. **Deps + bootstrap** (above). Custom components still work alongside the
framework Root.
2. **Icons** — rewrite every icon call site onto kit `Icon`/`IconName` and
delete `ui::icon` in the same PR (see §4.3). Mechanical churn: one Lucide
target per coop variant, `Icon::path` for brand marks; doing it early
settles the imports for all later slices.
3. **Small controls** — Button, Switch, Kbd, GroupBox, Avatar, Tooltip,
Popover; Indicator → `Marker`/`Badge`.
4. **Menu** — PopupMenu/DropdownMenu/ContextMenu (+ the popup-menu actions
already imported from `gpui_base::actions`).
5. **Input** — Input/InputState/Textarea; port the chat composer, dialog
fields, relay editor; drop the clear-button and input-styling modules.
6. **Dialog + Notification** — `open_dialog`/`AlertDialog`/
`push_notification`; port the settings, relays, profile, new-chat,
import… dialogs; the custom footers move to the Dialog footer API.
7. **Scroll + Resizable + Tab** — panels and the workspace tab strip.
8. **Sidebar/Nav** — port the nav onto `sidebar::{Sidebar, SidebarGroup,
SidebarMenu, …}` and delete `nav.rs`/`nav_item.rs` in the same PR.
9. **Title bar** — kit `TitleBar` + traffic-light handling.
10. **Dock** — the biggest single item (805 + 205 lines, `dock::add`
registry, `DockPlacement`). Port panels onto `DockArea`; do it last so
the panels it hosts are already kit-based.
11. **Message + Markdown** — TextView::markdown; port chat rows onto
`Message`/`Bubble`/`MessageGroup` + `MessageScroller`. Chat-specific
composition (author grouping, date separators, receipts) is app code in
`chat_ui`, built from kit elements — `crates/ui` keeps no message
component.
12. **Cleanup** — delete the replaced modules; `crates/ui` ends as a
utilities-only crate (see §4.4).
### 4.3 Icons
No mapping shim and no coop `IconName` enum: every call site uses the kit's
`Icon` + `IconName` (Lucide) directly. During slice 2, give each of coop's
128 variants a Lucide target — exact equivalent where one exists, nearest
match otherwise, accepting the visual difference. The only coop-owned SVGs
that survive are true brand/product marks under `assets/brand`, rendered
through the kit element with `Icon::path("brand/….svg")` (the kit `Icon`
accepts any asset path). Delete `assets/icons` entirely once the last call
site is mapped.
### 4.4 Theme removal
- **Call sites**: `theme::ActiveTheme` → `gpui_kit::component`'s `ActiveTheme`
(`cx.theme() -> &Theme`). Rewrite the ~340 token reads with the mapping
below (done per consumer crate, one slice each: `ui`, `workspace`,
`chat_ui`, `nip29_ui`, `settings` + `nip4e`).
| coop token (`cx.theme().x`) | gpui-component `ThemeColor` |
|---|---|
| `bg` | `background` |
| `surface` | `sidebar` |
| `surface_raised` | `secondary` |
| `surface_card` | `muted` |
| `surface_dialog`, `surface_overlay` | `popover` |
| `element_hover` | `accent` (gpui-component's accent **is** the item highlight) |
| `element_active` | `list_active` |
| `border` | `border` |
| `border_faint`, `border_strong` | `border` (one border token; faintness loss accepted) |
| `ring` | `ring` |
| `text` | `foreground` |
| `text_muted`, `text_faint`, `text_dim` | `muted_foreground` |
| `cursor`, `caret` | `caret` |
| `selection` | `selection` |
| `solid` / `on_solid` | `primary` / `primary_foreground` |
| `accent`, `accent_strong` | `primary` (emphasis ink) |
| `danger` / `danger_muted` | `danger` |
| `danger_strong` | `button_danger` |
| `warning` / `warning_muted` | `warning` / `warning.alpha(≈0.15)` at the call site |
| `success` | `success` |
| `input_bg`, `selection`, `caret` inside the custom input | die with the custom Input (§3) |
| `code_text`, `code_wash` | die with `ui::markdown` → `TextView` |
| `drop_line` / `drop_target` | `drag_border` / `drop_target` |
- **Non-color fields** move to `Theme` directly: `font_sans`/`font_mono` →
`theme.font_family`/`mono_font_family`; `shadow` → `theme.shadow`;
`scrollbar_mode` → `theme.scrollbar_mode`; notification placement →
`theme.notification` (gpui-component ships the same `NotificationSettings`
coop's was modeled on); `is_dark()` → `theme.mode`.
- **`crates/theme` disposition**:
- delete: `lib.rs` (Theme global, `AppExt`, `sync_base`, `PaletteBuilder`),
`colors.rs`, `palette.rs`, `brand.rs`, `appearance.rs`, `paint.rs`,
`color.rs`, `syntax.rs`, `notification.rs`, `scrollbar_mode.rs`,
`platform_kind.rs`;
- the `typography`, `layout`, `styled` and `motion` modules were NOT kept:
a follow-up revision removed them and adapted every call site to gpui-kit
tokens — the `TextStyle` ladder collapsed to `text_xs`/`text_sm` (plus
`font_semibold`/`font_medium`), the radius ladder to `cx.theme().radius`
/ `cx.theme().radius_lg`, `SPACE` to `gap_2`, `divider` to the kit
`Separator`, and motion was deleted unused. `geometry.rs` and the CSD
constants never carried over (the framework Root draws the window
border); `TAB_BAR_HEIGHT` (44px, the dock tab bar that doubles as the
title bar) now lives in `workspace::dock` and `SIDEBAR_WIDTH` in
`workspace`;
- end state: `crates/ui` is `avatar.rs` + `window_drag.rs` only — see the
status note at the top of this file for the restored dock skin and
avatar (the two deliberate exceptions to "no custom elements");
- `AppearanceMode` (System/Light/Dark, serde-persisted) moves into
`crates/settings`.
- The gpui-base projection coop did in `sync_base` is now the framework's job
(`Theme::update` / `Theme::change` keep colors, tokens and the base layer
in step per the coding guides).
### 4.5 Appearance handling (end of Phase A)
- `settings::apply_theme` → `Theme::change(mode, None, cx)` for a pinned
Light/Dark, `Theme::sync_system_appearance(None, cx)` for System.
- `workspace`'s `Theme::sync_system_appearance(Some(window), cx)` call stays,
now against the kit's `Theme`; window-appearance observation comes with the
framework Root/theme (verify; if not, keep the existing
`observe_window_appearance` wiring).
## 5. Phase B — native-theme-gpui
> **Implemented.** `native-theme` 0.6.1 (`watch`) + `native-theme-gpui` 0.6.1
> (default-features off, `system-fonts`) are workspace deps; only `desktop`
> depends on them, so nothing enters the wasm graph. `desktop/src/main.rs`
> applies the OS theme after `settings::init` and before the window opens,
> then honors the persisted appearance mode; on read failure it logs a
> warning and keeps the kit palette (which the appearance dropdown still
> drives). A `ThemeWatcher` global holds the `watch` subscription and
> re-applies on OS theme changes through a flume channel. The optional
> preset picker (step 3) was skipped, matching the signed reference where
> adoption *removed* the theme pickers. Settings, workspace and all UI
> crates are untouched — Phase A's appearance controls work on the native
> palettes as planned. The web fallback-preset idea was dropped with it:
> web keeps the kit palette and its appearance dropdown, exactly as before.
1. Add `native-theme-gpui = "0.6"` (pulls `native-theme`; keep default
features or trim icon features coop doesn't use).
2. Boot, after `gpui_kit::init` and before opening the window:
```rust,ignore
match native_theme::SystemTheme::from_system() {
Ok(sys) => native_theme_gpui::apply_system_theme(&sys, cx),
Err(error) => {
log::warn!("system theme unavailable: {error}");
// per-platform fallback preset: macos-sonoma / kde-breeze / windows-11
let prefs = native_theme_gpui::AccessibilityPreferences::default();
if let Ok((theme, resolved)) = native_theme_gpui::from_preset("macos-sonoma", true, &prefs) {
native_theme_gpui::apply(theme, &resolved, &prefs, cx);
}
}
}
```
`apply_system_theme` stores **both** variants and a `ThemeConfig` per
mode; afterwards the Phase A controls keep working unchanged:
`Theme::sync_system_appearance` / `Theme::change` switch between the
native palettes.
3. Optional preset picker (the same one proposed in the superseded plan):
`settings.theme_preset: Option<String>`, a dropdown in Preferences →
Appearance; selecting one calls `from_preset(name, is_dark, &prefs)` +
`apply` for both variants.
4. Fonts, radius, scrollbar geometry and the base layer are written by the
connector (it also re-asserts them after each upstream rebuild via its
observer). Coop writes nothing theme-related itself.
wasm: `SystemTheme::from_system` is unavailable on wasm32 — cfg-gate step 2
to native targets; on web, apply the fallback preset (or extend
`native-theme` with a web story later; presets need no OS reader).
## 6. Testing and QA
- Per slice: `cargo check` workspace, `cargo clippy`, `cargo fmt --check`,
plus the affected crate's tests; UI behavior verified by hand in the
desktop app.
- **Early spike (before slice 2)**: `cargo check -p web --target
wasm32-unknown-unknown` with the `gpui-kit` dependency added. The vendored
library was wasm-capable; confirm the published crates are too before
committing to the migration. If they are not, stop and reassess — the web
target is a launch blocker.
- End of Phase A:
- all dialogs open/close, focus trap and restore works; nested
popover-in-dialog works (framework Root owns the layers);
- notifications stack and auto-hide; settings' appearance dropdown switches
Light/Dark/System with a full repaint;
- dock panels move, split and zoom; sidebar resizes; scrollbars behave in
Hover/Always/Scrolling modes;
- markdown code blocks render through `TextView` with syntax colors from
`Theme.highlight_theme`;
- every icon renders from the kit Lucide bundle, and brand marks render
via `Icon::path` on desktop and web (on wasm the kit bundle is fetched
from CDN on demand; coop brand assets still come from coop's own
source).
- End of Phase B:
- macOS: OS light/dark follows; pinned modes; accent changes picked up at
next launch (documented limitation — no `watch` feature);
- Linux (KDE + GNOME/portal) and Windows: theme reads work or fall back to
the platform preset with a logged warning;
- preset switch renders in both modes; inputs/menus/dialogs/tooltips/
scrollbars all paint from the native palette;
- startup shows no wrong-palette flash (apply before the first window).
## 7. Risks
| Risk | Mitigation |
|---|---|
| Scale: ~8.7k lines of custom UI, ~140 `ui::` imports, ~340 token reads | Slices in §4.2 each compile; `crates/ui` acts as the facade during the migration so consumer churn is incremental, then shrinks to utilities (§4.4) |
| Dock port (largest, most stateful) | Scheduled last; panels are kit-based by then; DockArea owns layout state coop currently hand-rolls |
| gpui-component on wasm | Spike in §6 before committing; fallback would be staying on 0.7.1 desktop-only and parking the web target |
| Visual identity changes (shadcn-style stock palette in Phase A; kit Message/Sidebar look replacing coop's chat/nav styling) | Accepted — requirement says the current palette need not survive; Phase B replaces it with native themes anyway |
| Icon gaps (coop 128 vs Lucide 106) | Nearest-Lucide target per variant in slice 2; only true brand marks stay as coop assets via `Icon::path` |
| API drift between coop's wrappers and the kit (e.g. `Sizable`, `Disableable`, callback shapes) | Per the coding guides: never invent APIs; verify each component page / source signature during its slice |
| `TextStyle` ladder is a deliberate holdout from rem-based text | Documented exception; sizes are absolute points and don't claim to zoom. Revisit only if the kit's text scale is adopted later |
| native-theme-gpui version coupling | Pin `gpui-kit = "=0.7.1"`-era set (0.7.1, gpui-pre =0.3.8) matching the connector's verified table; bump all together |
## 8. Out of scope
- Migrating `motion.rs` or the panel-background image feature.
- Accessibility text scaling / reduce-motion plumbing (connector supports
it; adopt after Phase B if wanted).
- Replacing coop's composited panels (chat, screening, backup…) with kit
`Sheet`/`Settings` — revisit after the migration settles.
-98
View File
@@ -1,98 +0,0 @@
#!/usr/bin/env python3
"""Phase A fix-up: route gpui_kit::component imports to their real module paths,
rewrite font-weight helpers. Run from repo root. Idempotent."""
import re
from pathlib import Path
# item -> full module path under gpui_kit::component (None = keep at root)
MODULE_PATHS = {
"Avatar": "avatar",
"Button": "button",
"ButtonVariants": "button",
"ButtonVariant": "button",
"ButtonCustomVariant": "button",
"ButtonGroup": "button",
"DropdownButton": "button",
"Toggle": "button",
"Notification": "notification",
"NotificationType": "notification",
"Switch": "switch",
"GroupBox": "group_box",
"GroupBoxVariants": "group_box",
"GroupBoxVariant": "group_box",
"Tab": "tab",
"TabBar": "tab",
"Scrollbar": "scroll",
"ScrollableElement": "scroll",
"Input": "input",
"InputState": "input",
"Textarea": "textarea" if False else "input",
"TextareaState": "input",
"InputEvent": "input",
"PopupMenu": "menu",
"PopupMenuItem": "menu",
"DropdownMenu": "menu",
"ContextMenu": "menu",
"Dialog": "dialog",
"DialogButtonProps": "dialog",
"AlertDialog": "dialog",
"DockArea": "dock",
"DockLayout": "dock",
"DockPlacement": "dock",
"Panel": "dock",
"PanelEvent": "dock",
"PanelHandle": "dock",
"ClosePanel": "dock",
"DockSkin": "dock",
"Spinner": "spinner",
"Tooltip": "tooltip",
"Badge": "badge",
"Marker": "marker",
"Kbd": "kbd",
"Sidebar": "sidebar",
"SidebarMenu": "sidebar",
"SidebarMenuItem": "sidebar",
"SidebarGroup": "sidebar",
"SidebarHeader": "sidebar",
"SidebarFooter": "sidebar",
"SidebarItem": "sidebar",
}
FILES = list(Path("crates").rglob("*.rs"))
kit_re = re.compile(r"^use gpui_kit::component::\{([^}]*)\};")
for f in FILES:
src = f.read_text()
lines = src.split("\n")
buckets = {}
keep = []
for line in lines:
m = kit_re.match(line.strip())
if m:
for item in [i.strip() for i in m.group(1).split(",") if i.strip()]:
base = item.split(" as ")[0].strip()
mod = MODULE_PATHS.get(base)
buckets.setdefault(mod, []).append(item)
else:
keep.append(line)
if buckets:
imports = []
for mod, items in sorted(buckets.items(), key=lambda kv: kv[0] or ""):
uniq = sorted(set(items))
if mod is None:
imports.append(f"use gpui_kit::component::{{{', '.join(uniq)}}};")
else:
imports.append(f"use gpui_kit::component::{mod}::{{{', '.join(uniq)}}};")
# insert after the last existing `use` at top or just append imports where first import was
keep[0:0] = imports
src = "\n".join(keep)
# font weight helpers (gpui_base::StyledExt has none)
src = src.replace(".font_semibold()", ".font_weight(gpui::FontWeight::SEMIBOLD)")
src = src.replace(".font_medium()", ".font_weight(gpui::FontWeight::MEDIUM)")
src = src.replace(".font_normal()", ".font_weight(gpui::FontWeight::NORMAL)")
if src != f.read_text():
f.write_text(src)
print("fixed", f)
-189
View File
@@ -1,189 +0,0 @@
#!/usr/bin/env python3
"""Phase A sweep: mechanical import + token rewrites toward gpui-kit.
Run from repo root: python3 script/migration/sweep.py
Idempotent; only touches consumer crates.
"""
import re
import sys
from pathlib import Path
FILES = [
"crates/settings/src/lib.rs",
"crates/nip4e/src/lib.rs",
"crates/nip29_ui/src/lib.rs",
"crates/nip29_ui/src/message.rs",
"crates/nip29_ui/src/details.rs",
"crates/nip29_ui/src/invite.rs",
"crates/nip29_ui/src/metadata.rs",
"crates/nip29_ui/src/util.rs",
"crates/chat_ui/src/lib.rs",
"crates/chat_ui/src/text.rs",
"crates/chat_ui/src/actions.rs",
"crates/chat_ui/src/file.rs",
"crates/workspace/src/lib.rs",
"crates/workspace/src/dialogs/import.rs",
"crates/workspace/src/dialogs/join_group.rs",
"crates/workspace/src/dialogs/new_chat.rs",
"crates/workspace/src/dialogs/new_group.rs",
"crates/workspace/src/dialogs/profile.rs",
"crates/workspace/src/dialogs/relays.rs",
"crates/workspace/src/dialogs/restore.rs",
"crates/workspace/src/dialogs/screening.rs",
"crates/workspace/src/dialogs/settings.rs",
"crates/workspace/src/panels/backup.rs",
"crates/workspace/src/panels/browse.rs",
"crates/workspace/src/panels/contact_list.rs",
"crates/workspace/src/panels/greeter.rs",
"crates/workspace/src/panels/inbox.rs",
"crates/workspace/src/sidebar/group_view.rs",
"crates/workspace/src/sidebar/header.rs",
"crates/workspace/src/sidebar/inbox.rs",
"crates/workspace/src/sidebar/list.rs",
"crates/workspace/src/sidebar/mod.rs",
"crates/workspace/src/sidebar/onboarding.rs",
"crates/workspace/src/sidebar/rows.rs",
"crates/workspace/src/sidebar/search.rs",
]
KIT = "gpui_kit::component"
# ui:: items that move to the kit's component root
KIT_ITEMS = {
"Icon", "IconName", "Sizable", "Size", "Disableable", "Selectable",
"StyledExt", "h_flex", "v_flex", "WindowExt",
"Notification", "NotificationType",
"Button", "ButtonVariants", "ButtonVariant", "ButtonCustomVariant", "IconPosition",
"Switch", "Avatar", "GroupBox", "GroupBoxVariants", "GroupBoxVariant",
"Tab", "TabBar", "Scrollbar", "ScrollableElement", "Tooltip",
"Input", "InputState", "Textarea", "TextareaState", "InputEvent",
"PopupMenu", "PopupMenuItem", "DropdownMenu", "ContextMenu",
"Dialog", "DialogButtonProps", "AlertDialog",
"ActiveTheme", "Theme", "ThemeMode",
"Spinner", "Kbd", "TitleBar",
"DockArea", "DockLayout", "DockPlacement", "Panel", "PanelEvent",
"PanelHandle", "ClosePanel",
"Divider", "Separator",
}
# ui:: items that stay in the coop ui crate (utilities)
UI_ITEMS = {
"TextStyle", "Typeset", "Metrics", "base_text_size", "DEFAULT_BASE_TEXT_SIZE",
"SPACE", "CONTENT_MARGIN", "EDGE", "BUTTON_HEIGHT", "CONTROL_HEIGHT_SMALL",
"BASE_RADIUS", "bubble_radius", "surface_radius", "panel_radius", "button_radius",
"control_radius", "inset_radius",
"TABBAR_HEIGHT", "SIDEBAR_WIDTH", "TRAFFIC_LIGHT_PADDING",
"CLIENT_SIDE_DECORATION_ROUNDING", "CLIENT_SIDE_DECORATION_SHADOW",
"CLIENT_SIDE_DECORATION_BORDER",
"title_bar_drag_handlers",
}
TOKEN_RENAMES = [
# order matters: longest first
("surface_raised_hover", "secondary_hover"),
("surface_raised", "secondary"),
("surface_dialog", "popover"),
("surface_overlay", "popover"),
("surface_card", "muted"),
("surface", "sidebar"),
("element_hover", "accent"),
("element_active", "list_active"),
("border_faint", "border"),
("border_strong", "border"),
("text_muted", "muted_foreground"),
("text_faint", "muted_foreground"),
("text_dim", "muted_foreground"),
("on_solid", "primary_foreground"),
("solid", "primary"),
("accent_strong", "primary"),
("accent", "primary"),
("danger_strong", "button_danger"),
("danger_muted", "danger"),
("warning_muted", "warning.opacity(0.15)"),
("drop_line", "drag_border"),
("input_bg", "input"),
("bg", "background"),
("text", "foreground"),
]
def rewrite_use(line: str):
"""Rewrite one `use ui::...;` or `use theme::...;` line. Returns new lines."""
s = line.strip()
if s.startswith("use ui::{") and s.endswith(";"):
inner = s[len("use ui::{"):-2]
items = [i.strip() for i in inner.split(",") if i.strip()]
kit, ui = [], []
for item in items:
name = item.split(" as ")[0].strip()
(ui if name in UI_ITEMS else kit).append(item)
out = []
if kit:
out.append(f"use {KIT}::{{{', '.join(sorted(set(kit)))}}};")
if ui:
out.append(f"use ui::{{{', '.join(sorted(set(ui)))}}};")
return out
if s.startswith("use ui::") and s.endswith(";"):
path = s[4:-1]
item = path.split("::")[-1]
if item in UI_ITEMS:
return [f"use ui::{item};"]
return [f"use {KIT}::{{{item}}};"]
if s.startswith("use theme::{") and s.endswith(";"):
inner = s[len("use theme::{"):-2]
items = [i.strip() for i in inner.split(",") if i.strip()]
kit, ui = [], []
for item in items:
name = item.split("::")[-1]
if name == "ActiveTheme" or name in KIT_ITEMS:
kit.append(name)
else:
ui.append(name)
out = []
if kit:
out.append(f"use {KIT}::{{{', '.join(sorted(set(kit)))}}};")
if ui:
out.append(f"use ui::{{{', '.join(sorted(set(ui)))}}};")
return out
if s.startswith("use theme::") and s.endswith(";"):
name = s[11:-1].split("::")[-1]
if name == "ActiveTheme" or name in KIT_ITEMS:
return [f"use {KIT}::{name};"]
return [f"use ui::{name};"]
return [line]
def process(path: Path):
src = path.read_text()
lines = src.split("\n")
out = []
for line in lines:
s = line.strip()
if s.startswith("use ui::") or s.startswith("use theme::"):
out.extend(rewrite_use(line))
else:
out.append(line)
src = "\n".join(out)
# theme token renames, applied to `theme().x` reads only
for old, new in TOKEN_RENAMES:
src = re.sub(
re.escape("theme()." + old) + r"(?![a-z_0-9])",
"theme()." + new,
src,
)
# method/type renames
src = src.replace("WindowExtension", "WindowExt")
src = src.replace(".clear_notification_by_id::<", ".remove_notification::<")
src = src.replace(".clear_notification::<", ".remove_notification::<")
src = src.replace("with_kind(NotificationKind::", "with_type(NotificationType::")
src = src.replace("NotificationKind", "NotificationType")
path.write_text(src)
for f in FILES:
p = Path(f)
if p.exists():
process(p)
print("swept", f)
else:
print("MISSING", f, file=sys.stderr)
-63
View File
@@ -1,63 +0,0 @@
#!/usr/bin/env python3
'''One-shot sweep: replace custom ui helpers with gpui-kit tokens.'''
import pathlib
CRATES = pathlib.Path('crates')
REPLACEMENTS = [
# Text ladder -> gpui text classes (Caption2 before Caption)
('.text_style(TextStyle::Headline)', '.text_sm().font_semibold()'),
('.text_style(TextStyle::Body)', '.text_sm()'),
('.text_style(TextStyle::Caption2)', '.text_xs().font_medium()'),
('.text_style(TextStyle::Caption)', '.text_xs()'),
('.text_style(TextStyle::Callout)', '.text_xs()'),
('.text_style(TextStyle::Subheadline)', '.text_xs()'),
# Radius ladder -> theme tokens
('px(ui::bubble_radius())', 'cx.theme().radius_lg'),
('px(ui::surface_radius())', 'cx.theme().radius_lg'),
('px(ui::panel_radius())', 'cx.theme().radius_lg'),
('px(ui::control_radius())', 'cx.theme().radius'),
('px(ui::button_radius())', 'cx.theme().radius'),
('px(bubble_radius())', 'cx.theme().radius_lg'),
('px(surface_radius())', 'cx.theme().radius_lg'),
('px(panel_radius())', 'cx.theme().radius_lg'),
('px(control_radius())', 'cx.theme().radius'),
('px(button_radius())', 'cx.theme().radius'),
# Spacing -> Tailwind gap
('gap(px(ui::SPACE))', 'gap_2()'),
# Custom divider -> kit Separator
('divider(cx)', 'Separator::horizontal()'),
# Avatar import swap
('use gpui_kit::component::avatar::Avatar;', 'use ui::Avatar;'),
# Import lines that are now unused (whole line, with newline)
('use ui::{TextStyle, Typeset as _, bubble_radius, control_radius};\n', ''),
('use ui::{TextStyle, Typeset as _, button_radius, control_radius};\n', ''),
('use ui::{TextStyle, Typeset as _, button_radius, panel_radius};\n', ''),
('use ui::{TextStyle, Typeset as _, control_radius};\n', ''),
('use ui::{TextStyle, Typeset as _, button_radius};\n', ''),
('use ui::{TextStyle, Typeset as _, surface_radius};\n', ''),
('use ui::{TextStyle, Typeset as _};\n', ''),
('use ui::divider;\n', ''),
('use ui::button_radius;\n', ''),
]
def main() -> None:
changed = []
for path in CRATES.rglob('*.rs'):
if str(path).startswith('crates/ui/'):
continue
text = path.read_text()
original = text
for old, new in REPLACEMENTS:
text = text.replace(old, new)
if text != original:
path.write_text(text)
changed.append(str(path))
print('\n'.join(changed))
print(f'{len(changed)} files changed')
if __name__ == '__main__':
main()