add settings dialog
Rust / build (macos-latest, stable) (push) Waiting to run
Rust / build (ubuntu-latest, stable) (push) Waiting to run
Rust / build (windows-latest, stable) (push) Waiting to run
Rust / build (macos-latest, stable) (pull_request) Waiting to run
Rust / build (ubuntu-latest, stable) (pull_request) Waiting to run
Rust / build (windows-latest, stable) (pull_request) Waiting to run

This commit is contained in:
2026-11-12 09:03:20 +07:00
parent fd382405fd
commit e86f0fdbb1
8 changed files with 532 additions and 13 deletions
Generated
+8 -8
View File
@@ -3059,7 +3059,7 @@ dependencies = [
[[package]]
name = "gpui-native-core"
version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=ccd8997801186a33d4ed299d5bc9446c0f015781#ccd8997801186a33d4ed299d5bc9446c0f015781"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=9678a7676f4c70f91f04b6ab0ad68148659c2558#9678a7676f4c70f91f04b6ab0ad68148659c2558"
dependencies = [
"cpal",
"flume 0.12.0",
@@ -3074,7 +3074,7 @@ dependencies = [
[[package]]
name = "gpui-native-data"
version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=ccd8997801186a33d4ed299d5bc9446c0f015781#ccd8997801186a33d4ed299d5bc9446c0f015781"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=9678a7676f4c70f91f04b6ab0ad68148659c2558#9678a7676f4c70f91f04b6ab0ad68148659c2558"
dependencies = [
"gpui",
"gpui-native-core",
@@ -3086,7 +3086,7 @@ dependencies = [
[[package]]
name = "gpui-native-editor"
version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=ccd8997801186a33d4ed299d5bc9446c0f015781#ccd8997801186a33d4ed299d5bc9446c0f015781"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=9678a7676f4c70f91f04b6ab0ad68148659c2558#9678a7676f4c70f91f04b6ab0ad68148659c2558"
dependencies = [
"gpui",
"gpui-native-core",
@@ -3105,7 +3105,7 @@ dependencies = [
[[package]]
name = "gpui-native-icons"
version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=ccd8997801186a33d4ed299d5bc9446c0f015781#ccd8997801186a33d4ed299d5bc9446c0f015781"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=9678a7676f4c70f91f04b6ab0ad68148659c2558#9678a7676f4c70f91f04b6ab0ad68148659c2558"
dependencies = [
"anyhow",
"gpui",
@@ -3114,7 +3114,7 @@ dependencies = [
[[package]]
name = "gpui-native-kit"
version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=ccd8997801186a33d4ed299d5bc9446c0f015781#ccd8997801186a33d4ed299d5bc9446c0f015781"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=9678a7676f4c70f91f04b6ab0ad68148659c2558#9678a7676f4c70f91f04b6ab0ad68148659c2558"
dependencies = [
"gpui",
"gpui-native-core",
@@ -3128,7 +3128,7 @@ dependencies = [
[[package]]
name = "gpui-native-layout"
version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=ccd8997801186a33d4ed299d5bc9446c0f015781#ccd8997801186a33d4ed299d5bc9446c0f015781"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=9678a7676f4c70f91f04b6ab0ad68148659c2558#9678a7676f4c70f91f04b6ab0ad68148659c2558"
dependencies = [
"gpui",
"gpui-native-core",
@@ -3141,7 +3141,7 @@ dependencies = [
[[package]]
name = "gpui-native-theme"
version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=ccd8997801186a33d4ed299d5bc9446c0f015781#ccd8997801186a33d4ed299d5bc9446c0f015781"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=9678a7676f4c70f91f04b6ab0ad68148659c2558#9678a7676f4c70f91f04b6ab0ad68148659c2558"
dependencies = [
"anyhow",
"async-channel",
@@ -3162,7 +3162,7 @@ dependencies = [
[[package]]
name = "gpui-native-utils"
version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=ccd8997801186a33d4ed299d5bc9446c0f015781#ccd8997801186a33d4ed299d5bc9446c0f015781"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=9678a7676f4c70f91f04b6ab0ad68148659c2558#9678a7676f4c70f91f04b6ab0ad68148659c2558"
dependencies = [
"gpui",
"gpui-native-theme",
+1 -1
View File
@@ -14,7 +14,7 @@ gpui = { git = "https://github.com/zed-industries/zed", rev = "f8cfc7ed9e57503de
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "f8cfc7ed9e57503def9479138894f3139f5400cc", features = ["font-kit"] }
reqwest_client = { git = "https://github.com/zed-industries/zed", rev = "f8cfc7ed9e57503def9479138894f3139f5400cc" }
gpui-native-kit = { git = "https://git.reya.info/reya/gpui-native-kit", rev = "ccd8997801186a33d4ed299d5bc9446c0f015781", features = ["layout", "editor"] }
gpui-native-kit = { git = "https://git.reya.info/reya/gpui-native-kit", rev = "9678a7676f4c70f91f04b6ab0ad68148659c2558", features = ["layout", "editor"] }
settings = { path = "crates/settings" }
signed_app = { path = "crates/signed_app" }
+6 -1
View File
@@ -4,7 +4,12 @@ use settings::{AppearanceMode, Settings};
/// Applies the persisted light/dark/system preference to the kit theme.
pub fn apply_appearance(settings: &Settings, cx: &mut App) {
match settings.appearance {
apply_appearance_mode(settings.appearance, cx);
}
/// Applies one appearance mode to the kit theme.
pub fn apply_appearance_mode(mode: AppearanceMode, cx: &mut App) {
match mode {
AppearanceMode::System => follow_system(cx),
AppearanceMode::Light => set_mode_override(Some(ThemeVariant::Light), cx),
AppearanceMode::Dark => set_mode_override(Some(ThemeVariant::Dark), cx),
+2 -1
View File
@@ -5,11 +5,12 @@ mod inbox;
mod issues;
mod pull_requests;
mod repo;
mod settings;
mod shared;
mod sidebar;
pub use app::AppRoot;
pub use app::helpers::apply_appearance;
pub use app::helpers::{apply_appearance, apply_appearance_mode};
use gpui::{App, AppContext as _, Entity, Window};
+4 -2
View File
@@ -55,8 +55,10 @@ impl InitRepoDialog {
let state = cx.new(|_| DialogProgress::default());
let grasp_state = cx.new(|_| GraspServersState::new_default(&grasp_settings));
let name_input = InputState::builder().text(default_name).build(cx);
name_input.update(cx, |state, cx| state.set_read_only(true, cx));
let name_input = InputState::builder()
.text(default_name)
.read_only()
.build(cx);
let desc_input = InputState::builder().multiline().build(cx);
let relay_input = InputState::new(cx);
+369
View File
@@ -0,0 +1,369 @@
use std::path::PathBuf;
use gpui::{
AnyElement, App, AppContext as _, Entity, IntoElement, ParentElement as _, PathPromptOptions,
SharedString, Styled as _, WeakEntity, Window, div, px,
};
use gpui_native_kit::button::Button;
use gpui_native_kit::common::{h_flex, v_flex};
use gpui_native_kit::icons::{Icon, IconRole};
use gpui_native_kit::input::{Input, InputState};
use gpui_native_kit::select::{Select, SelectOption};
use gpui_native_kit::theme::ActiveTheme as _;
use gpui_native_kit::tooltip::Tooltip;
use settings::{AppearanceMode, EventFetchingStrategy, SettingsStore};
use super::SettingsDialog;
use crate::repo::dialogs::grasp_servers::{normalize_server, server_host};
/// A settings row with the label and description on the left and the control on the right.
pub fn setting_row(
cx: &App,
title: impl Into<SharedString>,
description: impl Into<SharedString>,
control: impl IntoElement,
) -> AnyElement {
h_flex()
.w_full()
.justify_between()
.gap_4()
.child(
v_flex()
.flex_1()
.min_w_0()
.gap_1()
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(title.into()),
)
.child(
div()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.child(description.into()),
),
)
.child(div().w(px(200.)).flex_shrink_0().child(control))
.into_any_element()
}
/// A full-width settings block with title and subtitle in one header.
pub fn setting_block(
cx: &App,
title: impl Into<SharedString>,
description: impl Into<SharedString>,
control: impl IntoElement,
) -> AnyElement {
v_flex()
.w_full()
.gap_3()
.child(
v_flex()
.gap_1()
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(title.into()),
)
.child(
div()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.child(description.into()),
),
)
.child(control)
.into_any_element()
}
pub fn appearance_select(cx: &App) -> Select<AppearanceMode> {
let current = SettingsStore::global(cx).read(cx).settings().appearance;
Select::new("settings-appearance")
.options(vec![
SelectOption::new(AppearanceMode::System, "System"),
SelectOption::new(AppearanceMode::Light, "Light"),
SelectOption::new(AppearanceMode::Dark, "Dark"),
])
.selected(Some(current))
.on_change(|value, _, cx| {
let Some(mode) = value else {
return;
};
SettingsStore::global(cx).update(cx, |store, cx| {
store.edit(|settings| settings.appearance = mode, cx);
});
crate::apply_appearance_mode(mode, cx);
})
}
pub fn event_fetching_select(cx: &App) -> Select<EventFetchingStrategy> {
let current = SettingsStore::global(cx).read(cx).settings().event_fetching;
Select::new("settings-event-fetching")
.options(vec![
SelectOption::new(EventFetchingStrategy::Curated, "Curated"),
SelectOption::new(EventFetchingStrategy::Uncensored, "Uncensored"),
])
.selected(Some(current))
.on_change(|value, _, cx| {
let Some(strategy) = value else {
return;
};
SettingsStore::global(cx).update(cx, |store, cx| {
store.edit(|settings| settings.event_fetching = strategy, cx);
});
})
}
pub fn grasp_servers_editor(input: &Entity<InputState>, cx: &App) -> AnyElement {
let servers = SettingsStore::global(cx)
.read(cx)
.settings()
.grasp_servers
.default_servers
.clone();
v_flex()
.gap_2()
.children(servers.iter().enumerate().map(|(ix, server)| {
let store = SettingsStore::global(cx);
h_flex()
.gap_1()
.child(
div()
.flex_1()
.h_8()
.px_2()
.bg(cx.theme().colors.muted)
.text_color(cx.theme().colors.muted_foreground)
.text_sm()
.rounded(cx.theme().radius.control)
.child(display_server(server)),
)
.child(
Button::new(format!("settings-remove-server:{ix}"))
.icon(Icon::named(IconRole::Close))
.variant(gpui_native_kit::button::ButtonVariant::Ghost)
.tooltip(Tooltip::text("Remove"))
.on_click({
let store = store.clone();
move |_, _, cx| {
store.update(cx, |store, cx| {
store.edit(
|settings| {
settings.grasp_servers.default_servers.remove(ix);
},
cx,
);
});
}
}),
)
.into_any_element()
}))
.child(
h_flex()
.gap_1()
.child(
div()
.flex_1()
.child(Input::new("settings-grasp-input", input.clone())),
)
.child(
Button::new("settings-add-server")
.icon(Icon::named(IconRole::Plus))
.variant(gpui_native_kit::button::ButtonVariant::Ghost)
.tooltip(Tooltip::text("Add grasp server"))
.on_click({
let input = input.clone();
move |_, window, cx| add_server(&input, window, cx)
}),
),
)
.into_any_element()
}
fn display_server(server: &str) -> SharedString {
match normalize_server(server) {
Some((_, relay)) => server_host(&relay),
None => SharedString::from(server.to_owned()),
}
}
fn add_server(input: &Entity<InputState>, window: &mut Window, cx: &mut App) {
let value = input.read(cx).value().trim().to_owned();
if value.is_empty() {
return;
}
let Some((normalized, _)) = normalize_server(&value) else {
return;
};
SettingsStore::global(cx).update(cx, |store, cx| {
store.edit(
|settings| {
if !settings.grasp_servers.default_servers.contains(&normalized) {
settings
.grasp_servers
.default_servers
.push(normalized.clone());
}
},
cx,
);
});
input.update(cx, |input, cx| input.set_value("", window, cx));
}
pub fn scan_paths_editor(cx: &App) -> AnyElement {
let scan_paths = SettingsStore::global(cx)
.read(cx)
.settings()
.local_repos
.scan_paths
.clone();
let store = SettingsStore::global(cx);
v_flex()
.gap_2()
.children(scan_paths.iter().enumerate().map(|(ix, path)| {
let store = store.clone();
h_flex()
.gap_1()
.child(
div()
.flex_1()
.h_8()
.px_2()
.bg(cx.theme().colors.muted)
.text_color(cx.theme().colors.muted_foreground)
.text_sm()
.rounded(cx.theme().radius.control)
.child(path.display().to_string()),
)
.child(
Button::new(format!("settings-remove-path:{ix}"))
.icon(Icon::named(IconRole::Close))
.variant(gpui_native_kit::button::ButtonVariant::Ghost)
.tooltip(Tooltip::text("Remove"))
.on_click(move |_, _, cx| {
store.update(cx, |store, cx| {
store.edit(
|settings| {
settings.local_repos.scan_paths.remove(ix);
},
cx,
);
});
}),
)
.into_any_element()
}))
.child(
h_flex().justify_end().child(
Button::new("settings-add-path")
.icon(Icon::named(IconRole::Plus))
.label("Add directory")
.on_click(|_, _, cx| add_scan_path(cx)),
),
)
.into_any_element()
}
fn add_scan_path(cx: &mut App) {
let prompt = cx.prompt_for_paths(PathPromptOptions {
files: false,
directories: true,
multiple: true,
prompt: Some("Choose directories".into()),
});
cx.spawn(async move |cx| {
if let Ok(Ok(Some(paths))) = prompt.await {
cx.update(|cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(
|settings| {
for path in paths {
let path = path.to_string_lossy().to_string();
if !settings
.local_repos
.scan_paths
.iter()
.any(|existing| existing.to_string_lossy() == path)
{
settings.local_repos.scan_paths.push(PathBuf::from(path));
}
}
},
cx,
);
});
});
}
})
.detach();
}
pub fn default_folder_selector(
default_folder: &Entity<InputState>,
weak: WeakEntity<SettingsDialog>,
) -> AnyElement {
let folder = default_folder.clone();
h_flex()
.gap_1()
.child(
div()
.flex_1()
.child(Input::new("settings-default-folder", folder.clone())),
)
.child(
Button::new("settings-choose-folder")
.icon(Icon::named(IconRole::Folder))
.variant(gpui_native_kit::button::ButtonVariant::Ghost)
.tooltip(Tooltip::text("Choose folder"))
.on_click(move |_, window, cx| {
choose_default_folder(&folder, &weak, window, cx);
}),
)
.into_any_element()
}
fn choose_default_folder(
default_folder: &Entity<InputState>,
weak: &WeakEntity<SettingsDialog>,
window: &mut Window,
cx: &mut App,
) {
let handle = window.window_handle();
let default_folder = default_folder.clone();
let weak = weak.clone();
let prompt = cx.prompt_for_paths(PathPromptOptions {
files: false,
directories: true,
multiple: false,
prompt: Some("Choose default folder".into()),
});
cx.spawn(async move |cx| {
if let Ok(Ok(Some(mut paths))) = prompt.await
&& let Some(path) = paths.pop()
{
let text = path.to_string_lossy().to_string();
if let Err(error) = cx.update_window(handle, |_, window, cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(
|settings| settings.create_repository.default_folder = Some(path.clone()),
cx,
);
});
default_folder.update(cx, |input, cx| input.set_value(text, window, cx));
let _ = weak.update(cx, |_this, cx| cx.notify());
}) {
log::warn!("failed to apply default folder: {error}");
}
}
})
.detach();
}
+112
View File
@@ -0,0 +1,112 @@
mod helpers;
use gpui::{
App, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
Window, px,
};
use gpui_native_kit::common::v_flex;
use gpui_native_kit::dialog::Dialog;
use gpui_native_kit::input::InputState;
use gpui_native_kit::scroll_area::ScrollArea;
use gpui_native_kit::separator::Separator;
use helpers::{
appearance_select, default_folder_selector, event_fetching_select, grasp_servers_editor,
scan_paths_editor, setting_block, setting_row,
};
use settings::SettingsStore;
use crate::shared::dialogs::open_global;
pub struct SettingsDialog {
grasp_input: Entity<InputState>,
default_folder: Entity<InputState>,
}
impl SettingsDialog {
pub fn new(cx: &mut App) -> Entity<Self> {
let store = SettingsStore::global(cx);
let settings = store.read(cx).settings().clone();
let grasp_input = InputState::new(cx);
let folder = settings
.create_repository
.default_folder
.clone()
.unwrap_or_else(paths::desktop_dir);
let default_folder = InputState::builder()
.text(folder.to_string_lossy())
.read_only()
.build(cx);
cx.new(|_| Self {
grasp_input,
default_folder,
})
}
}
impl Render for SettingsDialog {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let grasp_input = self.grasp_input.clone();
let default_folder = self.default_folder.clone();
let weak = cx.entity().downgrade();
Dialog::new("settings-dialog")
.open(true)
.min_width(px(650.))
.on_change(|open, _, cx| {
if !open {
crate::shared::dialogs::close_global(cx);
}
})
.title("Settings")
.content(move |_, cx| {
ScrollArea::new("settings-scroll")
.max_h(px(560.))
.child(
v_flex()
.gap_4()
.child(setting_row(
cx,
"Appearance",
"Choose whether the app follows the system theme or uses a light/dark theme.",
appearance_select(cx),
))
.child(Separator::horizontal())
.child(setting_row(
cx,
"Event Fetching Strategy",
"Curated fetches events from relays in the repository's announcement. Uncensored also fetches from every maintainer's relays.",
event_fetching_select(cx),
))
.child(Separator::horizontal())
.child(setting_block(
cx,
"Grasp Servers",
"Servers used to host your git repositories via the Grasp protocol",
grasp_servers_editor(&grasp_input, cx),
))
.child(Separator::horizontal())
.child(setting_block(
cx,
"Scan Directories",
"Directories scanned for local git repositories.",
scan_paths_editor(cx),
))
.child(setting_block(
cx,
"Default Folder",
"The folder for newly created repositories.",
default_folder_selector(&default_folder, weak.clone()),
)),
)
.into_any_element()
})
}
}
/// Opens the settings dialog.
pub fn open(cx: &mut App) {
open_global(SettingsDialog::new(cx), cx);
}
+30
View File
@@ -30,6 +30,7 @@ use crate::explore::ExploreView;
use crate::identity;
use crate::inbox::InboxView;
use crate::repo::dialogs;
use crate::settings;
mod helpers;
@@ -226,6 +227,7 @@ impl SidebarView {
Sidebar::new("signed-sidebar")
.transparent(true)
.header(self.render_user(&profile, self.scanning, cx))
.footer(self.render_footer(cx))
.entries(move |_, cx| {
let muted = cx.theme().colors.muted_foreground;
let mut entries = vec![
@@ -439,6 +441,34 @@ impl SidebarView {
);
}
fn render_footer(&self, _cx: &App) -> impl IntoElement {
let guide_weak = self.weak.clone();
let settings_click = |_: &gpui::ClickEvent, _: &mut Window, cx: &mut gpui::App| {
settings::open(cx);
};
v_flex()
.p_2()
.gap_1()
.w_full()
.child(
Button::new("guide")
.label("Guide")
.variant(ButtonVariant::Ghost)
.on_click(move |_, window, cx| {
let _ = guide_weak.update(cx, |this, cx| {
this.open_explore(window, cx);
});
}),
)
.child(
Button::new("settings")
.label("Settings")
.variant(ButtonVariant::Ghost)
.on_click(settings_click),
)
}
fn open_inbox(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let dock = self.dock.clone();
let dock_for_build = dock.clone();