Merge branch 'main' into switch-to-mixpanel

This commit is contained in:
Joseph T Lyons 2022-10-20 13:53:39 -04:00
commit ac5d5e2451
72 changed files with 2336 additions and 1888 deletions

152
Cargo.lock generated
View file

@ -855,22 +855,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chat_panel"
version = "0.1.0"
dependencies = [
"client",
"editor",
"gpui",
"menu",
"postage",
"settings",
"theme",
"time 0.3.15",
"util",
"workspace",
]
[[package]]
name = "chrono"
version = "0.4.22"
@ -1532,9 +1516,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.78"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4"
checksum = "3f83d0ebf42c6eafb8d7c52f7e5f2d3003b89c7aa4fd2b79229209459a849af8"
dependencies = [
"cc",
"cxxbridge-flags",
@ -1544,9 +1528,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.78"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199"
checksum = "07d050484b55975889284352b0ffc2ecbda25c0c55978017c132b29ba0818a86"
dependencies = [
"cc",
"codespan-reporting",
@ -1559,15 +1543,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.78"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c"
checksum = "99d2199b00553eda8012dfec8d3b1c75fce747cf27c169a270b3b99e3448ab78"
[[package]]
name = "cxxbridge-macro"
version = "1.0.78"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea"
checksum = "dcb67a6de1f602736dd7eaead0080cf3435df806c61b24b13328db128c58868f"
dependencies = [
"proc-macro2",
"quote",
@ -1729,12 +1713,9 @@ dependencies = [
[[package]]
name = "dotenvy"
version = "0.15.5"
version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9155c8f4dc55c7470ae9da3f63c6785245093b3f6aeb0f5bf2e968efbba314"
dependencies = [
"dirs 4.0.0",
]
checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0"
[[package]]
name = "drag_and_drop"
@ -2730,9 +2711,9 @@ dependencies = [
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa"
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
dependencies = [
"cxx",
"cxx-build",
@ -3449,7 +3430,7 @@ dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
"windows-sys 0.36.1",
]
[[package]]
@ -3878,7 +3859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core 0.9.3",
"parking_lot_core 0.9.4",
]
[[package]]
@ -3897,15 +3878,15 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.3"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"smallvec",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -4200,9 +4181,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.46"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
dependencies = [
"unicode-ident",
]
@ -4926,9 +4907,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.20.6"
version = "0.20.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c"
dependencies = [
"log",
"ring",
@ -5007,7 +4988,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
dependencies = [
"lazy_static",
"windows-sys",
"windows-sys 0.36.1",
]
[[package]]
@ -5618,7 +5599,7 @@ dependencies = [
"paste",
"percent-encoding",
"rand 0.8.5",
"rustls 0.20.6",
"rustls 0.20.7",
"rustls-pemfile",
"serde",
"serde_json",
@ -5939,6 +5920,18 @@ dependencies = [
"workspace",
]
[[package]]
name = "theme_testbench"
version = "0.1.0"
dependencies = [
"gpui",
"project",
"settings",
"smallvec",
"theme",
"workspace",
]
[[package]]
name = "thiserror"
version = "1.0.37"
@ -6125,7 +6118,7 @@ version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [
"rustls 0.20.6",
"rustls 0.20.7",
"tokio",
"webpki 0.22.0",
]
@ -7409,43 +7402,100 @@ version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_msvc",
"windows_aarch64_msvc 0.36.1",
"windows_i686_gnu 0.36.1",
"windows_i686_msvc 0.36.1",
"windows_x86_64_gnu 0.36.1",
"windows_x86_64_msvc 0.36.1",
]
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.42.0",
"windows_i686_gnu 0.42.0",
"windows_i686_msvc 0.42.0",
"windows_x86_64_gnu 0.42.0",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.42.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
[[package]]
name = "winreg"
version = "0.10.1"
@ -7532,9 +7582,9 @@ checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
name = "xmlparser"
version = "0.13.3"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8"
checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
[[package]]
name = "xmlwriter"
@ -7565,7 +7615,6 @@ dependencies = [
"backtrace",
"breadcrumbs",
"call",
"chat_panel",
"chrono",
"cli",
"client",
@ -7624,6 +7673,7 @@ dependencies = [
"text",
"theme",
"theme_selector",
"theme_testbench",
"thiserror",
"tiny_http",
"toml",

View file

@ -1,20 +0,0 @@
[package]
name = "chat_panel"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/chat_panel.rs"
doctest = false
[dependencies]
client = { path = "../client" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
postage = { version = "0.4.1", features = ["futures-traits"] }
time = { version = "0.3", features = ["serde", "serde-well-known"] }

View file

@ -1,433 +0,0 @@
use client::{
channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
Client,
};
use editor::Editor;
use gpui::{
actions,
elements::*,
platform::CursorStyle,
views::{ItemType, Select, SelectStyle},
AnyViewHandle, AppContext, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
Subscription, Task, View, ViewContext, ViewHandle,
};
use menu::Confirm;
use postage::prelude::Stream;
use settings::{Settings, SoftWrap};
use std::sync::Arc;
use time::{OffsetDateTime, UtcOffset};
use util::{ResultExt, TryFutureExt};
const MESSAGE_LOADING_THRESHOLD: usize = 50;
pub struct ChatPanel {
rpc: Arc<Client>,
channel_list: ModelHandle<ChannelList>,
active_channel: Option<(ModelHandle<Channel>, Subscription)>,
message_list: ListState,
input_editor: ViewHandle<Editor>,
channel_select: ViewHandle<Select>,
local_timezone: UtcOffset,
_observe_status: Task<()>,
}
pub enum Event {}
actions!(chat_panel, [LoadMoreMessages]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ChatPanel::send);
cx.add_action(ChatPanel::load_more_messages);
}
impl ChatPanel {
pub fn new(
rpc: Arc<Client>,
channel_list: ModelHandle<ChannelList>,
cx: &mut ViewContext<Self>,
) -> Self {
let input_editor = cx.add_view(|cx| {
let mut editor =
Editor::auto_height(4, Some(|theme| theme.chat_panel.input_editor.clone()), cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor
});
let channel_select = cx.add_view(|cx| {
let channel_list = channel_list.clone();
Select::new(0, cx, {
move |ix, item_type, is_hovered, cx| {
Self::render_channel_name(
&channel_list,
ix,
item_type,
is_hovered,
&cx.global::<Settings>().theme.chat_panel.channel_select,
cx,
)
}
})
.with_style(move |cx| {
let theme = &cx.global::<Settings>().theme.chat_panel.channel_select;
SelectStyle {
header: theme.header.container,
menu: theme.menu,
}
})
});
let mut message_list = ListState::new(0, Orientation::Bottom, 1000., cx, {
let this = cx.weak_handle();
move |_, ix, cx| {
let this = this.upgrade(cx).unwrap().read(cx);
let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix);
this.render_message(message, cx)
}
});
message_list.set_scroll_handler(|visible_range, cx| {
if visible_range.start < MESSAGE_LOADING_THRESHOLD {
cx.dispatch_action(LoadMoreMessages);
}
});
let _observe_status = cx.spawn_weak(|this, mut cx| {
let mut status = rpc.status();
async move {
while (status.recv().await).is_some() {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |_, cx| cx.notify());
} else {
break;
}
}
}
});
let mut this = Self {
rpc,
channel_list,
active_channel: Default::default(),
message_list,
input_editor,
channel_select,
local_timezone: cx.platform().local_timezone(),
_observe_status,
};
this.init_active_channel(cx);
cx.observe(&this.channel_list, |this, _, cx| {
this.init_active_channel(cx);
})
.detach();
cx.observe(&this.channel_select, |this, channel_select, cx| {
let selected_ix = channel_select.read(cx).selected_index();
let selected_channel = this.channel_list.update(cx, |channel_list, cx| {
let available_channels = channel_list.available_channels()?;
let channel_id = available_channels.get(selected_ix)?.id;
channel_list.get_channel(channel_id, cx)
});
if let Some(selected_channel) = selected_channel {
this.set_active_channel(selected_channel, cx);
}
})
.detach();
this
}
fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
let (active_channel, channel_count) = self.channel_list.update(cx, |list, cx| {
let channel_count;
let mut active_channel = None;
if let Some(available_channels) = list.available_channels() {
channel_count = available_channels.len();
if self.active_channel.is_none() {
if let Some(channel_id) = available_channels.first().map(|channel| channel.id) {
active_channel = list.get_channel(channel_id, cx);
}
}
} else {
channel_count = 0;
}
(active_channel, channel_count)
});
if let Some(active_channel) = active_channel {
self.set_active_channel(active_channel, cx);
} else {
self.message_list.reset(0);
self.active_channel = None;
}
self.channel_select.update(cx, |select, cx| {
select.set_item_count(channel_count, cx);
});
}
fn set_active_channel(&mut self, channel: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
{
let channel = channel.read(cx);
self.message_list.reset(channel.message_count());
let placeholder = format!("Message #{}", channel.name());
self.input_editor.update(cx, move |editor, cx| {
editor.set_placeholder_text(placeholder, cx);
});
}
let subscription = cx.subscribe(&channel, Self::channel_did_change);
self.active_channel = Some((channel, subscription));
}
}
fn channel_did_change(
&mut self,
_: ModelHandle<Channel>,
event: &ChannelEvent,
cx: &mut ViewContext<Self>,
) {
match event {
ChannelEvent::MessagesUpdated {
old_range,
new_count,
} => {
self.message_list.splice(old_range.clone(), *new_count);
}
}
cx.notify();
}
fn render_channel(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme;
Flex::column()
.with_child(
Container::new(ChildView::new(&self.channel_select, cx).boxed())
.with_style(theme.chat_panel.channel_select.container)
.boxed(),
)
.with_child(self.render_active_channel_messages())
.with_child(self.render_input_box(cx))
.boxed()
}
fn render_active_channel_messages(&self) -> ElementBox {
let messages = if self.active_channel.is_some() {
List::new(self.message_list.clone()).boxed()
} else {
Empty::new().boxed()
};
FlexItem::new(messages).flex(1., true).boxed()
}
fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox {
let now = OffsetDateTime::now_utc();
let settings = cx.global::<Settings>();
let theme = if message.is_pending() {
&settings.theme.chat_panel.pending_message
} else {
&settings.theme.chat_panel.message
};
Container::new(
Flex::column()
.with_child(
Flex::row()
.with_child(
Container::new(
Label::new(
message.sender.github_login.clone(),
theme.sender.text.clone(),
)
.boxed(),
)
.with_style(theme.sender.container)
.boxed(),
)
.with_child(
Container::new(
Label::new(
format_timestamp(message.timestamp, now, self.local_timezone),
theme.timestamp.text.clone(),
)
.boxed(),
)
.with_style(theme.timestamp.container)
.boxed(),
)
.boxed(),
)
.with_child(Text::new(message.body.clone(), theme.body.clone()).boxed())
.boxed(),
)
.with_style(theme.container)
.boxed()
}
fn render_input_box(&self, cx: &AppContext) -> ElementBox {
let theme = &cx.global::<Settings>().theme;
Container::new(ChildView::new(&self.input_editor, cx).boxed())
.with_style(theme.chat_panel.input_editor.container)
.boxed()
}
fn render_channel_name(
channel_list: &ModelHandle<ChannelList>,
ix: usize,
item_type: ItemType,
is_hovered: bool,
theme: &theme::ChannelSelect,
cx: &AppContext,
) -> ElementBox {
let channel = &channel_list.read(cx).available_channels().unwrap()[ix];
let theme = match (item_type, is_hovered) {
(ItemType::Header, _) => &theme.header,
(ItemType::Selected, false) => &theme.active_item,
(ItemType::Selected, true) => &theme.hovered_active_item,
(ItemType::Unselected, false) => &theme.item,
(ItemType::Unselected, true) => &theme.hovered_item,
};
Container::new(
Flex::row()
.with_child(
Container::new(Label::new("#".to_string(), theme.hash.text.clone()).boxed())
.with_style(theme.hash.container)
.boxed(),
)
.with_child(Label::new(channel.name.clone(), theme.name.clone()).boxed())
.boxed(),
)
.with_style(theme.container)
.boxed()
}
fn render_sign_in_prompt(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let rpc = self.rpc.clone();
let this = cx.handle();
enum SignInPromptLabel {}
Align::new(
MouseEventHandler::<SignInPromptLabel>::new(0, cx, |mouse_state, _| {
Label::new(
"Sign in to use chat".to_string(),
if mouse_state.hovered() {
theme.chat_panel.hovered_sign_in_prompt.clone()
} else {
theme.chat_panel.sign_in_prompt.clone()
},
)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
let rpc = rpc.clone();
let this = this.clone();
cx.spawn(|mut cx| async move {
if rpc
.authenticate_and_connect(true, &cx)
.log_err()
.await
.is_some()
{
cx.update(|cx| {
if let Some(this) = this.upgrade(cx) {
if this.is_focused(cx) {
this.update(cx, |this, cx| cx.focus(&this.input_editor));
}
}
})
}
})
.detach();
})
.boxed(),
)
.boxed()
}
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some((channel, _)) = self.active_channel.as_ref() {
let body = self.input_editor.update(cx, |editor, cx| {
let body = editor.text(cx);
editor.clear(cx);
body
});
if let Some(task) = channel
.update(cx, |channel, cx| channel.send_message(body, cx))
.log_err()
{
task.detach();
}
}
}
fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
if let Some((channel, _)) = self.active_channel.as_ref() {
channel.update(cx, |channel, cx| {
channel.load_more_messages(cx);
})
}
}
}
impl Entity for ChatPanel {
type Event = Event;
}
impl View for ChatPanel {
fn ui_name() -> &'static str {
"ChatPanel"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let element = if self.rpc.user_id().is_some() {
self.render_channel(cx)
} else {
self.render_sign_in_prompt(cx)
};
let theme = &cx.global::<Settings>().theme;
ConstrainedBox::new(
Container::new(element)
.with_style(theme.chat_panel.container)
.boxed(),
)
.with_min_width(150.)
.boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if matches!(
*self.rpc.status().borrow(),
client::Status::Connected { .. }
) {
cx.focus(&self.input_editor);
}
}
}
fn format_timestamp(
mut timestamp: OffsetDateTime,
mut now: OffsetDateTime,
local_timezone: UtcOffset,
) -> String {
timestamp = timestamp.to_offset(local_timezone);
now = now.to_offset(local_timezone);
let today = now.date();
let date = timestamp.date();
let mut hour = timestamp.hour();
let mut part = "am";
if hour > 12 {
hour -= 12;
part = "pm";
}
if date == today {
format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
} else if date.next_day() == Some(today) {
format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
} else {
format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
}
}

View file

@ -13,11 +13,13 @@ use async_tungstenite::tungstenite::{
http::{Request, StatusCode},
};
use db::Db;
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryStreamExt};
use gpui::{
actions, serde_json::Value, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle,
AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task, View, ViewContext, ViewHandle,
actions,
serde_json::{self, Value},
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext,
ViewHandle,
};
use http::HttpClient;
use lazy_static::lazy_static;
@ -25,6 +27,7 @@ use parking_lot::RwLock;
use postage::watch;
use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage};
use serde::Deserialize;
use std::{
any::TypeId,
collections::HashMap,
@ -50,6 +53,9 @@ lazy_static! {
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
}
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
@ -919,6 +925,37 @@ impl Client {
self.establish_websocket_connection(credentials, cx)
}
async fn get_rpc_url(http: Arc<dyn HttpClient>) -> Result<Url> {
let url = format!("{}/rpc", *ZED_SERVER_URL);
let response = http.get(&url, Default::default(), false).await?;
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
// The website's /rpc endpoint redirects to a collab server's /rpc endpoint,
// which requires authorization via an HTTP header.
//
// For testing purposes, ZED_SERVER_URL can also set to the direct URL of
// of a collab server. In that case, a request to the /rpc endpoint will
// return an 'unauthorized' response.
let collab_url = if response.status().is_redirection() {
response
.headers()
.get("Location")
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
.to_str()
.map_err(EstablishConnectionError::other)?
.to_string()
} else if response.status() == StatusCode::UNAUTHORIZED {
url
} else {
Err(anyhow!(
"unexpected /rpc response status {}",
response.status()
))?
};
Url::parse(&collab_url).context("invalid rpc url")
}
fn establish_websocket_connection(
self: &Arc<Self>,
credentials: &Credentials,
@ -933,28 +970,7 @@ impl Client {
let http = self.http.clone();
cx.background().spawn(async move {
let mut rpc_url = format!("{}/rpc", *ZED_SERVER_URL);
let rpc_response = http.get(&rpc_url, Default::default(), false).await?;
if rpc_response.status().is_redirection() {
rpc_url = rpc_response
.headers()
.get("Location")
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
.to_str()
.map_err(EstablishConnectionError::other)?
.to_string();
}
// Until we switch the zed.dev domain to point to the new Next.js app, there
// will be no redirect required, and the app will connect directly to
// wss://zed.dev/rpc.
else if rpc_response.status() != StatusCode::UPGRADE_REQUIRED {
Err(anyhow!(
"unexpected /rpc response status {}",
rpc_response.status()
))?
}
let mut rpc_url = Url::parse(&rpc_url).context("invalid rpc url")?;
let mut rpc_url = Self::get_rpc_url(http).await?;
let rpc_host = rpc_url
.host_str()
.zip(rpc_url.port_or_known_default())
@ -997,6 +1013,7 @@ impl Client {
let platform = cx.platform();
let executor = cx.background();
let telemetry = self.telemetry.clone();
let http = self.http.clone();
executor.clone().spawn(async move {
// Generate a pair of asymmetric encryption keys. The public key will be used by the
// zed server to encrypt the user's access token, so that it can'be intercepted by
@ -1006,6 +1023,10 @@ impl Client {
let public_key_string =
String::try_from(public_key).expect("failed to serialize public key for auth");
if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) {
return Self::authenticate_as_admin(http, login.clone(), token.clone()).await;
}
// Start an HTTP server to receive the redirect from Zed's sign-in page.
let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
let port = server.server_addr().port();
@ -1084,6 +1105,50 @@ impl Client {
})
}
async fn authenticate_as_admin(
http: Arc<dyn HttpClient>,
login: String,
mut api_token: String,
) -> Result<Credentials> {
#[derive(Deserialize)]
struct AuthenticatedUserResponse {
user: User,
}
#[derive(Deserialize)]
struct User {
id: u64,
}
// Use the collab server's admin API to retrieve the id
// of the impersonated user.
let mut url = Self::get_rpc_url(http.clone()).await?;
url.set_path("/user");
url.set_query(Some(&format!("github_login={login}")));
let request = Request::get(url.as_str())
.header("Authorization", format!("token {api_token}"))
.body("".into())?;
let mut response = http.send(request).await?;
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
if !response.status().is_success() {
Err(anyhow!(
"admin user request failed {} - {}",
response.status().as_u16(),
body,
))?;
}
let response: AuthenticatedUserResponse = serde_json::from_str(&body)?;
// Use the admin API token to authenticate as the impersonated user.
api_token.insert_str(0, "ADMIN_TOKEN:");
Ok(Credentials {
user_id: response.user.id,
access_token: api_token,
})
}
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
let conn_id = self.connection_id()?;
self.peer.disconnect(conn_id);

View file

@ -3,7 +3,5 @@ HTTP_PORT = 8080
API_TOKEN = "secret"
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
# HONEYCOMB_API_KEY=
# HONEYCOMB_DATASET=
# RUST_LOG=info
# LOG_JSON=true

View file

@ -65,31 +65,6 @@ spec:
secretKeyRef:
name: database
key: url
- name: SESSION_SECRET
valueFrom:
secretKeyRef:
name: session
key: secret
- name: GITHUB_APP_ID
valueFrom:
secretKeyRef:
name: github
key: appId
- name: GITHUB_CLIENT_ID
valueFrom:
secretKeyRef:
name: github
key: clientId
- name: GITHUB_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: github
key: clientSecret
- name: GITHUB_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: github
key: privateKey
- name: API_TOKEN
valueFrom:
secretKeyRef:
@ -101,13 +76,6 @@ spec:
value: ${RUST_LOG}
- name: LOG_JSON
value: "true"
- name: HONEYCOMB_DATASET
value: "collab"
- name: HONEYCOMB_API_KEY
valueFrom:
secretKeyRef:
name: honeycomb
key: apiKey
securityContext:
capabilities:
# FIXME - Switch to the more restrictive `PERFMON` capability.

View file

@ -76,7 +76,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
let state = req.extensions().get::<Arc<AppState>>().unwrap();
if token != state.api_token {
if token != state.config.api_token {
Err(Error::Http(
StatusCode::UNAUTHORIZED,
"invalid authorization token".to_string(),
@ -88,7 +88,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
#[derive(Debug, Deserialize)]
struct AuthenticatedUserParams {
github_user_id: i32,
github_user_id: Option<i32>,
github_login: String,
}
@ -104,7 +104,7 @@ async fn get_authenticated_user(
) -> Result<Json<AuthenticatedUserResponse>> {
let user = app
.db
.get_user_by_github_account(&params.github_login, Some(params.github_user_id))
.get_user_by_github_account(&params.github_login, params.github_user_id)
.await?
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
@ -156,7 +156,7 @@ async fn create_user(
Json(params): Json<CreateUserParams>,
Extension(app): Extension<Arc<AppState>>,
Extension(rpc_server): Extension<Arc<rpc::Server>>,
) -> Result<Json<CreateUserResponse>> {
) -> Result<Json<Option<CreateUserResponse>>> {
let user = NewUserParams {
github_login: params.github_login,
github_user_id: params.github_user_id,
@ -165,7 +165,8 @@ async fn create_user(
// Creating a user via the normal signup process
let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
app.db
if let Some(result) = app
.db
.create_user_from_invite(
&Invite {
email_address: params.email_address,
@ -174,6 +175,11 @@ async fn create_user(
user,
)
.await?
{
result
} else {
return Ok(Json(None));
}
}
// Creating a user as an admin
else if params.admin {
@ -200,11 +206,11 @@ async fn create_user(
.await?
.ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
Ok(Json(CreateUserResponse {
Ok(Json(Some(CreateUserResponse {
user,
metrics_id: result.metrics_id,
signup_device_id: result.signup_device_id,
}))
})))
}
#[derive(Deserialize)]

View file

@ -1,7 +1,7 @@
use std::sync::Arc;
use super::db::{self, UserId};
use crate::{AppState, Error, Result};
use crate::{
db::{self, UserId},
AppState, Error, Result,
};
use anyhow::{anyhow, Context};
use axum::{
http::{self, Request, StatusCode},
@ -13,6 +13,7 @@ use scrypt::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Scrypt,
};
use std::sync::Arc;
pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl IntoResponse {
let mut auth_header = req
@ -21,7 +22,7 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
.and_then(|header| header.to_str().ok())
.ok_or_else(|| {
Error::Http(
StatusCode::BAD_REQUEST,
StatusCode::UNAUTHORIZED,
"missing authorization header".to_string(),
)
})?
@ -41,12 +42,18 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
)
})?;
let state = req.extensions().get::<Arc<AppState>>().unwrap();
let mut credentials_valid = false;
for password_hash in state.db.get_access_token_hashes(user_id).await? {
if verify_access_token(access_token, &password_hash)? {
let state = req.extensions().get::<Arc<AppState>>().unwrap();
if let Some(admin_token) = access_token.strip_prefix("ADMIN_TOKEN:") {
if state.config.api_token == admin_token {
credentials_valid = true;
break;
}
} else {
for password_hash in state.db.get_access_token_hashes(user_id).await? {
if verify_access_token(access_token, &password_hash)? {
credentials_valid = true;
break;
}
}
}

View file

@ -51,7 +51,7 @@ pub trait Db: Send + Sync {
&self,
invite: &Invite,
user: NewUserParams,
) -> Result<NewUserResult>;
) -> Result<Option<NewUserResult>>;
/// Registers a new project for the given user.
async fn register_project(&self, host_user_id: UserId) -> Result<ProjectId>;
@ -482,7 +482,7 @@ impl Db for PostgresDb {
&self,
invite: &Invite,
user: NewUserParams,
) -> Result<NewUserResult> {
) -> Result<Option<NewUserResult>> {
let mut tx = self.pool.begin().await?;
let (signup_id, existing_user_id, inviting_user_id, signup_device_id): (
@ -506,10 +506,7 @@ impl Db for PostgresDb {
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?;
if existing_user_id.is_some() {
Err(Error::Http(
StatusCode::UNPROCESSABLE_ENTITY,
"invitation already redeemed".to_string(),
))?;
return Ok(None);
}
let (user_id, metrics_id): (UserId, String) = sqlx::query_as(
@ -576,12 +573,12 @@ impl Db for PostgresDb {
}
tx.commit().await?;
Ok(NewUserResult {
Ok(Some(NewUserResult {
user_id,
metrics_id,
inviting_user_id,
signup_device_id,
})
}))
}
// invite codes
@ -1958,7 +1955,7 @@ mod test {
&self,
_invite: &Invite,
_user: NewUserParams,
) -> Result<NewUserResult> {
) -> Result<Option<NewUserResult>> {
unimplemented!()
}

View file

@ -852,6 +852,7 @@ async fn test_invite_codes() {
},
)
.await
.unwrap()
.unwrap();
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
assert_eq!(invite_count, 1);
@ -897,6 +898,7 @@ async fn test_invite_codes() {
},
)
.await
.unwrap()
.unwrap();
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
assert_eq!(invite_count, 0);
@ -954,6 +956,7 @@ async fn test_invite_codes() {
)
.await
.unwrap()
.unwrap()
.user_id;
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
@ -1099,6 +1102,7 @@ async fn test_signups() {
},
)
.await
.unwrap()
.unwrap();
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
assert!(inviting_user_id.is_none());
@ -1108,19 +1112,21 @@ async fn test_signups() {
assert_eq!(signup_device_id.unwrap(), "device_id_0");
// cannot redeem the same signup again.
db.create_user_from_invite(
&Invite {
email_address: signups_batch1[0].email_address.clone(),
email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
},
NewUserParams {
github_login: "some-other-github_account".into(),
github_user_id: 1,
invite_count: 5,
},
)
.await
.unwrap_err();
assert!(db
.create_user_from_invite(
&Invite {
email_address: signups_batch1[0].email_address.clone(),
email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
},
NewUserParams {
github_login: "some-other-github_account".into(),
github_user_id: 1,
invite_count: 5,
},
)
.await
.unwrap()
.is_none());
// cannot redeem a signup with the wrong confirmation code.
db.create_user_from_invite(

View file

@ -6357,8 +6357,7 @@ impl TestServer {
async fn build_app_state(test_db: &TestDb) -> Arc<AppState> {
Arc::new(AppState {
db: test_db.db().clone(),
api_token: Default::default(),
invite_link_prefix: Default::default(),
config: Default::default(),
})
}

View file

@ -28,25 +28,21 @@ pub struct Config {
pub database_url: String,
pub api_token: String,
pub invite_link_prefix: String,
pub honeycomb_api_key: Option<String>,
pub honeycomb_dataset: Option<String>,
pub rust_log: Option<String>,
pub log_json: Option<bool>,
}
pub struct AppState {
db: Arc<dyn Db>,
api_token: String,
invite_link_prefix: String,
config: Config,
}
impl AppState {
async fn new(config: &Config) -> Result<Arc<Self>> {
async fn new(config: Config) -> Result<Arc<Self>> {
let db = PostgresDb::new(&config.database_url, 5).await?;
let this = Self {
db: Arc::new(db),
api_token: config.api_token.clone(),
invite_link_prefix: config.invite_link_prefix.clone(),
config,
};
Ok(Arc::new(this))
}
@ -63,9 +59,9 @@ async fn main() -> Result<()> {
let config = envy::from_env::<Config>().expect("error loading config");
init_tracing(&config);
let state = AppState::new(&config).await?;
let state = AppState::new(config).await?;
let listener = TcpListener::bind(&format!("0.0.0.0:{}", config.http_port))
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
.expect("failed to bind TCP listener");
let rpc_server = rpc::Server::new(state.clone(), None);

View file

@ -397,7 +397,7 @@ impl Server {
if let Some((code, count)) = invite_code {
this.peer.send(connection_id, proto::UpdateInviteInfo {
url: format!("{}{}", this.app_state.invite_link_prefix, code),
url: format!("{}{}", this.app_state.config.invite_link_prefix, code),
count,
})?;
}
@ -561,7 +561,7 @@ impl Server {
self.peer.send(
connection_id,
proto::UpdateInviteInfo {
url: format!("{}{}", self.app_state.invite_link_prefix, &code),
url: format!("{}{}", self.app_state.config.invite_link_prefix, &code),
count: user.invite_count as u32,
},
)?;
@ -579,7 +579,10 @@ impl Server {
self.peer.send(
connection_id,
proto::UpdateInviteInfo {
url: format!("{}{}", self.app_state.invite_link_prefix, invite_code),
url: format!(
"{}{}",
self.app_state.config.invite_link_prefix, invite_code
),
count: user.invite_count as u32,
},
)?;

View file

@ -1,10 +1,8 @@
use std::{ffi::OsStr, os::unix::prelude::OsStrExt, path::PathBuf, sync::Arc};
use std::{ffi::OsStr, fmt::Display, hash::Hash, os::unix::prelude::OsStrExt, path::PathBuf};
use anyhow::Result;
use rusqlite::{
named_params, params,
types::{FromSql, FromSqlError, FromSqlResult, ValueRef},
};
use collections::HashSet;
use rusqlite::{named_params, params};
use super::Db;
@ -31,7 +29,13 @@ pub enum SerializedItemKind {
Diagnostics,
}
#[derive(Clone, Debug, PartialEq, Eq)]
impl Display for SerializedItemKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&format!("{:?}", self))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum SerializedItem {
Editor(usize, PathBuf),
Terminal(usize),
@ -39,27 +43,6 @@ pub enum SerializedItem {
Diagnostics(usize),
}
impl FromSql for SerializedItemKind {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
match value {
ValueRef::Null => Err(FromSqlError::InvalidType),
ValueRef::Integer(_) => Err(FromSqlError::InvalidType),
ValueRef::Real(_) => Err(FromSqlError::InvalidType),
ValueRef::Text(bytes) => {
let str = std::str::from_utf8(bytes).map_err(|_| FromSqlError::InvalidType)?;
match str {
"Editor" => Ok(SerializedItemKind::Editor),
"Terminal" => Ok(SerializedItemKind::Terminal),
"ProjectSearch" => Ok(SerializedItemKind::ProjectSearch),
"Diagnostics" => Ok(SerializedItemKind::Diagnostics),
_ => Err(FromSqlError::InvalidType),
}
}
ValueRef::Blob(_) => Err(FromSqlError::InvalidType),
}
}
}
impl SerializedItem {
fn kind(&self) -> SerializedItemKind {
match self {
@ -82,117 +65,206 @@ impl SerializedItem {
impl Db {
fn write_item(&self, serialized_item: SerializedItem) -> Result<()> {
let mut lock = self.connection.lock();
let tx = lock.transaction()?;
self.real()
.map(|db| {
let mut lock = db.connection.lock();
let tx = lock.transaction()?;
// Serialize the item
let id = serialized_item.id();
{
let kind = format!("{:?}", serialized_item.kind());
// Serialize the item
let id = serialized_item.id();
{
let mut stmt = tx.prepare_cached(
"INSERT OR REPLACE INTO items(id, kind) VALUES ((?), (?))",
)?;
let mut stmt =
tx.prepare_cached("INSERT OR REPLACE INTO items(id, kind) VALUES ((?), (?))")?;
dbg!("inserting item");
stmt.execute(params![id, serialized_item.kind().to_string()])?;
}
stmt.execute(params![id, kind])?;
}
// Serialize item data
match &serialized_item {
SerializedItem::Editor(_, path) => {
dbg!("inserting path");
let mut stmt = tx.prepare_cached(
"INSERT OR REPLACE INTO item_path(item_id, path) VALUES ((?), (?))",
)?;
// Serialize item data
match &serialized_item {
SerializedItem::Editor(_, path) => {
let mut stmt = tx.prepare_cached(
"INSERT OR REPLACE INTO item_path(item_id, path) VALUES ((?), (?))",
)?;
let path_bytes = path.as_os_str().as_bytes();
stmt.execute(params![id, path_bytes])?;
}
SerializedItem::ProjectSearch(_, query) => {
dbg!("inserting query");
let mut stmt = tx.prepare_cached(
"INSERT OR REPLACE INTO item_query(item_id, query) VALUES ((?), (?))",
)?;
let path_bytes = path.as_os_str().as_bytes();
stmt.execute(params![id, path_bytes])?;
}
SerializedItem::ProjectSearch(_, query) => {
let mut stmt = tx.prepare_cached(
"INSERT OR REPLACE INTO item_query(item_id, query) VALUES ((?), (?))",
)?;
stmt.execute(params![id, query])?;
}
_ => {}
}
stmt.execute(params![id, query])?;
}
_ => {}
}
tx.commit()?;
tx.commit()?;
let mut stmt = lock.prepare_cached("SELECT id, kind FROM items")?;
let _ = stmt
.query_map([], |row| {
let zero: usize = row.get(0)?;
let one: String = row.get(1)?;
Ok(())
dbg!(zero, one);
Ok(())
})?
.collect::<Vec<Result<(), _>>>();
Ok(())
})
.unwrap_or(Ok(()))
}
fn delete_item(&self, item_id: usize) -> Result<()> {
let lock = self.connection.lock();
self.real()
.map(|db| {
let lock = db.connection.lock();
let mut stmt = lock.prepare_cached(
"
DELETE FROM items WHERE id = (:id);
DELETE FROM item_path WHERE id = (:id);
DELETE FROM item_query WHERE id = (:id);
",
)?;
let mut stmt = lock.prepare_cached(
r#"
DELETE FROM items WHERE id = (:id);
DELETE FROM item_path WHERE id = (:id);
DELETE FROM item_query WHERE id = (:id);
"#,
)?;
stmt.execute(named_params! {":id": item_id})?;
stmt.execute(named_params! {":id": item_id})?;
Ok(())
Ok(())
})
.unwrap_or(Ok(()))
}
fn take_items(&self) -> Result<Vec<SerializedItem>> {
let mut lock = self.connection.lock();
fn take_items(&self) -> Result<HashSet<SerializedItem>> {
self.real()
.map(|db| {
let mut lock = db.connection.lock();
let tx = lock.transaction()?;
let tx = lock.transaction()?;
// When working with transactions in rusqlite, need to make this kind of scope
// To make the borrow stuff work correctly. Don't know why, rust is wild.
let result = {
let mut read_stmt = tx.prepare_cached(
"
SELECT items.id, items.kind, item_path.path, item_query.query
FROM items
LEFT JOIN item_path
ON items.id = item_path.item_id
LEFT JOIN item_query
ON items.id = item_query.item_id
ORDER BY items.id
",
)?;
// When working with transactions in rusqlite, need to make this kind of scope
// To make the borrow stuff work correctly. Don't know why, rust is wild.
let result = {
let mut editors_stmt = tx.prepare_cached(
r#"
SELECT items.id, item_path.path
FROM items
LEFT JOIN item_path
ON items.id = item_path.item_id
WHERE items.kind = ?;
"#,
)?;
let result = read_stmt
.query_map([], |row| {
let id: usize = row.get(0)?;
let kind: SerializedItemKind = row.get(1)?;
let editors_iter = editors_stmt.query_map(
[SerializedItemKind::Editor.to_string()],
|row| {
let id: usize = row.get(0)?;
match kind {
SerializedItemKind::Editor => {
let buf: Vec<u8> = row.get(2)?;
let buf: Vec<u8> = row.get(1)?;
let path: PathBuf = OsStr::from_bytes(&buf).into();
Ok(SerializedItem::Editor(id, path))
}
SerializedItemKind::Terminal => Ok(SerializedItem::Terminal(id)),
SerializedItemKind::ProjectSearch => {
let query: Arc<str> = row.get(3)?;
Ok(SerializedItem::ProjectSearch(id, query.to_string()))
}
SerializedItemKind::Diagnostics => Ok(SerializedItem::Diagnostics(id)),
}
})?
.collect::<Result<Vec<SerializedItem>, rusqlite::Error>>()?;
},
)?;
let mut delete_stmt = tx.prepare_cached(
"DELETE FROM items;
DELETE FROM item_path;
DELETE FROM item_query;",
)?;
let mut terminals_stmt = tx.prepare_cached(
r#"
SELECT items.id
FROM items
WHERE items.kind = ?;
"#,
)?;
let terminals_iter = terminals_stmt.query_map(
[SerializedItemKind::Terminal.to_string()],
|row| {
let id: usize = row.get(0)?;
delete_stmt.execute([])?;
Ok(SerializedItem::Terminal(id))
},
)?;
result
};
let mut search_stmt = tx.prepare_cached(
r#"
SELECT items.id, item_query.query
FROM items
LEFT JOIN item_query
ON items.id = item_query.item_id
WHERE items.kind = ?;
"#,
)?;
let searches_iter = search_stmt.query_map(
[SerializedItemKind::ProjectSearch.to_string()],
|row| {
let id: usize = row.get(0)?;
let query = row.get(1)?;
tx.commit()?;
Ok(SerializedItem::ProjectSearch(id, query))
},
)?;
Ok(result)
#[cfg(debug_assertions)]
let tmp =
searches_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
#[cfg(debug_assertions)]
debug_assert!(tmp.len() == 0 || tmp.len() == 1);
#[cfg(debug_assertions)]
let searches_iter = tmp.into_iter();
let mut diagnostic_stmt = tx.prepare_cached(
r#"
SELECT items.id
FROM items
WHERE items.kind = ?;
"#,
)?;
let diagnostics_iter = diagnostic_stmt.query_map(
[SerializedItemKind::Diagnostics.to_string()],
|row| {
let id: usize = row.get(0)?;
Ok(SerializedItem::Diagnostics(id))
},
)?;
#[cfg(debug_assertions)]
let tmp =
diagnostics_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
#[cfg(debug_assertions)]
debug_assert!(tmp.len() == 0 || tmp.len() == 1);
#[cfg(debug_assertions)]
let diagnostics_iter = tmp.into_iter();
let res = editors_iter
.chain(terminals_iter)
.chain(diagnostics_iter)
.chain(searches_iter)
.collect::<Result<HashSet<SerializedItem>, rusqlite::Error>>()?;
let mut delete_stmt = tx.prepare_cached(
r#"
DELETE FROM items;
DELETE FROM item_path;
DELETE FROM item_query;
"#,
)?;
delete_stmt.execute([])?;
res
};
tx.commit()?;
Ok(result)
})
.unwrap_or(Ok(HashSet::default()))
}
}
@ -204,29 +276,32 @@ mod test {
#[test]
fn test_items_round_trip() -> Result<()> {
let db = Db::open_in_memory()?;
let db = Db::open_in_memory();
let mut items = vec![
SerializedItem::Editor(0, PathBuf::from("/tmp/test.txt")),
SerializedItem::Terminal(1),
SerializedItem::ProjectSearch(2, "Test query!".to_string()),
SerializedItem::Diagnostics(3),
];
]
.into_iter()
.collect::<HashSet<_>>();
for item in items.iter() {
dbg!("Inserting... ");
db.write_item(item.clone())?;
}
assert_eq!(items, db.take_items()?);
// Check that it's empty, as expected
assert_eq!(Vec::<SerializedItem>::new(), db.take_items()?);
assert_eq!(HashSet::default(), db.take_items()?);
for item in items.iter() {
db.write_item(item.clone())?;
}
items.remove(2);
items.remove(&SerializedItem::ProjectSearch(2, "Test query!".to_string()));
db.delete_item(2)?;
assert_eq!(items, db.take_items()?);

View file

@ -35,7 +35,6 @@ serde = { version = "1.0", features = ["derive"] }
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
client = { path = "../client", features = ["test-support"]}

View file

@ -1,77 +1,71 @@
use alacritty_terminal::{ansi::Color as AnsiColor, term::color::Rgb as AlacRgb};
use gpui::color::Color;
use theme::TerminalColors;
use theme::TerminalStyle;
///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
pub fn convert_color(alac_color: &AnsiColor, colors: &TerminalColors, modal: bool) -> Color {
let background = if modal {
colors.modal_background
} else {
colors.background
};
pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
match alac_color {
//Named and theme defined colors
alacritty_terminal::ansi::Color::Named(n) => match n {
alacritty_terminal::ansi::NamedColor::Black => colors.black,
alacritty_terminal::ansi::NamedColor::Red => colors.red,
alacritty_terminal::ansi::NamedColor::Green => colors.green,
alacritty_terminal::ansi::NamedColor::Yellow => colors.yellow,
alacritty_terminal::ansi::NamedColor::Blue => colors.blue,
alacritty_terminal::ansi::NamedColor::Magenta => colors.magenta,
alacritty_terminal::ansi::NamedColor::Cyan => colors.cyan,
alacritty_terminal::ansi::NamedColor::White => colors.white,
alacritty_terminal::ansi::NamedColor::BrightBlack => colors.bright_black,
alacritty_terminal::ansi::NamedColor::BrightRed => colors.bright_red,
alacritty_terminal::ansi::NamedColor::BrightGreen => colors.bright_green,
alacritty_terminal::ansi::NamedColor::BrightYellow => colors.bright_yellow,
alacritty_terminal::ansi::NamedColor::BrightBlue => colors.bright_blue,
alacritty_terminal::ansi::NamedColor::BrightMagenta => colors.bright_magenta,
alacritty_terminal::ansi::NamedColor::BrightCyan => colors.bright_cyan,
alacritty_terminal::ansi::NamedColor::BrightWhite => colors.bright_white,
alacritty_terminal::ansi::NamedColor::Foreground => colors.foreground,
alacritty_terminal::ansi::NamedColor::Background => background,
alacritty_terminal::ansi::NamedColor::Cursor => colors.cursor,
alacritty_terminal::ansi::NamedColor::DimBlack => colors.dim_black,
alacritty_terminal::ansi::NamedColor::DimRed => colors.dim_red,
alacritty_terminal::ansi::NamedColor::DimGreen => colors.dim_green,
alacritty_terminal::ansi::NamedColor::DimYellow => colors.dim_yellow,
alacritty_terminal::ansi::NamedColor::DimBlue => colors.dim_blue,
alacritty_terminal::ansi::NamedColor::DimMagenta => colors.dim_magenta,
alacritty_terminal::ansi::NamedColor::DimCyan => colors.dim_cyan,
alacritty_terminal::ansi::NamedColor::DimWhite => colors.dim_white,
alacritty_terminal::ansi::NamedColor::BrightForeground => colors.bright_foreground,
alacritty_terminal::ansi::NamedColor::DimForeground => colors.dim_foreground,
alacritty_terminal::ansi::NamedColor::Black => style.black,
alacritty_terminal::ansi::NamedColor::Red => style.red,
alacritty_terminal::ansi::NamedColor::Green => style.green,
alacritty_terminal::ansi::NamedColor::Yellow => style.yellow,
alacritty_terminal::ansi::NamedColor::Blue => style.blue,
alacritty_terminal::ansi::NamedColor::Magenta => style.magenta,
alacritty_terminal::ansi::NamedColor::Cyan => style.cyan,
alacritty_terminal::ansi::NamedColor::White => style.white,
alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black,
alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red,
alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green,
alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow,
alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue,
alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta,
alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan,
alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white,
alacritty_terminal::ansi::NamedColor::Foreground => style.foreground,
alacritty_terminal::ansi::NamedColor::Background => style.background,
alacritty_terminal::ansi::NamedColor::Cursor => style.cursor,
alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black,
alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red,
alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green,
alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow,
alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue,
alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta,
alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan,
alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white,
alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground,
alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground,
},
//'True' colors
alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX),
//8 bit, indexed colors
alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(&(*i as usize), colors),
alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(&(*i as usize), style),
}
}
///Converts an 8 bit ANSI color to it's GPUI equivalent.
///Accepts usize for compatability with the alacritty::Colors interface,
///Other than that use case, should only be called with values in the [0,255] range
pub fn get_color_at_index(index: &usize, colors: &TerminalColors) -> Color {
pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color {
match index {
//0-15 are the same as the named colors above
0 => colors.black,
1 => colors.red,
2 => colors.green,
3 => colors.yellow,
4 => colors.blue,
5 => colors.magenta,
6 => colors.cyan,
7 => colors.white,
8 => colors.bright_black,
9 => colors.bright_red,
10 => colors.bright_green,
11 => colors.bright_yellow,
12 => colors.bright_blue,
13 => colors.bright_magenta,
14 => colors.bright_cyan,
15 => colors.bright_white,
0 => style.black,
1 => style.red,
2 => style.green,
3 => style.yellow,
4 => style.blue,
5 => style.magenta,
6 => style.cyan,
7 => style.white,
8 => style.bright_black,
9 => style.bright_red,
10 => style.bright_green,
11 => style.bright_yellow,
12 => style.bright_blue,
13 => style.bright_magenta,
14 => style.bright_cyan,
15 => style.bright_white,
//16-231 are mapped to their RGB colors on a 0-5 range per channel
16..=231 => {
let (r, g, b) = rgb_for_index(&(*index as u8)); //Split the index into it's ANSI-RGB components
@ -85,19 +79,19 @@ pub fn get_color_at_index(index: &usize, colors: &TerminalColors) -> Color {
Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
}
//For compatability with the alacritty::Colors interface
256 => colors.foreground,
257 => colors.background,
258 => colors.cursor,
259 => colors.dim_black,
260 => colors.dim_red,
261 => colors.dim_green,
262 => colors.dim_yellow,
263 => colors.dim_blue,
264 => colors.dim_magenta,
265 => colors.dim_cyan,
266 => colors.dim_white,
267 => colors.bright_foreground,
268 => colors.black, //'Dim Background', non-standard color
256 => style.foreground,
257 => style.background,
258 => style.cursor,
259 => style.dim_black,
260 => style.dim_red,
261 => style.dim_green,
262 => style.dim_yellow,
263 => style.dim_blue,
264 => style.dim_magenta,
265 => style.dim_cyan,
266 => style.dim_white,
267 => style.bright_foreground,
268 => style.black, //'Dim Background', non-standard color
_ => Color::new(0, 0, 0, 255),
}
}

View file

@ -603,7 +603,7 @@ impl Terminal {
InternalEvent::ColorRequest(index, format) => {
let color = term.colors()[*index].unwrap_or_else(|| {
let term_style = &cx.global::<Settings>().theme.terminal;
to_alac_rgb(get_color_at_index(index, &term_style.colors))
to_alac_rgb(get_color_at_index(index, &term_style))
});
self.write_to_pty(format(color))
}

View file

@ -44,7 +44,6 @@ impl TerminalContainerContent {
}
pub struct TerminalContainer {
modal: bool,
pub content: TerminalContainerContent,
associated_directory: Option<PathBuf>,
}
@ -128,7 +127,6 @@ impl TerminalContainer {
cx.focus(content.handle());
TerminalContainer {
modal,
content,
associated_directory: working_directory,
}
@ -141,7 +139,6 @@ impl TerminalContainer {
) -> Self {
let connected_view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
TerminalContainer {
modal,
content: TerminalContainerContent::Connected(connected_view),
associated_directory: None,
}
@ -161,17 +158,11 @@ impl View for TerminalContainer {
}
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
let child_view = match &self.content {
match &self.content {
TerminalContainerContent::Connected(connected) => ChildView::new(connected, cx),
TerminalContainerContent::Error(error) => ChildView::new(error, cx),
};
if self.modal {
let settings = cx.global::<Settings>();
let container_style = settings.theme.terminal.modal_container;
child_view.contained().with_style(container_style).boxed()
} else {
child_view.boxed()
}
.boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@ -179,14 +170,6 @@ impl View for TerminalContainer {
cx.focus(self.content.handle());
}
}
fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
let mut context = Self::default_keymap_context();
if self.modal {
context.set.insert("ModalTerminal".into());
}
context
}
}
impl View for ErrorView {

View file

@ -152,7 +152,6 @@ impl LayoutRect {
pub struct TerminalElement {
terminal: WeakModelHandle<Terminal>,
view: WeakViewHandle<TerminalView>,
modal: bool,
focused: bool,
cursor_visible: bool,
}
@ -161,14 +160,12 @@ impl TerminalElement {
pub fn new(
view: WeakViewHandle<TerminalView>,
terminal: WeakModelHandle<Terminal>,
modal: bool,
focused: bool,
cursor_visible: bool,
) -> TerminalElement {
TerminalElement {
view,
terminal,
modal,
focused,
cursor_visible,
}
@ -182,7 +179,6 @@ impl TerminalElement {
terminal_theme: &TerminalStyle,
text_layout_cache: &TextLayoutCache,
font_cache: &FontCache,
modal: bool,
hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
let mut cells = vec![];
@ -222,7 +218,7 @@ impl TerminalElement {
cur_rect = Some(LayoutRect::new(
Point::new(line_index as i32, cell.point.column.0 as i32),
1,
convert_color(&bg, &terminal_theme.colors, modal),
convert_color(&bg, &terminal_theme),
));
}
}
@ -231,7 +227,7 @@ impl TerminalElement {
cur_rect = Some(LayoutRect::new(
Point::new(line_index as i32, cell.point.column.0 as i32),
1,
convert_color(&bg, &terminal_theme.colors, modal),
convert_color(&bg, &terminal_theme),
));
}
}
@ -248,7 +244,6 @@ impl TerminalElement {
terminal_theme,
text_style,
font_cache,
modal,
hyperlink,
);
@ -308,11 +303,10 @@ impl TerminalElement {
style: &TerminalStyle,
text_style: &TextStyle,
font_cache: &FontCache,
modal: bool,
hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
) -> RunStyle {
let flags = indexed.cell.flags;
let fg = convert_color(&fg, &style.colors, modal);
let fg = convert_color(&fg, &style);
let mut underline = flags
.intersects(Flags::ALL_UNDERLINES)
@ -574,11 +568,7 @@ impl Element for TerminalElement {
Default::default()
};
let background_color = if self.modal {
terminal_theme.colors.modal_background
} else {
terminal_theme.colors.background
};
let background_color = terminal_theme.background;
let terminal_handle = self.terminal.upgrade(cx).unwrap();
let last_hovered_hyperlink = terminal_handle.update(cx.app, |terminal, cx| {
@ -639,7 +629,6 @@ impl Element for TerminalElement {
&terminal_theme,
cx.text_layout_cache,
cx.font_cache(),
self.modal,
last_hovered_hyperlink
.as_ref()
.map(|(_, range, _)| (link_style, range)),
@ -655,9 +644,9 @@ impl Element for TerminalElement {
let str_trxt = cursor_char.to_string();
let color = if self.focused {
terminal_theme.colors.background
terminal_theme.background
} else {
terminal_theme.colors.foreground
terminal_theme.foreground
};
cx.text_layout_cache.layout_str(
@ -691,7 +680,7 @@ impl Element for TerminalElement {
cursor_position,
block_width,
dimensions.line_height,
terminal_theme.colors.cursor,
terminal_theme.cursor,
shape,
text,
)

View file

@ -321,7 +321,6 @@ impl View for TerminalView {
TerminalElement::new(
cx.handle(),
terminal_handle,
self.modal,
focused,
self.should_show_cursor(focused, cx),
)

View file

@ -2,7 +2,7 @@ mod theme_registry;
use gpui::{
color::Color,
elements::{ContainerStyle, ImageStyle, LabelStyle, TooltipStyle},
elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, TooltipStyle},
fonts::{HighlightStyle, TextStyle},
Border, MouseState,
};
@ -18,7 +18,6 @@ pub struct Theme {
pub meta: ThemeMeta,
pub workspace: Workspace,
pub context_menu: ContextMenu,
pub chat_panel: ChatPanel,
pub contacts_popover: ContactsPopover,
pub contact_list: ContactList,
pub contact_finder: ContactFinder,
@ -35,6 +34,7 @@ pub struct Theme {
pub incoming_call_notification: IncomingCallNotification,
pub tooltip: TooltipStyle,
pub terminal: TerminalStyle,
pub color_scheme: ColorScheme,
}
#[derive(Deserialize, Default, Clone)]
@ -318,18 +318,6 @@ pub struct SidebarItem {
pub icon_size: f32,
}
#[derive(Deserialize, Default)]
pub struct ChatPanel {
#[serde(flatten)]
pub container: ContainerStyle,
pub message: ChatMessage,
pub pending_message: ChatMessage,
pub channel_select: ChannelSelect,
pub input_editor: FieldEditor,
pub sign_in_prompt: TextStyle,
pub hovered_sign_in_prompt: TextStyle,
}
#[derive(Deserialize, Default)]
pub struct ProjectPanel {
#[serde(flatten)]
@ -772,12 +760,6 @@ pub struct HoverPopover {
#[derive(Clone, Deserialize, Default)]
pub struct TerminalStyle {
pub colors: TerminalColors,
pub modal_container: ContainerStyle,
}
#[derive(Clone, Deserialize, Default)]
pub struct TerminalColors {
pub black: Color,
pub red: Color,
pub green: Color,
@ -809,3 +791,67 @@ pub struct TerminalColors {
pub bright_foreground: Color,
pub dim_foreground: Color,
}
#[derive(Clone, Deserialize, Default)]
pub struct ColorScheme {
pub name: String,
pub is_light: bool,
pub ramps: RampSet,
pub lowest: Layer,
pub middle: Layer,
pub highest: Layer,
pub popover_shadow: Shadow,
pub modal_shadow: Shadow,
pub players: Vec<Player>,
}
#[derive(Clone, Deserialize, Default)]
pub struct Player {
pub cursor: Color,
pub selection: Color,
}
#[derive(Clone, Deserialize, Default)]
pub struct RampSet {
pub neutral: Vec<Color>,
pub red: Vec<Color>,
pub orange: Vec<Color>,
pub yellow: Vec<Color>,
pub green: Vec<Color>,
pub cyan: Vec<Color>,
pub blue: Vec<Color>,
pub violet: Vec<Color>,
pub magenta: Vec<Color>,
}
#[derive(Clone, Deserialize, Default)]
pub struct Layer {
pub base: StyleSet,
pub variant: StyleSet,
pub on: StyleSet,
pub accent: StyleSet,
pub positive: StyleSet,
pub warning: StyleSet,
pub negative: StyleSet,
}
#[derive(Clone, Deserialize, Default)]
pub struct StyleSet {
pub default: Style,
pub active: Style,
pub disabled: Style,
pub hovered: Style,
pub pressed: Style,
pub inverted: Style,
}
#[derive(Clone, Deserialize, Default)]
pub struct Style {
pub background: Color,
pub border: Color,
pub foreground: Color,
}

View file

@ -0,0 +1,18 @@
[package]
name = "theme_testbench"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/theme_testbench.rs"
doctest = false
[dependencies]
gpui = { path = "../gpui" }
theme = { path = "../theme" }
settings = { path = "../settings" }
workspace = { path = "../workspace" }
project = { path = "../project" }
smallvec = { version = "1.6", features = ["union"] }

View file

@ -0,0 +1,357 @@
use gpui::{
actions,
color::Color,
elements::{
Canvas, Container, ContainerStyle, ElementBox, Flex, Label, Margin, MouseEventHandler,
Padding, ParentElement,
},
fonts::TextStyle,
Border, Element, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext,
};
use project::{Project, ProjectEntryId, ProjectPath};
use settings::Settings;
use smallvec::SmallVec;
use theme::{ColorScheme, Layer, Style, StyleSet};
use workspace::{Item, Workspace};
actions!(theme, [DeployThemeTestbench]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ThemeTestbench::deploy);
}
pub struct ThemeTestbench {}
impl ThemeTestbench {
pub fn deploy(
workspace: &mut Workspace,
_: &DeployThemeTestbench,
cx: &mut ViewContext<Workspace>,
) {
let view = cx.add_view(|_| ThemeTestbench {});
workspace.add_item(Box::new(view), cx);
}
fn render_ramps(color_scheme: &ColorScheme) -> Flex {
fn display_ramp(ramp: &Vec<Color>) -> ElementBox {
Flex::row()
.with_children(ramp.iter().cloned().map(|color| {
Canvas::new(move |bounds, _, cx| {
cx.scene.push_quad(Quad {
bounds,
background: Some(color),
..Default::default()
});
})
.flex(1.0, false)
.boxed()
}))
.flex(1.0, false)
.boxed()
}
Flex::column()
.with_child(display_ramp(&color_scheme.ramps.neutral))
.with_child(display_ramp(&color_scheme.ramps.red))
.with_child(display_ramp(&color_scheme.ramps.orange))
.with_child(display_ramp(&color_scheme.ramps.yellow))
.with_child(display_ramp(&color_scheme.ramps.green))
.with_child(display_ramp(&color_scheme.ramps.cyan))
.with_child(display_ramp(&color_scheme.ramps.blue))
.with_child(display_ramp(&color_scheme.ramps.violet))
.with_child(display_ramp(&color_scheme.ramps.magenta))
}
fn render_layer(
layer_index: usize,
layer: &Layer,
cx: &mut RenderContext<'_, Self>,
) -> Container {
Flex::column()
.with_child(
Self::render_button_set(0, layer_index, "base", &layer.base, cx)
.flex(1., false)
.boxed(),
)
.with_child(
Self::render_button_set(1, layer_index, "variant", &layer.variant, cx)
.flex(1., false)
.boxed(),
)
.with_child(
Self::render_button_set(2, layer_index, "on", &layer.on, cx)
.flex(1., false)
.boxed(),
)
.with_child(
Self::render_button_set(3, layer_index, "accent", &layer.accent, cx)
.flex(1., false)
.boxed(),
)
.with_child(
Self::render_button_set(4, layer_index, "positive", &layer.positive, cx)
.flex(1., false)
.boxed(),
)
.with_child(
Self::render_button_set(5, layer_index, "warning", &layer.warning, cx)
.flex(1., false)
.boxed(),
)
.with_child(
Self::render_button_set(6, layer_index, "negative", &layer.negative, cx)
.flex(1., false)
.boxed(),
)
.contained()
.with_style(ContainerStyle {
margin: Margin {
top: 10.,
bottom: 10.,
left: 10.,
right: 10.,
},
background_color: Some(layer.base.default.background),
..Default::default()
})
}
fn render_button_set(
set_index: usize,
layer_index: usize,
set_name: &'static str,
style_set: &StyleSet,
cx: &mut RenderContext<'_, Self>,
) -> Flex {
Flex::row()
.with_child(Self::render_button(
set_index * 6,
layer_index,
set_name,
&style_set,
None,
cx,
))
.with_child(Self::render_button(
set_index * 6 + 1,
layer_index,
"hovered",
&style_set,
Some(|style_set| &style_set.hovered),
cx,
))
.with_child(Self::render_button(
set_index * 6 + 2,
layer_index,
"pressed",
&style_set,
Some(|style_set| &style_set.pressed),
cx,
))
.with_child(Self::render_button(
set_index * 6 + 3,
layer_index,
"active",
&style_set,
Some(|style_set| &style_set.active),
cx,
))
.with_child(Self::render_button(
set_index * 6 + 4,
layer_index,
"disabled",
&style_set,
Some(|style_set| &style_set.disabled),
cx,
))
.with_child(Self::render_button(
set_index * 6 + 5,
layer_index,
"inverted",
&style_set,
Some(|style_set| &style_set.inverted),
cx,
))
}
fn render_button(
button_index: usize,
layer_index: usize,
text: &'static str,
style_set: &StyleSet,
style_override: Option<fn(&StyleSet) -> &Style>,
cx: &mut RenderContext<'_, Self>,
) -> ElementBox {
enum TestBenchButton {}
MouseEventHandler::<TestBenchButton>::new(layer_index + button_index, cx, |state, cx| {
let style = if let Some(style_override) = style_override {
style_override(&style_set)
} else if state.clicked().is_some() {
&style_set.pressed
} else if state.hovered() {
&style_set.hovered
} else {
&style_set.default
};
Self::render_label(text.to_string(), style, cx)
.contained()
.with_style(ContainerStyle {
margin: Margin {
top: 4.,
bottom: 4.,
left: 4.,
right: 4.,
},
padding: Padding {
top: 4.,
bottom: 4.,
left: 4.,
right: 4.,
},
background_color: Some(style.background),
border: Border {
width: 1.,
color: style.border,
overlay: false,
top: true,
bottom: true,
left: true,
right: true,
},
corner_radius: 2.,
..Default::default()
})
.boxed()
})
.flex(1., true)
.boxed()
}
fn render_label(text: String, style: &Style, cx: &mut RenderContext<'_, Self>) -> Label {
let settings = cx.global::<Settings>();
let font_cache = cx.font_cache();
let family_id = settings.buffer_font_family;
let font_size = settings.buffer_font_size;
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let text_style = TextStyle {
color: style.foreground,
font_family_id: family_id,
font_family_name: font_cache.family_name(family_id).unwrap(),
font_id,
font_size,
font_properties: Default::default(),
underline: Default::default(),
};
Label::new(text, text_style)
}
}
impl Entity for ThemeTestbench {
type Event = ();
}
impl View for ThemeTestbench {
fn ui_name() -> &'static str {
"ThemeTestbench"
}
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
let color_scheme = &cx.global::<Settings>().theme.clone().color_scheme;
Flex::row()
.with_child(
Self::render_ramps(color_scheme)
.contained()
.with_margin_right(10.)
.flex(0.1, false)
.boxed(),
)
.with_child(
Flex::column()
.with_child(
Self::render_layer(100, &color_scheme.lowest, cx)
.flex(1., true)
.boxed(),
)
.with_child(
Self::render_layer(200, &color_scheme.middle, cx)
.flex(1., true)
.boxed(),
)
.with_child(
Self::render_layer(300, &color_scheme.highest, cx)
.flex(1., true)
.boxed(),
)
.flex(1., false)
.boxed(),
)
.boxed()
}
}
impl Item for ThemeTestbench {
fn tab_content(
&self,
_: Option<usize>,
style: &theme::Tab,
_: &gpui::AppContext,
) -> gpui::ElementBox {
Label::new("Theme Testbench".into(), style.label.clone())
.aligned()
.contained()
.boxed()
}
fn project_path(&self, _: &gpui::AppContext) -> Option<ProjectPath> {
None
}
fn project_entry_ids(&self, _: &gpui::AppContext) -> SmallVec<[ProjectEntryId; 3]> {
SmallVec::new()
}
fn is_singleton(&self, _: &gpui::AppContext) -> bool {
false
}
fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
fn can_save(&self, _: &gpui::AppContext) -> bool {
false
}
fn save(
&mut self,
_: gpui::ModelHandle<Project>,
_: &mut ViewContext<Self>,
) -> gpui::Task<gpui::anyhow::Result<()>> {
unreachable!("save should not have been called");
}
fn save_as(
&mut self,
_: gpui::ModelHandle<Project>,
_: std::path::PathBuf,
_: &mut ViewContext<Self>,
) -> gpui::Task<gpui::anyhow::Result<()>> {
unreachable!("save_as should not have been called");
}
fn reload(
&mut self,
_: gpui::ModelHandle<Project>,
_: &mut ViewContext<Self>,
) -> gpui::Task<gpui::anyhow::Result<()>> {
gpui::Task::ready(Ok(()))
}
fn to_item_events(_: &Self::Event) -> Vec<workspace::ItemEvent> {
Vec::new()
}
}

View file

@ -20,7 +20,6 @@ assets = { path = "../assets" }
auto_update = { path = "../auto_update" }
breadcrumbs = { path = "../breadcrumbs" }
call = { path = "../call" }
chat_panel = { path = "../chat_panel" }
cli = { path = "../cli" }
collab_ui = { path = "../collab_ui" }
collections = { path = "../collections" }
@ -52,6 +51,7 @@ text = { path = "../text" }
terminal = { path = "../terminal" }
theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" }
theme_testbench = { path = "../theme_testbench" }
util = { path = "../util" }
vim = { path = "../vim" }
workspace = { path = "../workspace" }

View file

@ -21,12 +21,12 @@ fn main() {
let output = Command::new("npm")
.current_dir("../../styles")
.args(["run", "build-themes"])
.args(["run", "build"])
.output()
.expect("failed to run npm");
if !output.status.success() {
panic!(
"build-themes script failed {}",
"build script failed {}",
String::from_utf8_lossy(&output.stderr)
);
}

View file

@ -116,7 +116,6 @@ fn main() {
editor::init(cx);
go_to_line::init(cx);
file_finder::init(cx);
chat_panel::init(cx);
outline::init(cx);
project_symbols::init(cx);
project_panel::init(cx);
@ -124,6 +123,7 @@ fn main() {
search::init(cx);
vim::init(cx);
terminal::init(cx);
theme_testbench::init(cx);
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
.detach();
@ -441,7 +441,7 @@ async fn watch_themes(
while (events.next().await).is_some() {
let output = Command::new("npm")
.current_dir("styles")
.args(["run", "build-themes"])
.args(["run", "build"])
.output()
.await
.log_err()?;
@ -449,7 +449,7 @@ async fn watch_themes(
cx.update(|cx| theme_selector::ThemeSelector::reload(themes.clone(), cx))
} else {
eprintln!(
"build-themes script failed {}",
"build script failed {}",
String::from_utf8_lossy(&output.stderr)
);
}

View file

@ -1 +1 @@
ZED_SERVER_URL=http://localhost:3000 cargo run $@
ZED_ADMIN_API_TOKEN=secret ZED_SERVER_URL=http://localhost:3000 cargo run $@

View file

@ -1,19 +1,18 @@
{
"name": "styles",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "npm run build-themes && npm run build-tokens",
"build-themes": "ts-node ./src/buildThemes.ts"
},
"author": "",
"license": "ISC",
"dependencies": {
"@types/chroma-js": "^2.1.3",
"@types/node": "^17.0.23",
"case-anything": "^2.1.10",
"chroma-js": "^2.4.2",
"ts-node": "^10.7.0"
}
"name": "styles",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "ts-node ./src/buildThemes.ts"
},
"author": "",
"license": "ISC",
"dependencies": {
"@types/chroma-js": "^2.1.3",
"@types/node": "^17.0.23",
"case-anything": "^2.1.10",
"chroma-js": "^2.4.2",
"ts-node": "^10.7.0"
}
}

View file

@ -2,9 +2,12 @@ import * as fs from "fs";
import * as path from "path";
import { tmpdir } from "os";
import app from "./styleTree/app";
import themes, { internalThemes, experimentalThemes } from "./themes";
import colorSchemes, {
internalColorSchemes,
experimentalColorSchemes,
} from "./colorSchemes";
import snakeCase from "./utils/snakeCase";
import Theme from "./themes/common/theme";
import { ColorScheme } from "./themes/common/colorScheme";
const themeDirectory = `${__dirname}/../../assets/themes`;
const internalDirectory = `${themeDirectory}/internal`;
@ -16,7 +19,7 @@ function clearThemes(themeDirectory: string) {
for (const file of fs.readdirSync(themeDirectory)) {
if (file.endsWith(".json")) {
const name = file.replace(/\.json$/, "");
if (!themes.find((theme) => theme.name === name)) {
if (!colorSchemes.find((colorScheme) => colorScheme.name === name)) {
fs.unlinkSync(path.join(themeDirectory, file));
}
}
@ -27,12 +30,12 @@ clearThemes(themeDirectory);
clearThemes(internalDirectory);
clearThemes(experimentsDirectory);
function writeThemes(themes: Theme[], outputDirectory: string) {
for (let theme of themes) {
let styleTree = snakeCase(app(theme));
function writeThemes(colorSchemes: ColorScheme[], outputDirectory: string) {
for (let colorScheme of colorSchemes) {
let styleTree = snakeCase(app(colorScheme));
let styleTreeJSON = JSON.stringify(styleTree, null, 2);
let tempPath = path.join(tempDirectory, `${theme.name}.json`);
let outPath = path.join(outputDirectory, `${theme.name}.json`);
let tempPath = path.join(tempDirectory, `${colorScheme.name}.json`);
let outPath = path.join(outputDirectory, `${colorScheme.name}.json`);
fs.writeFileSync(tempPath, styleTreeJSON);
fs.renameSync(tempPath, outPath);
console.log(`- ${outPath} created`);
@ -40,6 +43,6 @@ function writeThemes(themes: Theme[], outputDirectory: string) {
}
// Write new themes to theme directory
writeThemes(themes, themeDirectory);
writeThemes(internalThemes, internalDirectory);
writeThemes(experimentalThemes, experimentsDirectory);
writeThemes(colorSchemes, themeDirectory);
writeThemes(internalColorSchemes, internalDirectory);
writeThemes(experimentalColorSchemes, experimentsDirectory);

View file

@ -0,0 +1,35 @@
import fs from "fs";
import path from "path";
import { ColorScheme } from "./themes/common/colorScheme";
const colorSchemes: ColorScheme[] = [];
export default colorSchemes;
const internalColorSchemes: ColorScheme[] = [];
export { internalColorSchemes };
const experimentalColorSchemes: ColorScheme[] = [];
export { experimentalColorSchemes };
function fillColorSchemes(themesPath: string, colorSchemes: ColorScheme[]) {
for (const fileName of fs.readdirSync(themesPath)) {
if (fileName == "template.ts") continue;
const filePath = path.join(themesPath, fileName);
if (fs.statSync(filePath).isFile()) {
const colorScheme = require(filePath);
if (colorScheme.dark) colorSchemes.push(colorScheme.dark);
if (colorScheme.light) colorSchemes.push(colorScheme.light);
}
}
}
fillColorSchemes(path.resolve(`${__dirname}/themes`), colorSchemes);
fillColorSchemes(
path.resolve(`${__dirname}/themes/internal`),
internalColorSchemes
);
fillColorSchemes(
path.resolve(`${__dirname}/themes/experiments`),
experimentalColorSchemes
);

View file

@ -1,5 +1,3 @@
import Theme from "../themes/common/theme";
import chatPanel from "./chatPanel";
import { text } from "./components";
import contactFinder from "./contactFinder";
import contactsPopover from "./contactsPopover";
@ -18,40 +16,51 @@ import tooltip from "./tooltip";
import terminal from "./terminal";
import contactList from "./contactList";
import incomingCallNotification from "./incomingCallNotification";
import { ColorScheme } from "../themes/common/colorScheme";
export const panel = {
padding: { top: 12, bottom: 12 },
};
export default function app(theme: Theme): Object {
export default function app(colorScheme: ColorScheme): Object {
return {
meta: {
name: theme.name,
isLight: theme.isLight
name: colorScheme.name,
isLight: colorScheme.isLight,
},
picker: picker(theme),
workspace: workspace(theme),
contextMenu: contextMenu(theme),
editor: editor(theme),
projectDiagnostics: projectDiagnostics(theme),
commandPalette: commandPalette(theme),
projectPanel: projectPanel(theme),
chatPanel: chatPanel(theme),
contactsPopover: contactsPopover(theme),
contactList: contactList(theme),
contactFinder: contactFinder(theme),
search: search(theme),
commandPalette: commandPalette(colorScheme),
contactNotification: contactNotification(colorScheme),
projectSharedNotification: projectSharedNotification(colorScheme),
incomingCallNotification: incomingCallNotification(colorScheme),
picker: picker(colorScheme),
workspace: workspace(colorScheme),
contextMenu: contextMenu(colorScheme),
editor: editor(colorScheme),
projectDiagnostics: projectDiagnostics(colorScheme),
projectPanel: projectPanel(colorScheme),
contactsPopover: contactsPopover(colorScheme),
contactFinder: contactFinder(colorScheme),
contactList: contactList(colorScheme),
search: search(colorScheme),
breadcrumbs: {
...text(theme, "sans", "secondary"),
...text(colorScheme.highest, "sans", "variant"),
padding: {
left: 6,
},
},
contactNotification: contactNotification(theme),
updateNotification: updateNotification(theme),
projectSharedNotification: projectSharedNotification(theme),
incomingCallNotification: incomingCallNotification(theme),
tooltip: tooltip(theme),
terminal: terminal(theme),
updateNotification: updateNotification(colorScheme),
tooltip: tooltip(colorScheme),
terminal: terminal(colorScheme),
colorScheme: {
...colorScheme,
players: Object.values(colorScheme.players),
ramps: {
neutral: colorScheme.ramps.neutral.colors(100, "hex"),
red: colorScheme.ramps.red.colors(100, "hex"),
orange: colorScheme.ramps.orange.colors(100, "hex"),
yellow: colorScheme.ramps.yellow.colors(100, "hex"),
green: colorScheme.ramps.green.colors(100, "hex"),
cyan: colorScheme.ramps.cyan.colors(100, "hex"),
blue: colorScheme.ramps.blue.colors(100, "hex"),
violet: colorScheme.ramps.violet.colors(100, "hex"),
magenta: colorScheme.ramps.magenta.colors(100, "hex"),
},
},
};
}

View file

@ -1,108 +0,0 @@
import Theme from "../themes/common/theme";
import { panel } from "./app";
import {
backgroundColor,
border,
player,
text,
TextColor,
popoverShadow,
} from "./components";
export default function chatPanel(theme: Theme) {
function channelSelectItem(
theme: Theme,
textColor: TextColor,
hovered: boolean
) {
return {
name: text(theme, "sans", textColor),
padding: 4,
hash: {
...text(theme, "sans", "muted"),
margin: {
right: 8,
},
},
background: hovered ? backgroundColor(theme, 300, "hovered") : undefined,
cornerRadius: hovered ? 6 : 0,
};
}
const message = {
body: text(theme, "sans", "secondary"),
timestamp: text(theme, "sans", "muted", { size: "sm" }),
padding: {
bottom: 6,
},
sender: {
...text(theme, "sans", "primary", { weight: "bold" }),
margin: {
right: 8,
},
},
};
return {
...panel,
channelName: text(theme, "sans", "primary", { weight: "bold" }),
channelNameHash: {
...text(theme, "sans", "muted"),
padding: {
right: 8,
},
},
channelSelect: {
header: {
...channelSelectItem(theme, "primary", false),
padding: {
bottom: 4,
left: 0,
},
},
item: channelSelectItem(theme, "secondary", false),
hoveredItem: channelSelectItem(theme, "secondary", true),
activeItem: channelSelectItem(theme, "primary", false),
hoveredActiveItem: channelSelectItem(theme, "primary", true),
menu: {
background: backgroundColor(theme, 500),
cornerRadius: 6,
padding: 4,
border: border(theme, "primary"),
shadow: popoverShadow(theme),
},
},
signInPrompt: text(theme, "sans", "secondary", { underline: true }),
hoveredSignInPrompt: text(theme, "sans", "primary", { underline: true }),
message,
pendingMessage: {
...message,
body: {
...message.body,
color: theme.textColor.muted,
},
sender: {
...message.sender,
color: theme.textColor.muted,
},
timestamp: {
...message.timestamp,
color: theme.textColor.muted,
},
},
inputEditor: {
background: backgroundColor(theme, 500),
cornerRadius: 6,
text: text(theme, "mono", "primary"),
placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
selection: player(theme, 1).selection,
border: border(theme, "secondary"),
padding: {
bottom: 7,
left: 8,
right: 8,
top: 7,
},
},
};
}

View file

@ -1,25 +1,29 @@
import Theme from "../themes/common/theme";
import { text, backgroundColor, border } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { withOpacity } from "../utils/color";
import { text, background } from "./components";
export default function commandPalette(theme: Theme) {
export default function commandPalette(colorScheme: ColorScheme) {
let layer = colorScheme.highest;
return {
keystrokeSpacing: 8,
key: {
text: text(theme, "mono", "secondary", { size: "xs" }),
cornerRadius: 4,
background: backgroundColor(theme, "on300"),
border: border(theme, "secondary"),
text: text(layer, "mono", "variant", "default", { size: "xs" }),
cornerRadius: 2,
background: background(layer, "on"),
padding: {
top: 2,
bottom: 2,
left: 8,
right: 8,
top: 1,
bottom: 1,
left: 6,
right: 6,
},
margin: {
top: 1,
bottom: 1,
left: 2,
},
active: {
text: text(theme, "mono", "active", { size: "xs" }),
text: text(layer, "mono", "on", "default", { size: "xs" }),
background: withOpacity(background(layer, "on"), 0.2),
},
},
};

View file

@ -1,31 +1,157 @@
import Theme, { BackgroundColorSet } from "../themes/common/theme";
import { fontFamilies, fontSizes, FontWeight } from "../common";
import { Layer, Styles, StyleSets, Style } from "../themes/common/colorScheme";
export type TextColor = keyof Theme["textColor"];
export function text(
theme: Theme,
fontFamily: keyof typeof fontFamilies,
color: TextColor,
properties?: {
size?: keyof typeof fontSizes;
weight?: FontWeight;
underline?: boolean;
function isStyleSet(key: any): key is StyleSets {
return [
"base",
"variant",
"on",
"accent",
"positive",
"warning",
"negative",
].includes(key);
}
function isStyle(key: any): key is Styles {
return ["default", "active", "disabled", "hovered", "pressed", "inverted"].includes(key);
}
function getStyle(
layer: Layer,
possibleStyleSetOrStyle?: any,
possibleStyle?: any
): Style {
let styleSet: StyleSets = "base";
let style: Styles = "default";
if (isStyleSet(possibleStyleSetOrStyle)) {
styleSet = possibleStyleSetOrStyle;
} else if (isStyle(possibleStyleSetOrStyle)) {
style = possibleStyleSetOrStyle;
}
if (isStyle(possibleStyle)) {
style = possibleStyle;
}
return layer[styleSet][style];
}
export function background(layer: Layer, style?: Styles): string;
export function background(
layer: Layer,
styleSet?: StyleSets,
style?: Styles
): string;
export function background(
layer: Layer,
styleSetOrStyles?: StyleSets | Styles,
style?: Styles
): string {
return getStyle(layer, styleSetOrStyles, style).background;
}
export function borderColor(layer: Layer, style?: Styles): string;
export function borderColor(
layer: Layer,
styleSet?: StyleSets,
style?: Styles
): string;
export function borderColor(
layer: Layer,
styleSetOrStyles?: StyleSets | Styles,
style?: Styles
): string {
return getStyle(layer, styleSetOrStyles, style).border;
}
export function foreground(layer: Layer, style?: Styles): string;
export function foreground(
layer: Layer,
styleSet?: StyleSets,
style?: Styles
): string;
export function foreground(
layer: Layer,
styleSetOrStyles?: StyleSets | Styles,
style?: Styles
): string {
return getStyle(layer, styleSetOrStyles, style).foreground;
}
interface Text {
family: keyof typeof fontFamilies;
color: string;
size: number;
weight?: FontWeight;
underline?: boolean;
}
interface TextProperties {
size?: keyof typeof fontSizes;
weight?: FontWeight;
underline?: boolean;
}
export function text(
layer: Layer,
fontFamily: keyof typeof fontFamilies,
styleSet: StyleSets,
style: Styles,
properties?: TextProperties
): Text;
export function text(
layer: Layer,
fontFamily: keyof typeof fontFamilies,
styleSet: StyleSets,
properties?: TextProperties
): Text;
export function text(
layer: Layer,
fontFamily: keyof typeof fontFamilies,
style: Styles,
properties?: TextProperties
): Text;
export function text(
layer: Layer,
fontFamily: keyof typeof fontFamilies,
properties?: TextProperties
): Text;
export function text(
layer: Layer,
fontFamily: keyof typeof fontFamilies,
styleSetStyleOrProperties?: StyleSets | Styles | TextProperties,
styleOrProperties?: Styles | TextProperties,
properties?: TextProperties
) {
let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties);
if (typeof styleSetStyleOrProperties === "object") {
properties = styleSetStyleOrProperties;
}
if (typeof styleOrProperties === "object") {
properties = styleOrProperties;
}
let size = fontSizes[properties?.size || "sm"];
return {
family: fontFamilies[fontFamily],
color: theme.textColor[color],
color: style.foreground,
...properties,
size,
};
}
export function textColor(theme: Theme, color: TextColor) {
return theme.textColor[color];
export interface Border {
color: string;
width: number;
top?: boolean;
bottom?: boolean;
left?: boolean;
right?: boolean;
overlay?: boolean;
}
export type BorderColor = keyof Theme["borderColor"];
export interface BorderOptions {
export interface BorderProperties {
width?: number;
top?: boolean;
bottom?: boolean;
@ -33,72 +159,42 @@ export interface BorderOptions {
right?: boolean;
overlay?: boolean;
}
export function border(
theme: Theme,
color: BorderColor,
options?: BorderOptions
) {
layer: Layer,
styleSet: StyleSets,
style: Styles,
properties?: BorderProperties
): Border;
export function border(
layer: Layer,
styleSet: StyleSets,
properties?: BorderProperties
): Border;
export function border(
layer: Layer,
style: Styles,
properties?: BorderProperties
): Border;
export function border(layer: Layer, properties?: BorderProperties): Border;
export function border(
layer: Layer,
styleSetStyleOrProperties?: StyleSets | Styles | BorderProperties,
styleOrProperties?: Styles | BorderProperties,
properties?: BorderProperties
): Border {
let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties);
if (typeof styleSetStyleOrProperties === "object") {
properties = styleSetStyleOrProperties;
}
if (typeof styleOrProperties === "object") {
properties = styleOrProperties;
}
return {
color: borderColor(theme, color),
color: style.border,
width: 1,
...options,
};
}
export function borderColor(theme: Theme, color: BorderColor) {
return theme.borderColor[color];
}
export type IconColor = keyof Theme["iconColor"];
export function iconColor(theme: Theme, color: IconColor) {
return theme.iconColor[color];
}
export type PlayerIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export interface Player {
selection: {
cursor: string;
selection: string;
};
}
export function player(theme: Theme, playerNumber: PlayerIndex): Player {
return {
selection: {
cursor: theme.player[playerNumber].cursorColor,
selection: theme.player[playerNumber].selectionColor,
},
};
}
export type BackgroundColor = keyof Theme["backgroundColor"];
export type BackgroundState = keyof BackgroundColorSet;
export function backgroundColor(
theme: Theme,
name: BackgroundColor,
state?: BackgroundState
): string {
return theme.backgroundColor[name][state || "base"];
}
export function modalShadow(theme: Theme) {
return {
blur: 16,
color: theme.shadow,
offset: [0, 2],
};
}
export function popoverShadow(theme: Theme) {
return {
blur: 4,
color: theme.shadow,
offset: [1, 2],
};
}
export function draggedShadow(theme: Theme) {
return {
blur: 6,
color: theme.shadow,
offset: [1, 2],
...properties,
};
}

View file

@ -1,12 +1,14 @@
import Theme from "../themes/common/theme";
import picker from "./picker";
import { backgroundColor, border, iconColor, player, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, foreground, text } from "./components";
export default function contactFinder(colorScheme: ColorScheme) {
let layer = colorScheme.highest;
export default function contactFinder(theme: Theme) {
const sideMargin = 6;
const contactButton = {
background: backgroundColor(theme, 100),
color: iconColor(theme, "primary"),
background: background(layer, "variant"),
color: foreground(layer, "variant"),
iconWidth: 8,
buttonWidth: 16,
cornerRadius: 8,
@ -15,17 +17,17 @@ export default function contactFinder(theme: Theme) {
return {
picker: {
item: {
...picker(theme).item,
...picker(colorScheme).item,
margin: { left: sideMargin, right: sideMargin }
},
empty: picker(theme).empty,
empty: picker(colorScheme).empty,
inputEditor: {
background: backgroundColor(theme, 500),
background: background(layer, "on"),
cornerRadius: 6,
text: text(theme, "mono", "primary"),
placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
selection: player(theme, 1).selection,
border: border(theme, "secondary"),
text: text(layer, "mono",),
placeholderText: text(layer, "mono", "variant", { size: "sm" }),
selection: colorScheme.players[0],
border: border(layer),
padding: {
bottom: 4,
left: 8,
@ -51,13 +53,13 @@ export default function contactFinder(theme: Theme) {
contactButton: {
...contactButton,
hover: {
background: backgroundColor(theme, 100, "hovered"),
background: background(layer, "variant", "hovered"),
},
},
disabledContactButton: {
...contactButton,
background: backgroundColor(theme, 100),
color: iconColor(theme, "muted"),
background: background(layer, "disabled"),
color: foreground(layer, "disabled"),
},
};
}

View file

@ -1,13 +1,21 @@
import Theme from "../themes/common/theme";
import { backgroundColor, border, borderColor, iconColor, player, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import {
background,
border,
borderColor,
foreground,
text,
} from "./components";
export default function contactList(theme: Theme) {
export default function contactsPanel(colorScheme: ColorScheme) {
const nameMargin = 8;
const sidePadding = 12;
let layer = colorScheme.middle;
const contactButton = {
background: backgroundColor(theme, 100),
color: iconColor(theme, "primary"),
background: background(layer, "on"),
color: foreground(layer, "on"),
iconWidth: 8,
buttonWidth: 16,
cornerRadius: 8,
@ -20,7 +28,7 @@ export default function contactList(theme: Theme) {
width: 14,
},
name: {
...text(theme, "mono", "placeholder", { size: "sm" }),
...text(layer, "mono", { size: "sm" }),
margin: {
left: nameMargin,
right: 6,
@ -39,13 +47,15 @@ export default function contactList(theme: Theme) {
};
return {
background: background(layer),
padding: { top: 12, bottom: 0 },
userQueryEditor: {
background: backgroundColor(theme, 500),
background: background(layer, "on"),
cornerRadius: 6,
text: text(theme, "mono", "primary"),
placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
selection: player(theme, 1).selection,
border: border(theme, "secondary"),
text: text(layer, "mono", "on"),
placeholderText: text(layer, "mono", "on", "disabled", { size: "sm" }),
selection: colorScheme.players[0],
border: border(layer, "on"),
padding: {
bottom: 4,
left: 8,
@ -58,27 +68,28 @@ export default function contactList(theme: Theme) {
},
userQueryEditorHeight: 33,
addContactButton: {
color: iconColor(theme, "primary"),
margin: { left: 6, right: 12 },
color: foreground(layer, "on"),
buttonWidth: 28,
iconWidth: 16,
},
rowHeight: 28,
sectionIconSize: 8,
headerRow: {
...text(theme, "mono", "secondary", { size: "sm" }),
margin: { top: 6 },
...text(layer, "mono", { size: "sm" }),
margin: { top: 14 },
padding: {
left: sidePadding,
right: sidePadding,
},
active: {
...text(theme, "mono", "primary", { size: "sm" }),
background: backgroundColor(theme, 100, "active"),
...text(layer, "mono", "active", { size: "sm" }),
background: background(layer, "active"),
},
},
leaveCall: {
background: backgroundColor(theme, 100),
border: border(theme, "secondary"),
background: background(layer),
border: border(layer),
cornerRadius: 6,
margin: {
top: 1,
@ -89,11 +100,11 @@ export default function contactList(theme: Theme) {
left: 7,
right: 7,
},
...text(theme, "sans", "secondary", { size: "xs" }),
...text(layer, "sans", "variant", { size: "xs" }),
hover: {
...text(theme, "sans", "active", { size: "xs" }),
background: backgroundColor(theme, "on300", "hovered"),
border: border(theme, "primary"),
...text(layer, "sans", "hovered", { size: "xs" }),
background: background(layer, "hovered"),
border: border(layer, "hovered"),
},
},
contactRow: {
@ -102,7 +113,7 @@ export default function contactList(theme: Theme) {
right: sidePadding,
},
active: {
background: backgroundColor(theme, 100, "active"),
background: background(layer, "active"),
},
},
contactAvatar: {
@ -113,16 +124,16 @@ export default function contactList(theme: Theme) {
cornerRadius: 4,
padding: 4,
margin: { top: 12, left: 12 },
background: iconColor(theme, "ok"),
background: foreground(layer, "positive"),
},
contactStatusBusy: {
cornerRadius: 4,
padding: 4,
margin: { top: 12, left: 12 },
background: iconColor(theme, "error"),
background: foreground(layer, "negative"),
},
contactUsername: {
...text(theme, "mono", "primary", { size: "sm" }),
...text(layer, "mono", { size: "sm" }),
margin: {
left: nameMargin,
},
@ -131,39 +142,39 @@ export default function contactList(theme: Theme) {
contactButton: {
...contactButton,
hover: {
background: backgroundColor(theme, "on300", "hovered"),
background: background(layer, "hovered"),
},
},
disabledButton: {
...contactButton,
background: backgroundColor(theme, 100),
color: iconColor(theme, "muted"),
background: background(layer, "on"),
color: foreground(layer, "on"),
},
callingIndicator: {
...text(theme, "mono", "muted", { size: "xs" })
...text(layer, "mono", "variant", { size: "xs" })
},
treeBranch: {
color: borderColor(theme, "active"),
color: borderColor(layer),
width: 1,
hover: {
color: borderColor(theme, "active"),
color: borderColor(layer),
},
active: {
color: borderColor(theme, "active"),
color: borderColor(layer),
},
},
projectRow: {
...projectRow,
background: backgroundColor(theme, 300),
background: background(layer, "on"),
name: {
...projectRow.name,
...text(theme, "mono", "secondary", { size: "sm" }),
...text(layer, "mono", { size: "sm" }),
},
hover: {
background: backgroundColor(theme, 300, "hovered"),
background: background(layer, "on", "hovered"),
},
active: {
background: backgroundColor(theme, 300, "active"),
background: background(layer, "on", "active"),
},
},
}

View file

@ -1,10 +1,11 @@
import Theme from "../themes/common/theme";
import { backgroundColor, iconColor, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, foreground, text } from "./components";
const avatarSize = 12;
const headerPadding = 8;
export default function contactNotification(theme: Theme): Object {
export default function contactNotification(colorScheme: ColorScheme): Object {
let layer = colorScheme.lowest;
return {
headerAvatar: {
height: avatarSize,
@ -12,32 +13,32 @@ export default function contactNotification(theme: Theme): Object {
cornerRadius: 6,
},
headerMessage: {
...text(theme, "sans", "primary", { size: "xs" }),
...text(layer, "sans", { size: "xs" }),
margin: { left: headerPadding, right: headerPadding },
},
headerHeight: 18,
bodyMessage: {
...text(theme, "sans", "secondary", { size: "xs" }),
...text(layer, "sans", { size: "xs" }),
margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
},
button: {
...text(theme, "sans", "primary", { size: "xs" }),
background: backgroundColor(theme, "on300"),
...text(layer, "sans", "on", { size: "xs" }),
background: background(layer, "on"),
padding: 4,
cornerRadius: 6,
margin: { left: 6 },
hover: {
background: backgroundColor(theme, "on300", "hovered"),
background: background(layer, "on", "hovered"),
},
},
dismissButton: {
color: iconColor(theme, "secondary"),
color: foreground(layer, "on"),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
hover: {
color: iconColor(theme, "primary"),
color: foreground(layer, "on", "hovered"),
},
},
};

View file

@ -1,15 +1,16 @@
import Theme from "../themes/common/theme";
import { backgroundColor, border, borderColor, popoverShadow, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, text } from "./components";
export default function contactsPopover(theme: Theme) {
export default function contactsPopover(colorScheme: ColorScheme) {
let layer = colorScheme.middle;
const sidePadding = 12;
return {
background: backgroundColor(theme, 300, "base"),
background: background(layer),
cornerRadius: 6,
padding: { top: 6 },
margin: { top: -6 },
shadow: popoverShadow(theme),
border: border(theme, "primary"),
shadow: colorScheme.popoverShadow,
border: border(layer),
width: 300,
height: 400,
inviteRowHeight: 28,
@ -18,10 +19,10 @@ export default function contactsPopover(theme: Theme) {
left: sidePadding,
right: sidePadding,
},
border: { top: true, width: 1, color: borderColor(theme, "primary") },
text: text(theme, "sans", "secondary", { size: "sm" }),
border: border(layer, { top: true }),
text: text(layer, "sans", "variant", { size: "sm" }),
hover: {
text: text(theme, "sans", "active", { size: "sm" }),
text: text(layer, "sans", "hovered", { size: "sm" }),
},
},
}

View file

@ -1,45 +1,40 @@
import Theme from "../themes/common/theme";
import {
backgroundColor,
border,
borderColor,
popoverShadow,
text,
} from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, borderColor, text } from "./components";
export default function contextMenu(theme: Theme) {
export default function contextMenu(colorScheme: ColorScheme) {
let layer = colorScheme.middle;
return {
background: backgroundColor(theme, 300, "base"),
cornerRadius: 6,
padding: 6,
shadow: popoverShadow(theme),
border: border(theme, "primary"),
background: background(layer),
cornerRadius: 10,
padding: 4,
shadow: colorScheme.popoverShadow,
border: border(layer),
keystrokeMargin: 30,
item: {
iconSpacing: 8,
iconWidth: 14,
padding: { left: 4, right: 4, top: 2, bottom: 2 },
padding: { left: 6, right: 6, top: 2, bottom: 2 },
cornerRadius: 6,
label: text(theme, "sans", "primary", { size: "sm" }),
label: text(layer, "sans", { size: "sm" }),
keystroke: {
...text(theme, "sans", "muted", { size: "sm", weight: "bold" }),
...text(layer, "sans", "variant", { size: "sm", weight: "bold" }),
padding: { left: 3, right: 3 },
},
hover: {
background: backgroundColor(theme, 300, "hovered"),
text: text(theme, "sans", "primary", { size: "sm" }),
background: background(layer, "hovered"),
label: text(layer, "sans", "hovered", { size: "sm" }),
},
active: {
background: backgroundColor(theme, 300, "active"),
text: text(theme, "sans", "active", { size: "sm" }),
background: background(layer, "active"),
label: text(layer, "sans", "active", { size: "sm" }),
},
activeHover: {
background: backgroundColor(theme, 300, "hovered"),
text: text(theme, "sans", "active", { size: "sm" }),
background: background(layer, "active"),
label: text(layer, "sans", "active", { size: "sm" }),
},
},
separator: {
background: borderColor(theme, "primary"),
background: borderColor(layer),
margin: { top: 2, bottom: 2 },
},
};

View file

@ -1,19 +1,22 @@
import Theme from "../themes/common/theme";
import { fontWeights } from "../common";
import { withOpacity } from "../utils/color";
import {
backgroundColor,
ColorScheme,
Layer,
StyleSets,
} from "../themes/common/colorScheme";
import {
background,
border,
borderColor,
iconColor,
player,
popoverShadow,
foreground,
text,
textColor,
TextColor,
} from "./components";
import hoverPopover from "./hoverPopover";
export default function editor(theme: Theme) {
export default function editor(colorScheme: ColorScheme) {
let layer = colorScheme.highest;
const autocompleteItem = {
cornerRadius: 6,
padding: {
@ -24,17 +27,17 @@ export default function editor(theme: Theme) {
},
};
function diagnostic(theme: Theme, color: TextColor) {
function diagnostic(layer: Layer, styleSet: StyleSets) {
return {
textScaleFactor: 0.857,
header: {
border: border(theme, "primary", {
border: border(layer, {
top: true,
}),
},
message: {
text: text(theme, "sans", color, { size: "sm" }),
highlightText: text(theme, "sans", color, {
text: text(layer, "sans", styleSet, "inverted", { size: "sm" }),
highlightText: text(layer, "sans", styleSet, "inverted", {
size: "sm",
weight: "bold",
}),
@ -42,121 +45,207 @@ export default function editor(theme: Theme) {
};
}
const syntax: any = {};
for (const syntaxKey in theme.syntax) {
const style = theme.syntax[syntaxKey];
syntax[syntaxKey] = {
color: style.color,
weight: style.weight,
underline: style.underline,
italic: style.italic,
};
}
const syntax = {
primary: {
color: colorScheme.ramps.neutral(1).hex(),
weight: fontWeights.normal,
},
comment: {
color: colorScheme.ramps.neutral(0.71).hex(),
weight: fontWeights.normal,
},
punctuation: {
color: colorScheme.ramps.neutral(0.86).hex(),
weight: fontWeights.normal,
},
constant: {
color: colorScheme.ramps.green(0.5).hex(),
weight: fontWeights.normal,
},
keyword: {
color: colorScheme.ramps.blue(0.5).hex(),
weight: fontWeights.normal,
},
function: {
color: colorScheme.ramps.yellow(0.5).hex(),
weight: fontWeights.normal,
},
type: {
color: colorScheme.ramps.cyan(0.5).hex(),
weight: fontWeights.normal,
},
constructor: {
color: colorScheme.ramps.blue(0.5).hex(),
weight: fontWeights.normal,
},
variant: {
color: colorScheme.ramps.blue(0.5).hex(),
weight: fontWeights.normal,
},
property: {
color: colorScheme.ramps.blue(0.5).hex(),
weight: fontWeights.normal,
},
enum: {
color: colorScheme.ramps.orange(0.5).hex(),
weight: fontWeights.normal,
},
operator: {
color: colorScheme.ramps.orange(0.5).hex(),
weight: fontWeights.normal,
},
string: {
color: colorScheme.ramps.orange(0.5).hex(),
weight: fontWeights.normal,
},
number: {
color: colorScheme.ramps.green(0.5).hex(),
weight: fontWeights.normal,
},
boolean: {
color: colorScheme.ramps.green(0.5).hex(),
weight: fontWeights.normal,
},
predictive: {
color: colorScheme.ramps.neutral(0.57).hex(),
weight: fontWeights.normal,
},
title: {
color: colorScheme.ramps.yellow(0.5).hex(),
weight: fontWeights.bold,
},
emphasis: {
color: colorScheme.ramps.blue(0.5).hex(),
weight: fontWeights.normal,
},
"emphasis.strong": {
color: colorScheme.ramps.blue(0.5).hex(),
weight: fontWeights.bold,
},
linkUri: {
color: colorScheme.ramps.green(0.5).hex(),
weight: fontWeights.normal,
underline: true,
},
linkText: {
color: colorScheme.ramps.orange(0.5).hex(),
weight: fontWeights.normal,
italic: true,
},
};
return {
textColor: theme.syntax.primary.color,
background: backgroundColor(theme, 500),
activeLineBackground: theme.editor.line.active,
textColor: syntax.primary.color,
background: background(layer),
activeLineBackground: background(layer, "on"),
highlightedLineBackground: background(layer, "on"),
codeActions: {
indicator: iconColor(theme, "secondary"),
verticalScale: 0.618
indicator: foreground(layer, "variant"),
verticalScale: 0.55,
},
diff: {
deleted: theme.iconColor.error,
inserted: theme.iconColor.ok,
modified: theme.iconColor.warning,
deleted: foreground(layer, "negative"),
modified: foreground(layer, "warning"),
inserted: foreground(layer, "positive"),
removedWidthEm: 0.275,
widthEm: 0.16,
cornerRadius: 0.05,
},
documentHighlightReadBackground: theme.editor.highlight.occurrence,
documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence,
errorColor: theme.textColor.error,
gutterBackground: backgroundColor(theme, 500),
documentHighlightReadBackground: colorScheme.ramps
.neutral(0.5)
.alpha(0.2)
.hex(), // TODO: This was blend
documentHighlightWriteBackground: colorScheme.ramps
.neutral(0.5)
.alpha(0.4)
.hex(), // TODO: This was blend * 2
errorColor: background(layer, "negative"),
gutterBackground: background(layer),
gutterPaddingFactor: 3.5,
highlightedLineBackground: theme.editor.line.highlighted,
lineNumber: theme.editor.gutter.primary,
lineNumberActive: theme.editor.gutter.active,
lineNumber: foreground(layer, "disabled"),
lineNumberActive: foreground(layer),
renameFade: 0.6,
unnecessaryCodeFade: 0.5,
selection: player(theme, 1).selection,
selection: colorScheme.players[0],
guestSelections: [
player(theme, 2).selection,
player(theme, 3).selection,
player(theme, 4).selection,
player(theme, 5).selection,
player(theme, 6).selection,
player(theme, 7).selection,
player(theme, 8).selection,
colorScheme.players[1],
colorScheme.players[2],
colorScheme.players[3],
colorScheme.players[4],
colorScheme.players[5],
colorScheme.players[6],
colorScheme.players[7],
],
autocomplete: {
background: backgroundColor(theme, 500),
background: background(colorScheme.middle),
cornerRadius: 8,
padding: 4,
border: border(theme, "secondary"),
shadow: popoverShadow(theme),
item: autocompleteItem,
hoveredItem: {
...autocompleteItem,
background: backgroundColor(theme, 500, "hovered"),
},
margin: {
left: -14,
},
matchHighlight: text(theme, "mono", "feature"),
border: border(colorScheme.middle),
shadow: colorScheme.popoverShadow,
matchHighlight: foreground(colorScheme.middle, "accent"),
item: autocompleteItem,
hoveredItem: {
...autocompleteItem,
matchHighlight: foreground(colorScheme.middle, "accent", "hovered"),
background: background(colorScheme.middle, "hovered"),
},
selectedItem: {
...autocompleteItem,
background: backgroundColor(theme, 500, "active"),
matchHighlight: foreground(colorScheme.middle, "accent", "active"),
background: background(colorScheme.middle, "active"),
},
},
diagnosticHeader: {
background: backgroundColor(theme, 300),
background: background(colorScheme.middle),
iconWidthFactor: 1.5,
textScaleFactor: 0.857, // NateQ: Will we need dynamic sizing for text? If so let's create tokens for these.
border: border(theme, "secondary", {
textScaleFactor: 0.857,
border: border(colorScheme.middle, {
bottom: true,
top: true,
}),
code: {
...text(theme, "mono", "secondary", { size: "sm" }),
...text(colorScheme.middle, "mono", { size: "sm" }),
margin: {
left: 10,
},
},
message: {
highlightText: text(theme, "sans", "primary", {
highlightText: text(colorScheme.middle, "sans", {
size: "sm",
weight: "bold",
}),
text: text(theme, "sans", "secondary", { size: "sm" }),
text: text(colorScheme.middle, "sans", { size: "sm" }),
},
},
diagnosticPathHeader: {
background: theme.editor.line.active,
background: background(colorScheme.middle),
textScaleFactor: 0.857,
filename: text(theme, "mono", "primary", { size: "sm" }),
filename: text(colorScheme.middle, "mono", { size: "sm" }),
path: {
...text(theme, "mono", "muted", { size: "sm" }),
...text(colorScheme.middle, "mono", { size: "sm" }),
margin: {
left: 12,
},
},
},
errorDiagnostic: diagnostic(theme, "error"),
warningDiagnostic: diagnostic(theme, "warning"),
informationDiagnostic: diagnostic(theme, "info"),
hintDiagnostic: diagnostic(theme, "info"),
invalidErrorDiagnostic: diagnostic(theme, "secondary"),
invalidHintDiagnostic: diagnostic(theme, "secondary"),
invalidInformationDiagnostic: diagnostic(theme, "secondary"),
invalidWarningDiagnostic: diagnostic(theme, "secondary"),
hoverPopover: hoverPopover(theme),
errorDiagnostic: diagnostic(colorScheme.middle, "negative"),
warningDiagnostic: diagnostic(colorScheme.middle, "warning"),
informationDiagnostic: diagnostic(colorScheme.middle, "accent"),
hintDiagnostic: diagnostic(colorScheme.middle, "warning"),
invalidErrorDiagnostic: diagnostic(colorScheme.middle, "base"),
invalidHintDiagnostic: diagnostic(colorScheme.middle, "base"),
invalidInformationDiagnostic: diagnostic(colorScheme.middle, "base"),
invalidWarningDiagnostic: diagnostic(colorScheme.middle, "base"),
hoverPopover: hoverPopover(colorScheme),
linkDefinition: {
color: theme.syntax.linkUri.color,
underline: theme.syntax.linkUri.underline,
color: syntax.linkUri.color,
underline: syntax.linkUri.underline,
},
jumpIcon: {
color: iconColor(theme, "secondary"),
color: foreground(layer, "on"),
iconWidth: 20,
buttonWidth: 20,
cornerRadius: 6,
@ -167,32 +256,28 @@ export default function editor(theme: Theme) {
right: 6,
},
hover: {
color: iconColor(theme, "active"),
background: backgroundColor(theme, "on500"),
color: foreground(layer, "on", "hovered"),
background: background(layer, "on", "hovered"),
},
},
scrollbar: {
width: 12,
minHeightFactor: 1.0,
track: {
border: {
left: true,
width: 1,
color: borderColor(theme, "secondary"),
},
border: border(layer, "variant", { left: true }),
},
thumb: {
background: withOpacity(borderColor(theme, "secondary"), 0.5),
background: withOpacity(borderColor(layer, "variant"), 0.5),
border: {
width: 1,
color: withOpacity(borderColor(theme, 'muted'), 0.5),
color: withOpacity(borderColor(layer, 'variant'), 0.5),
}
}
},
compositionMark: {
underline: {
thickness: 1.0,
color: borderColor(theme, "active")
color: borderColor(layer),
},
},
syntax,

View file

@ -1,18 +1,19 @@
import Theme from "../themes/common/theme";
import { backgroundColor, border, popoverShadow, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, text } from "./components";
export default function HoverPopover(theme: Theme) {
export default function HoverPopover(colorScheme: ColorScheme) {
let layer = colorScheme.middle;
let baseContainer = {
background: backgroundColor(theme, "on500"),
background: background(layer),
cornerRadius: 8,
padding: {
left: 8,
right: 8,
top: 4,
bottom: 4
bottom: 4,
},
shadow: popoverShadow(theme),
border: border(theme, "secondary"),
shadow: colorScheme.popoverShadow,
border: border(layer),
margin: {
left: -8,
},
@ -22,32 +23,23 @@ export default function HoverPopover(theme: Theme) {
container: baseContainer,
infoContainer: {
...baseContainer,
background: backgroundColor(theme, "on500Info"),
border: {
color: theme.ramps.blue(0).hex(),
width: 1,
},
background: background(layer, "accent"),
border: border(layer, "accent"),
},
warningContainer: {
...baseContainer,
background: backgroundColor(theme, "on500Warning"),
border: {
color: theme.ramps.yellow(0).hex(),
width: 1,
},
background: background(layer, "warning"),
border: border(layer, "warning"),
},
errorContainer: {
...baseContainer,
background: backgroundColor(theme, "on500Error"),
border: {
color: theme.ramps.red(0).hex(),
width: 1,
}
background: background(layer, "negative"),
border: border(layer, "negative"),
},
block_style: {
padding: { top: 4 },
},
prose: text(theme, "sans", "primary", { size: "sm" }),
highlight: theme.editor.highlight.occurrence,
prose: text(layer, "sans", { size: "sm" }),
highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
};
}

View file

@ -1,12 +1,13 @@
import Theme from "../themes/common/theme";
import { backgroundColor, borderColor, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, text } from "./components";
export default function incomingCallNotification(theme: Theme): Object {
export default function incomingCallNotification(colorScheme: ColorScheme): Object {
let layer = colorScheme.middle;
const avatarSize = 48;
return {
windowHeight: 74,
windowWidth: 380,
background: backgroundColor(theme, 300),
background: background(layer),
callerContainer: {
padding: 12,
},
@ -19,26 +20,26 @@ export default function incomingCallNotification(theme: Theme): Object {
margin: { left: 10 },
},
callerUsername: {
...text(theme, "sans", "active", { size: "sm", weight: "bold" }),
...text(layer, "sans", { size: "sm", weight: "bold" }),
margin: { top: -3 },
},
callerMessage: {
...text(theme, "sans", "secondary", { size: "xs" }),
...text(layer, "sans", "variant", { size: "xs" }),
margin: { top: -3 },
},
worktreeRoots: {
...text(theme, "sans", "secondary", { size: "xs", weight: "bold" }),
...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
margin: { top: -3 },
},
buttonWidth: 96,
acceptButton: {
background: backgroundColor(theme, "ok", "active"),
border: { left: true, bottom: true, width: 1, color: borderColor(theme, "primary") },
...text(theme, "sans", "ok", { size: "xs", weight: "extra_bold" })
background: background(layer, "accent"),
border: border(layer, { left: true, bottom: true }),
...text(layer, "sans", "positive", { size: "xs", weight: "extra_bold" })
},
declineButton: {
border: { left: true, width: 1, color: borderColor(theme, "primary") },
...text(theme, "sans", "error", { size: "xs", weight: "extra_bold" })
border: border(layer, { left: true }),
...text(layer, "sans", "negative", { size: "xs", weight: "extra_bold" })
},
};
}

View file

@ -1,17 +1,16 @@
import Theme from "../themes/common/theme";
import {
backgroundColor,
border,
player,
modalShadow,
text,
} from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, text } from "./components";
export default function picker(theme: Theme) {
export default function picker(colorScheme: ColorScheme) {
let layer = colorScheme.lowest;
return {
background: backgroundColor(theme, 300),
cornerRadius: 8,
padding: 8,
background: background(layer),
border: border(layer),
shadow: colorScheme.modalShadow,
cornerRadius: 12,
padding: {
bottom: 4,
},
item: {
padding: {
bottom: 4,
@ -19,41 +18,48 @@ export default function picker(theme: Theme) {
right: 12,
top: 4,
},
margin: {
top: 1,
left: 4,
right: 4,
},
cornerRadius: 8,
text: text(theme, "sans", "secondary"),
highlightText: text(theme, "sans", "feature", { weight: "bold" }),
text: text(layer, "sans", "variant"),
highlightText: text(layer, "sans", "accent", { weight: "bold" }),
active: {
background: backgroundColor(theme, 300, "active"),
text: text(theme, "sans", "active"),
background: background(layer, "base", "active"),
text: text(layer, "sans", "base", "active"),
highlightText: text(layer, "sans", "accent", {
weight: "bold",
}),
},
hover: {
background: backgroundColor(theme, 300, "hovered"),
background: background(layer, "hovered"),
},
},
border: border(theme, "primary"),
empty: {
text: text(theme, "sans", "muted"),
text: text(layer, "sans", "variant"),
padding: {
bottom: 4,
left: 12,
right: 12,
bottom: 8,
left: 16,
right: 16,
top: 8,
},
},
inputEditor: {
background: backgroundColor(theme, 500),
cornerRadius: 8,
placeholderText: text(theme, "sans", "placeholder"),
selection: player(theme, 1).selection,
text: text(theme, "mono", "primary"),
border: border(theme, "secondary"),
placeholderText: text(layer, "sans", "on", "disabled"),
selection: colorScheme.players[0],
text: text(layer, "mono", "on"),
border: border(layer, { bottom: true }),
padding: {
bottom: 7,
bottom: 8,
left: 16,
right: 16,
top: 7,
top: 8,
},
margin: {
bottom: 4,
},
},
shadow: modalShadow(theme),
};
}

View file

@ -1,12 +1,13 @@
import Theme from "../themes/common/theme";
import { backgroundColor, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, text } from "./components";
export default function projectDiagnostics(theme: Theme) {
export default function projectDiagnostics(colorScheme: ColorScheme) {
let layer = colorScheme.highest;
return {
background: backgroundColor(theme, 500),
background: background(layer),
tabIconSpacing: 4,
tabIconWidth: 13,
tabSummarySpacing: 10,
emptyMessage: text(theme, "sans", "secondary", { size: "md" }),
emptyMessage: text(layer, "sans", "variant", { size: "md" }),
};
}

View file

@ -1,36 +1,36 @@
import Theme from "../themes/common/theme";
import { panel } from "./app";
import { backgroundColor, iconColor, player, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, foreground, text } from "./components";
export default function projectPanel(theme: Theme) {
export default function projectPanel(colorScheme: ColorScheme) {
let layer = colorScheme.middle;
return {
...panel,
background: background(layer),
padding: { left: 12, right: 12, top: 6, bottom: 6 },
indentWidth: 8,
entry: {
height: 24,
iconColor: iconColor(theme, "muted"),
iconColor: foreground(layer, "variant"),
iconSize: 8,
iconSpacing: 8,
text: text(theme, "mono", "secondary", { size: "sm" }),
text: text(layer, "mono", "variant", { size: "sm" }),
hover: {
background: backgroundColor(theme, 300, "hovered"),
background: background(layer, "variant", "hovered"),
},
active: {
background: backgroundColor(theme, 300, "active"),
text: text(theme, "mono", "active", { size: "sm" }),
background: background(layer, "active"),
text: text(layer, "mono", "active", { size: "sm" }),
},
activeHover: {
background: backgroundColor(theme, 300, "active"),
text: text(theme, "mono", "active", { size: "sm" }),
background: background(layer, "active"),
text: text(layer, "mono", "active", { size: "sm" }),
},
},
cutEntryFade: 0.4,
ignoredEntryFade: 0.6,
filenameEditor: {
background: backgroundColor(theme, "on300"),
text: text(theme, "mono", "active", { size: "sm" }),
selection: player(theme, 1).selection,
background: background(layer, "on"),
text: text(layer, "mono", "on", { size: "sm" }),
selection: colorScheme.players[0],
},
};
}

View file

@ -1,12 +1,14 @@
import Theme from "../themes/common/theme";
import { backgroundColor, borderColor, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, text } from "./components";
export default function projectSharedNotification(colorScheme: ColorScheme): Object {
let layer = colorScheme.middle;
export default function projectSharedNotification(theme: Theme): Object {
const avatarSize = 48;
return {
windowHeight: 74,
windowWidth: 380,
background: backgroundColor(theme, 300),
background: background(layer),
ownerContainer: {
padding: 12,
},
@ -19,26 +21,26 @@ export default function projectSharedNotification(theme: Theme): Object {
margin: { left: 10 },
},
ownerUsername: {
...text(theme, "sans", "active", { size: "sm", weight: "bold" }),
...text(layer, "sans", { size: "sm", weight: "bold" }),
margin: { top: -3 },
},
message: {
...text(theme, "sans", "secondary", { size: "xs" }),
...text(layer, "sans", "variant", { size: "xs" }),
margin: { top: -3 },
},
worktreeRoots: {
...text(theme, "sans", "secondary", { size: "xs", weight: "bold" }),
...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
margin: { top: -3 },
},
buttonWidth: 96,
openButton: {
background: backgroundColor(theme, "info", "active"),
border: { left: true, bottom: true, width: 1, color: borderColor(theme, "primary") },
...text(theme, "sans", "info", { size: "xs", weight: "extra_bold" })
background: background(layer, "accent"),
border: border(layer, { left: true, bottom: true, }),
...text(layer, "sans", "accent", { size: "xs", weight: "extra_bold" })
},
dismissButton: {
border: { left: true, width: 1, color: borderColor(theme, "primary") },
...text(theme, "sans", "secondary", { size: "xs", weight: "extra_bold" })
border: border(layer, { left: true }),
...text(layer, "sans", "variant", { size: "xs", weight: "extra_bold" })
},
};
}

View file

@ -1,17 +1,19 @@
import Theme from "../themes/common/theme";
import { backgroundColor, border, player, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, text } from "./components";
export default function search(colorScheme: ColorScheme) {
let layer = colorScheme.highest;
export default function search(theme: Theme) {
// Search input
const editor = {
background: backgroundColor(theme, 500),
background: background(layer),
cornerRadius: 8,
minWidth: 200,
maxWidth: 500,
placeholderText: text(theme, "mono", "placeholder"),
selection: player(theme, 1).selection,
text: text(theme, "mono", "active"),
border: border(theme, "secondary"),
placeholderText: text(layer, "mono", "disabled"),
selection: colorScheme.players[0],
text: text(layer, "mono", "default"),
border: border(layer),
margin: {
right: 12,
},
@ -24,14 +26,14 @@ export default function search(theme: Theme) {
};
return {
matchBackground: theme.editor.highlight.match,
matchBackground: background(layer), // theme.editor.highlight.match,
tabIconSpacing: 8,
tabIconWidth: 14,
optionButton: {
...text(theme, "mono", "secondary"),
background: backgroundColor(theme, "on500"),
...text(layer, "mono", "on"),
background: background(layer, "on"),
cornerRadius: 6,
border: border(theme, "secondary"),
border: border(layer, "on"),
margin: {
right: 4,
},
@ -42,28 +44,28 @@ export default function search(theme: Theme) {
top: 2,
},
active: {
...text(theme, "mono", "active"),
background: backgroundColor(theme, "on500", "active"),
border: border(theme, "muted"),
...text(layer, "mono", "on", "inverted"),
background: background(layer, "on", "inverted"),
border: border(layer, "on", "inverted"),
},
clicked: {
...text(theme, "mono", "active"),
background: backgroundColor(theme, "on300", "active"),
border: border(theme, "secondary"),
...text(layer, "mono", "on", "pressed"),
background: background(layer, "on", "pressed"),
border: border(layer, "on", "pressed"),
},
hover: {
...text(theme, "mono", "active"),
background: backgroundColor(theme, "on500", "hovered"),
border: border(theme, "muted"),
...text(layer, "mono", "on", "hovered"),
background: background(layer, "on", "hovered"),
border: border(layer, "on", "hovered"),
},
},
editor,
invalidEditor: {
...editor,
border: border(theme, "error"),
border: border(layer, "negative"),
},
matchIndex: {
...text(theme, "mono", "muted"),
...text(layer, "mono", "variant"),
padding: 6,
},
optionButtonGroup: {
@ -73,7 +75,7 @@ export default function search(theme: Theme) {
},
},
resultsStatus: {
...text(theme, "mono", "primary"),
...text(layer, "mono", "on"),
size: 18,
},
};

View file

@ -1,8 +1,9 @@
import Theme from "../themes/common/theme";
import { backgroundColor, border, iconColor, text } from "./components";
import { workspaceBackground } from "./workspace";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, foreground, text } from "./components";
export default function statusBar(colorScheme: ColorScheme) {
let layer = colorScheme.lowest;
export default function statusBar(theme: Theme) {
const statusContainer = {
cornerRadius: 6,
padding: { top: 3, bottom: 3, left: 6, right: 6 },
@ -22,70 +23,70 @@ export default function statusBar(theme: Theme) {
left: 6,
right: 6,
},
border: border(theme, "primary", { top: true, overlay: true }),
cursorPosition: text(theme, "sans", "secondary"),
autoUpdateProgressMessage: text(theme, "sans", "secondary"),
autoUpdateDoneMessage: text(theme, "sans", "secondary"),
border: border(layer, { top: true, overlay: true }),
cursorPosition: text(layer, "sans", "variant"),
autoUpdateProgressMessage: text(layer, "sans", "variant"),
autoUpdateDoneMessage: text(layer, "sans", "variant"),
lspStatus: {
...diagnosticStatusContainer,
iconSpacing: 4,
iconWidth: 14,
height: 18,
message: text(theme, "sans", "secondary"),
iconColor: iconColor(theme, "muted"),
message: text(layer, "sans"),
iconColor: foreground(layer),
hover: {
message: text(theme, "sans", "primary"),
iconColor: iconColor(theme, "primary"),
background: backgroundColor(theme, 300, "hovered"),
message: text(layer, "sans"),
iconColor: foreground(layer),
background: background(layer),
},
},
diagnosticMessage: {
...text(theme, "sans", "secondary"),
hover: text(theme, "sans", "active"),
...text(layer, "sans"),
hover: text(layer, "sans", "hovered"),
},
feedback: {
...text(theme, "sans", "secondary"),
hover: text(theme, "sans", "active"),
...text(layer, "sans", "variant"),
hover: text(layer, "sans", "hovered"),
},
diagnosticSummary: {
height: 16,
height: 20,
iconWidth: 16,
iconSpacing: 2,
summarySpacing: 6,
text: text(theme, "sans", "primary", { size: "sm" }),
iconColorOk: iconColor(theme, "muted"),
iconColorWarning: iconColor(theme, "warning"),
iconColorError: iconColor(theme, "error"),
text: text(layer, "sans", { size: "sm" }),
iconColorOk: foreground(layer, "variant"),
iconColorWarning: foreground(layer, "warning"),
iconColorError: foreground(layer, "negative"),
containerOk: {
cornerRadius: 6,
padding: { top: 3, bottom: 3, left: 7, right: 7 },
},
containerWarning: {
...diagnosticStatusContainer,
background: backgroundColor(theme, "warning"),
border: border(theme, "warning"),
background: background(layer, "warning"),
border: border(layer, "warning"),
},
containerError: {
...diagnosticStatusContainer,
background: backgroundColor(theme, "error"),
border: border(theme, "error"),
background: background(layer, "negative"),
border: border(layer, "negative"),
},
hover: {
iconColorOk: iconColor(theme, "active"),
iconColorOk: foreground(layer, "on"),
containerOk: {
cornerRadius: 6,
padding: { top: 3, bottom: 3, left: 7, right: 7 },
background: backgroundColor(theme, 300, "hovered"),
background: background(layer, "on", "hovered"),
},
containerWarning: {
...diagnosticStatusContainer,
background: backgroundColor(theme, "warning", "hovered"),
border: border(theme, "warning"),
background: background(layer, "warning", "hovered"),
border: border(layer, "warning", "hovered"),
},
containerError: {
...diagnosticStatusContainer,
background: backgroundColor(theme, "error", "hovered"),
border: border(theme, "error"),
background: background(layer, "negative", "hovered"),
border: border(layer, "negative", "hovered"),
},
},
},
@ -95,22 +96,22 @@ export default function statusBar(theme: Theme) {
item: {
...statusContainer,
iconSize: 16,
iconColor: iconColor(theme, "muted"),
iconColor: foreground(layer, "variant"),
hover: {
iconColor: iconColor(theme, "active"),
background: backgroundColor(theme, 300, "hovered"),
iconColor: foreground(layer, "hovered"),
background: background(layer, "variant"),
},
active: {
iconColor: iconColor(theme, "active"),
background: backgroundColor(theme, 300, "active"),
iconColor: foreground(layer, "active"),
background: background(layer, "active"),
},
},
badge: {
cornerRadius: 3,
padding: 2,
margin: { bottom: -1, right: -1 },
border: { width: 1, color: workspaceBackground(theme) },
background: iconColor(theme, "feature"),
border: border(layer),
background: background(layer, "accent"),
},
},
};

View file

@ -1,76 +1,84 @@
import Theme from "../themes/common/theme";
import { ColorScheme } from "../themes/common/colorScheme";
import { withOpacity } from "../utils/color";
import { iconColor, text, border, backgroundColor, draggedShadow } from "./components";
import { text, border, background, foreground } from "./components";
export default function tabBar(theme: Theme) {
export default function tabBar(colorScheme: ColorScheme) {
const height = 32;
let activeLayer = colorScheme.highest;
let layer = colorScheme.middle;
const tab = {
height,
background: backgroundColor(theme, 300),
border: border(theme, "primary", {
left: true,
text: text(layer, "sans", "variant", { size: "sm" }),
background: background(layer),
border: border(layer, {
right: true,
bottom: true,
overlay: true,
}),
iconClose: iconColor(theme, "muted"),
iconCloseActive: iconColor(theme, "active"),
iconConflict: iconColor(theme, "warning"),
iconDirty: iconColor(theme, "info"),
iconWidth: 8,
spacing: 8,
text: text(theme, "sans", "secondary", { size: "sm" }),
padding: {
left: 8,
right: 8,
right: 12,
},
spacing: 8,
// Close icons
iconWidth: 8,
iconClose: foreground(layer, "variant"),
iconCloseActive: foreground(layer),
// Indicators
iconConflict: foreground(layer, "warning"),
iconDirty: foreground(layer, "accent"),
// When two tabs of the same name are open, a label appears next to them
description: {
margin: { left: 6, top: 1 },
...text(theme, "sans", "muted", { size: "2xs" })
}
margin: { left: 8 },
...text(layer, "sans", "disabled", { size: "2xs" }),
},
};
const activePaneActiveTab = {
...tab,
background: backgroundColor(theme, 500),
text: text(theme, "sans", "active", { size: "sm" }),
background: background(activeLayer),
text: text(activeLayer, "sans", "active", { size: "sm" }),
border: {
...tab.border,
bottom: false
bottom: false,
},
};
const inactivePaneInactiveTab = {
...tab,
background: backgroundColor(theme, 300),
text: text(theme, "sans", "muted", { size: "sm" }),
background: background(layer),
text: text(layer, "sans", "variant", { size: "sm" }),
};
const inactivePaneActiveTab = {
...tab,
background: backgroundColor(theme, 500),
text: text(theme, "sans", "secondary", { size: "sm" }),
background: background(activeLayer),
text: text(layer, "sans", "variant", { size: "sm" }),
border: {
...tab.border,
bottom: false
bottom: false,
},
}
};
const draggedTab = {
...activePaneActiveTab,
background: withOpacity(tab.background, 0.8),
border: undefined as any, // Remove border
shadow: draggedShadow(theme),
}
background: withOpacity(tab.background, 0.95),
border: undefined as any,
shadow: colorScheme.popoverShadow,
};
return {
height,
background: backgroundColor(theme, 300),
dropTargetOverlayColor: withOpacity(theme.textColor.muted, 0.6),
border: border(theme, "primary", {
bottom: true,
overlay: true,
}),
background: background(layer),
dropTargetOverlayColor: withOpacity(
foreground(layer),
0.6
),
activePane: {
activeTab: activePaneActiveTab,
inactiveTab: tab,
@ -81,11 +89,11 @@ export default function tabBar(theme: Theme) {
},
draggedTab,
paneButton: {
color: iconColor(theme, "secondary"),
color: foreground(layer, "variant"),
iconWidth: 12,
buttonWidth: activePaneActiveTab.height,
hover: {
color: iconColor(theme, "active"),
color: foreground(layer, "hovered"),
},
},
paneButtonContainer: {
@ -93,7 +101,7 @@ export default function tabBar(theme: Theme) {
border: {
...tab.border,
right: false,
}
}
}
}
},
},
};
}

View file

@ -1,65 +1,52 @@
import Theme from "../themes/common/theme";
import { border, modalShadow, player } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
export default function terminal(theme: Theme) {
export default function terminal(colorScheme: ColorScheme) {
/**
* Colors are controlled per-cell in the terminal grid.
* Cells can be set to any of these more 'theme-capable' colors
* or can be set directly with RGB values.
* Here are the common interpretations of these names:
* https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
*/
let colors = {
black: theme.ramps.neutral(0).hex(),
red: theme.ramps.red(0.5).hex(),
green: theme.ramps.green(0.5).hex(),
yellow: theme.ramps.yellow(0.5).hex(),
blue: theme.ramps.blue(0.5).hex(),
magenta: theme.ramps.magenta(0.5).hex(),
cyan: theme.ramps.cyan(0.5).hex(),
white: theme.ramps.neutral(7).hex(),
brightBlack: theme.ramps.neutral(4).hex(),
brightRed: theme.ramps.red(0.25).hex(),
brightGreen: theme.ramps.green(0.25).hex(),
brightYellow: theme.ramps.yellow(0.25).hex(),
brightBlue: theme.ramps.blue(0.25).hex(),
brightMagenta: theme.ramps.magenta(0.25).hex(),
brightCyan: theme.ramps.cyan(0.25).hex(),
brightWhite: theme.ramps.neutral(7).hex(),
* Colors are controlled per-cell in the terminal grid.
* Cells can be set to any of these more 'theme-capable' colors
* or can be set directly with RGB values.
* Here are the common interpretations of these names:
* https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
*/
return {
black: colorScheme.ramps.neutral(0).hex(),
red: colorScheme.ramps.red(0.5).hex(),
green: colorScheme.ramps.green(0.5).hex(),
yellow: colorScheme.ramps.yellow(0.5).hex(),
blue: colorScheme.ramps.blue(0.5).hex(),
magenta: colorScheme.ramps.magenta(0.5).hex(),
cyan: colorScheme.ramps.cyan(0.5).hex(),
white: colorScheme.ramps.neutral(1).hex(),
brightBlack: colorScheme.ramps.neutral(0.4).hex(),
brightRed: colorScheme.ramps.red(0.25).hex(),
brightGreen: colorScheme.ramps.green(0.25).hex(),
brightYellow: colorScheme.ramps.yellow(0.25).hex(),
brightBlue: colorScheme.ramps.blue(0.25).hex(),
brightMagenta: colorScheme.ramps.magenta(0.25).hex(),
brightCyan: colorScheme.ramps.cyan(0.25).hex(),
brightWhite: colorScheme.ramps.neutral(1).hex(),
/**
* Default color for characters
*/
foreground: theme.ramps.neutral(7).hex(),
foreground: colorScheme.ramps.neutral(1).hex(),
/**
* Default color for the rectangle background of a cell
*/
background: theme.ramps.neutral(0).hex(),
modalBackground: theme.ramps.neutral(1).hex(),
background: colorScheme.ramps.neutral(0).hex(),
modalBackground: colorScheme.ramps.neutral(0.1).hex(),
/**
* Default color for the cursor
*/
cursor: player(theme, 1).selection.cursor,
dimBlack: theme.ramps.neutral(7).hex(),
dimRed: theme.ramps.red(0.75).hex(),
dimGreen: theme.ramps.green(0.75).hex(),
dimYellow: theme.ramps.yellow(0.75).hex(),
dimBlue: theme.ramps.blue(0.75).hex(),
dimMagenta: theme.ramps.magenta(0.75).hex(),
dimCyan: theme.ramps.cyan(0.75).hex(),
dimWhite: theme.ramps.neutral(5).hex(),
brightForeground: theme.ramps.neutral(7).hex(),
dimForeground: theme.ramps.neutral(0).hex(),
};
return {
colors,
modalContainer: {
background: colors.modalBackground,
cornerRadius: 8,
padding: 8,
margin: 25,
border: border(theme, "primary"),
shadow: modalShadow(theme),
},
cursor: colorScheme.players[0].cursor,
dimBlack: colorScheme.ramps.neutral(1).hex(),
dimRed: colorScheme.ramps.red(0.75).hex(),
dimGreen: colorScheme.ramps.green(0.75).hex(),
dimYellow: colorScheme.ramps.yellow(0.75).hex(),
dimBlue: colorScheme.ramps.blue(0.75).hex(),
dimMagenta: colorScheme.ramps.magenta(0.75).hex(),
dimCyan: colorScheme.ramps.cyan(0.75).hex(),
dimWhite: colorScheme.ramps.neutral(0.6).hex(),
brightForeground: colorScheme.ramps.neutral(1).hex(),
dimForeground: colorScheme.ramps.neutral(0).hex(),
};
}

View file

@ -1,21 +1,22 @@
import Theme from "../themes/common/theme";
import { backgroundColor, border, popoverShadow, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, text } from "./components";
export default function tooltip(theme: Theme) {
export default function tooltip(colorScheme: ColorScheme) {
let layer = colorScheme.middle;
return {
background: backgroundColor(theme, 500),
border: border(theme, "secondary"),
background: background(layer),
border: border(layer),
padding: { top: 4, bottom: 4, left: 8, right: 8 },
margin: { top: 6, left: 6 },
shadow: popoverShadow(theme),
shadow: colorScheme.popoverShadow,
cornerRadius: 6,
text: text(theme, "sans", "primary", { size: "xs" }),
text: text(layer, "sans", { size: "xs" }),
keystroke: {
background: backgroundColor(theme, "on500"),
background: background(layer, "on"),
cornerRadius: 4,
margin: { left: 6 },
padding: { left: 4, right: 4 },
...text(theme, "mono", "secondary", { size: "xs", weight: "bold" }),
...text(layer, "mono", "on", { size: "xs", weight: "bold" }),
},
maxTextWidth: 200,
};

View file

@ -1,29 +1,30 @@
import Theme from "../themes/common/theme";
import { iconColor, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme";
import { foreground, text } from "./components";
const headerPadding = 8;
export default function updateNotification(theme: Theme): Object {
export default function updateNotification(colorScheme: ColorScheme): Object {
let layer = colorScheme.middle;
return {
message: {
...text(theme, "sans", "primary", { size: "xs" }),
...text(layer, "sans", { size: "xs" }),
margin: { left: headerPadding, right: headerPadding },
},
actionMessage: {
...text(theme, "sans", "secondary", { size: "xs" }),
...text(layer, "sans", { size: "xs" }),
margin: { left: headerPadding, top: 6, bottom: 6 },
hover: {
color: theme.textColor["active"],
color: foreground(layer, "hovered"),
},
},
dismissButton: {
color: iconColor(theme, "secondary"),
color: foreground(layer),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
hover: {
color: iconColor(theme, "primary"),
color: foreground(layer, "hovered"),
},
},
};

View file

@ -1,64 +1,58 @@
import Theme from "../themes/common/theme";
import { ColorScheme } from "../themes/common/colorScheme";
import { withOpacity } from "../utils/color";
import {
backgroundColor,
background,
border,
iconColor,
modalShadow,
borderColor,
foreground,
text,
} from "./components";
import statusBar from "./statusBar";
import tabBar from "./tabBar";
export function workspaceBackground(theme: Theme) {
return backgroundColor(theme, 300);
}
export default function workspace(theme: Theme) {
export default function workspace(colorScheme: ColorScheme) {
const layer = colorScheme.lowest;
const titlebarPadding = 6;
const titlebarButton = {
background: backgroundColor(theme, 100),
border: border(theme, "secondary"),
cornerRadius: 6,
margin: {
top: 1,
},
padding: {
top: 1,
bottom: 1,
left: 7,
right: 7,
left: 8,
right: 8,
},
...text(theme, "sans", "secondary", { size: "xs" }),
...text(layer, "sans", "variant", { size: "xs" }),
background: background(layer, "variant"),
border: border(layer),
hover: {
...text(theme, "sans", "active", { size: "xs" }),
background: backgroundColor(theme, "on300", "hovered"),
border: border(theme, "primary"),
...text(layer, "sans", "variant", "hovered", { size: "xs" }),
background: background(layer, "variant", "hovered"),
border: border(layer, "variant", "hovered"),
},
};
const avatarWidth = 18;
return {
background: backgroundColor(theme, 300),
background: background(layer),
joiningProjectAvatar: {
cornerRadius: 40,
width: 80,
},
joiningProjectMessage: {
padding: 12,
...text(theme, "sans", "primary", { size: "lg" }),
...text(layer, "sans", { size: "lg" }),
},
externalLocationMessage: {
background: backgroundColor(theme, "info"),
border: border(theme, "secondary"),
background: background(colorScheme.middle, "accent"),
border: border(colorScheme.middle, "accent"),
cornerRadius: 6,
padding: 12,
margin: { bottom: 8, right: 8 },
...text(theme, "sans", "secondary", { size: "xs" }),
...text(colorScheme.middle, "sans", "accent", { size: "xs" }),
},
leaderBorderOpacity: 0.7,
leaderBorderWidth: 2.0,
tabBar: tabBar(theme),
tabBar: tabBar(colorScheme),
modal: {
margin: {
bottom: 52,
@ -68,28 +62,28 @@ export default function workspace(theme: Theme) {
},
sidebar: {
initialSize: 240,
border: {
color: border(theme, "primary").color,
width: 1,
left: true,
right: true,
}
border: border(layer, { left: true, right: true }),
},
paneDivider: {
color: border(theme, "secondary").color,
color: borderColor(layer),
width: 1,
},
statusBar: statusBar(theme),
statusBar: statusBar(colorScheme),
titlebar: {
avatarWidth,
avatarMargin: 8,
height: 33,
background: backgroundColor(theme, 100),
height: 33, // 32px + 1px for overlaid border
background: background(layer),
border: border(layer, { bottom: true, overlay: true }),
padding: {
left: 80,
right: titlebarPadding,
},
title: text(theme, "sans", "primary"),
// Project
title: text(layer, "sans", "variant"),
// Collaborators
avatar: {
cornerRadius: avatarWidth / 2,
border: {
@ -108,15 +102,18 @@ export default function workspace(theme: Theme) {
avatarRibbon: {
height: 3,
width: 12,
// TODO: The background for this ideally should be
// set with a token, not hardcoded in rust
// TODO: Chore: Make avatarRibbon colors driven by the theme rather than being hard coded.
},
border: border(theme, "primary", { bottom: true, overlay: true }),
// Sign in buttom
// FlatButton, Variant
signInPrompt: {
...titlebarButton
},
// Offline Indicator
offlineIcon: {
color: iconColor(theme, "secondary"),
color: foreground(layer, "variant"),
width: 16,
margin: {
left: titlebarPadding,
@ -125,79 +122,82 @@ export default function workspace(theme: Theme) {
right: 4,
},
},
// Notice that the collaboration server is out of date
outdatedWarning: {
...text(theme, "sans", "warning", { size: "xs" }),
background: backgroundColor(theme, "warning"),
border: border(theme, "warning"),
...text(layer, "sans", "warning", { size: "xs" }),
background: withOpacity(background(layer, "warning"), 0.3),
border: border(layer, "warning"),
margin: {
left: titlebarPadding,
},
padding: {
left: 6,
right: 6,
left: 8,
right: 8,
},
cornerRadius: 6,
},
toggleContactsButton: {
cornerRadius: 6,
color: iconColor(theme, "secondary"),
color: foreground(layer, "variant"),
iconWidth: 8,
buttonWidth: 20,
active: {
background: backgroundColor(theme, "on300", "active"),
color: iconColor(theme, "active"),
background: background(layer, "variant", "active"),
color: foreground(layer, "variant", "active"),
},
hover: {
background: backgroundColor(theme, "on300", "hovered"),
color: iconColor(theme, "active"),
background: background(layer, "variant", "hovered"),
color: foreground(layer, "variant", "hovered"),
},
},
toggleContactsBadge: {
cornerRadius: 3,
padding: 2,
margin: { top: 3, left: 3 },
border: { width: 1, color: workspaceBackground(theme) },
background: iconColor(theme, "feature"),
border: border(layer),
background: foreground(layer, "accent"),
},
shareButton: {
...titlebarButton
}
},
toolbar: {
height: 34,
background: backgroundColor(theme, 500),
border: border(theme, "secondary", { bottom: true }),
background: background(colorScheme.highest),
border: border(colorScheme.highest, { bottom: true }),
itemSpacing: 8,
navButton: {
color: iconColor(theme, "primary"),
color: foreground(colorScheme.highest, "on"),
iconWidth: 12,
buttonWidth: 24,
cornerRadius: 6,
hover: {
color: iconColor(theme, "active"),
background: backgroundColor(theme, "on500", "hovered"),
color: foreground(colorScheme.highest, "on", "hovered"),
background: background(colorScheme.highest, "on", "hovered"),
},
disabled: {
color: withOpacity(iconColor(theme, "muted"), 0.6),
color: foreground(colorScheme.highest, "on", "disabled"),
},
},
padding: { left: 8, right: 8, top: 4, bottom: 4 },
},
breadcrumbs: {
...text(theme, "mono", "secondary"),
...text(layer, "mono", "variant"),
padding: { left: 6 },
},
disconnectedOverlay: {
...text(theme, "sans", "active"),
background: withOpacity(theme.backgroundColor[500].base, 0.8),
...text(layer, "sans"),
background: withOpacity(background(layer), 0.8),
},
notification: {
margin: { top: 10 },
background: backgroundColor(theme, 300),
background: background(colorScheme.middle),
cornerRadius: 6,
padding: 12,
border: border(theme, "primary"),
shadow: modalShadow(theme),
border: border(colorScheme.middle),
shadow: colorScheme.popoverShadow,
},
notifications: {
width: 400,
@ -206,18 +206,15 @@ export default function workspace(theme: Theme) {
dock: {
initialSizeRight: 640,
initialSizeBottom: 480,
wash_color: withOpacity(theme.backgroundColor[500].base, 0.5),
wash_color: withOpacity(background(colorScheme.highest), 0.5),
panel: {
border: {
...border(theme, "secondary"),
width: 1
},
border: border(colorScheme.highest),
},
maximized: {
margin: 24,
border: border(theme, "secondary", { "overlay": true }),
shadow: modalShadow(theme),
}
}
margin: 32,
border: border(colorScheme.highest, { overlay: true }),
shadow: colorScheme.modalShadow,
},
},
};
}

View file

@ -1,31 +0,0 @@
import fs from "fs";
import path from "path";
import Theme from "./themes/common/theme";
const themes: Theme[] = [];
export default themes;
const internalThemes: Theme[] = [];
export { internalThemes }
const experimentalThemes: Theme[] = [];
export { experimentalThemes }
function fillThemes(themesPath: string, themes: Theme[]) {
for (const fileName of fs.readdirSync(themesPath)) {
if (fileName == "template.ts") continue;
const filePath = path.join(themesPath, fileName);
if (fs.statSync(filePath).isFile()) {
const theme = require(filePath);
if (theme.dark) themes.push(theme.dark);
if (theme.light) themes.push(theme.light);
}
}
}
fillThemes(path.resolve(`${__dirname}/themes`), themes)
fillThemes(path.resolve(`${__dirname}/themes/internal`), internalThemes)
fillThemes(path.resolve(`${__dirname}/themes/experiments`), experimentalThemes)

View file

@ -1,28 +0,0 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
const name = "abruzzo";
const ramps = {
neutral: chroma.scale([
"#1b0d05",
"#2c1e18",
"#654035",
"#9d5e4a",
"#b37354",
"#c1825a",
"#dda66e",
"#fbf3e2",
]),
red: colorRamp(chroma("#e594c4")),
orange: colorRamp(chroma("#d9e87e")),
yellow: colorRamp(chroma("#fd9d83")),
green: colorRamp(chroma("#96adf7")),
cyan: colorRamp(chroma("#fc798f")),
blue: colorRamp(chroma("#BCD0F5")),
violet: colorRamp(chroma("#dac5eb")),
magenta: colorRamp(chroma("#c1a3ef")),
};
export const dark = createTheme(`${name}`, false, ramps);
// export const light = createTheme(`${name}-light`, true, ramps);

View file

@ -1,19 +1,21 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
import { colorRamp, createColorScheme } from "./common/ramps";
const name = "andromeda";
const ramps = {
neutral: chroma.scale([
"#1E2025",
"#23262E",
"#292E38",
"#2E323C",
"#ACA8AE",
"#CBC9CF",
"#E1DDE4",
"#F7F7F8",
]),
neutral: chroma
.scale([
"#1E2025",
"#23262E",
"#292E38",
"#2E323C",
"#ACA8AE",
"#CBC9CF",
"#E1DDE4",
"#F7F7F8",
])
.domain([0, 0.15, 0.25, 0.35, 0.7, 0.8, 0.9, 1]),
red: colorRamp(chroma("#F92672")),
orange: colorRamp(chroma("#F39C12")),
yellow: colorRamp(chroma("#FFE66D")),
@ -24,4 +26,4 @@ const ramps = {
magenta: colorRamp(chroma("#C74DED")),
};
export const dark = createTheme(`${name}`, false, ramps);
export const dark = createColorScheme(`${name}`, false, ramps);

View file

@ -1,28 +0,0 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
const name = "brush-tree";
const ramps = {
neutral: chroma.scale([
"#485867",
"#5A6D7A",
"#6D828E",
"#8299A1",
"#98AFB5",
"#B0C5C8",
"#C9DBDC",
"#E3EFEF",
]),
red: colorRamp(chroma("#b38686")),
orange: colorRamp(chroma("#d8bba2")),
yellow: colorRamp(chroma("#aab386")),
green: colorRamp(chroma("#87b386")),
cyan: colorRamp(chroma("#86b3b3")),
blue: colorRamp(chroma("#868cb3")),
violet: colorRamp(chroma("#b386b2")),
magenta: colorRamp(chroma("#b39f9f")),
};
export const dark = createTheme(`${name}-dark`, false, ramps);
export const light = createTheme(`${name}-light`, true, ramps);

View file

@ -1,19 +1,21 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
import { colorRamp, createColorScheme } from "./common/ramps";
const name = "cave";
const ramps = {
neutral: chroma.scale([
"#19171c",
"#26232a",
"#585260",
"#655f6d",
"#7e7887",
"#8b8792",
"#e2dfe7",
"#efecf4",
]),
export const dark = createColorScheme(`${name}-dark`, false, {
neutral: chroma
.scale([
"#19171c",
"#26232a",
"#585260",
"#655f6d",
"#7e7887",
"#8b8792",
"#e2dfe7",
"#efecf4",
])
.domain([0, 0.15, 0.45, 0.6, 0.65, 0.7, 0.85, 1]),
red: colorRamp(chroma("#be4678")),
orange: colorRamp(chroma("#aa573c")),
yellow: colorRamp(chroma("#a06e3b")),
@ -22,7 +24,26 @@ const ramps = {
blue: colorRamp(chroma("#576ddb")),
violet: colorRamp(chroma("#955ae7")),
magenta: colorRamp(chroma("#bf40bf")),
};
});
export const dark = createTheme(`${name}-dark`, false, ramps);
export const light = createTheme(`${name}-light`, true, ramps);
export const light = createColorScheme(`${name}-light`, true, {
neutral: chroma
.scale([
"#19171c",
"#26232a",
"#585260",
"#655f6d",
"#7e7887",
"#8b8792",
"#e2dfe7",
"#efecf4",
]).correctLightness(),
red: colorRamp(chroma("#be4678")),
orange: colorRamp(chroma("#aa573c")),
yellow: colorRamp(chroma("#a06e3b")),
green: colorRamp(chroma("#2a9292")),
cyan: colorRamp(chroma("#398bc6")),
blue: colorRamp(chroma("#576ddb")),
violet: colorRamp(chroma("#955ae7")),
magenta: colorRamp(chroma("#bf40bf")),
});

View file

@ -0,0 +1,78 @@
import { Scale } from "chroma-js";
export interface ColorScheme {
name: string;
isLight: boolean;
lowest: Layer;
middle: Layer;
highest: Layer;
ramps: RampSet;
popoverShadow: Shadow;
modalShadow: Shadow;
players: Players;
}
export interface Player {
cursor: string;
selection: string;
}
export interface Players {
"0": Player;
"1": Player;
"2": Player;
"3": Player;
"4": Player;
"5": Player;
"6": Player;
"7": Player;
}
export interface Shadow {
blur: number;
color: string;
offset: number[];
}
export type StyleSets = keyof Layer;
export interface Layer {
base: StyleSet;
variant: StyleSet;
on: StyleSet;
accent: StyleSet;
positive: StyleSet;
warning: StyleSet;
negative: StyleSet;
}
export interface RampSet {
neutral: Scale;
red: Scale;
orange: Scale;
yellow: Scale;
green: Scale;
cyan: Scale;
blue: Scale;
violet: Scale;
magenta: Scale;
}
export type Styles = keyof StyleSet;
export interface StyleSet {
default: Style;
active: Style;
disabled: Style;
hovered: Style;
pressed: Style;
inverted: Style;
}
export interface Style {
background: string;
border: string;
foreground: string;
}

View file

@ -0,0 +1,202 @@
import chroma, { Color, Scale } from "chroma-js";
import {
ColorScheme,
Layer,
Player,
RampSet,
Style,
Styles,
StyleSet,
} from "./colorScheme";
export function colorRamp(color: Color): Scale {
let endColor = color.desaturate(1).brighten(5);
let startColor = color.desaturate(1).darken(4);
return chroma.scale([startColor, color, endColor]).mode("lab");
}
export function createColorScheme(
name: string,
isLight: boolean,
colorRamps: { [rampName: string]: Scale }
): ColorScheme {
// Chromajs scales from 0 to 1 flipped if isLight is true
let ramps: RampSet = {} as any;
// Chromajs mutates the underlying ramp when you call domain. This causes problems because
// we now store the ramps object in the theme so that we can pull colors out of them.
// So instead of calling domain and storing the result, we have to construct new ramps for each
// theme so that we don't modify the passed in ramps.
// This combined with an error in the type definitions for chroma js means we have to cast the colors
// function to any in order to get the colors back out from the original ramps.
if (isLight) {
for (var rampName in colorRamps) {
(ramps as any)[rampName] = chroma.scale(
colorRamps[rampName].colors(100).reverse()
);
}
ramps.neutral = chroma.scale(colorRamps.neutral.colors(100).reverse());
} else {
for (var rampName in colorRamps) {
(ramps as any)[rampName] = chroma.scale(colorRamps[rampName].colors(100));
}
ramps.neutral = chroma.scale(colorRamps.neutral.colors(100));
}
let lowest = lowestLayer(ramps);
let middle = middleLayer(ramps);
let highest = highestLayer(ramps);
let popoverShadow = {
blur: 4,
color: ramps
.neutral(isLight ? 7 : 0)
.darken()
.alpha(0.2)
.hex(), // TODO used blend previously. Replace with something else
offset: [1, 2],
};
let modalShadow = {
blur: 16,
color: ramps
.neutral(isLight ? 7 : 0)
.darken()
.alpha(0.2)
.hex(), // TODO used blend previously. Replace with something else
offset: [0, 2],
};
let players = {
"0": player(ramps.blue),
"1": player(ramps.green),
"2": player(ramps.magenta),
"3": player(ramps.orange),
"4": player(ramps.violet),
"5": player(ramps.cyan),
"6": player(ramps.red),
"7": player(ramps.yellow),
};
return {
name,
isLight,
ramps,
lowest,
middle,
highest,
popoverShadow,
modalShadow,
players,
};
}
function player(ramp: Scale): Player {
return {
selection: ramp(0.5).alpha(0.24).hex(),
cursor: ramp(0.5).hex(),
};
}
function lowestLayer(ramps: RampSet): Layer {
return {
base: buildStyleSet(ramps.neutral, 0.2, 1),
variant: buildStyleSet(ramps.neutral, 0.2, 0.7),
on: buildStyleSet(ramps.neutral, 0.1, 1),
accent: buildStyleSet(ramps.blue, 0.1, 0.5),
positive: buildStyleSet(ramps.green, 0.1, 0.5),
warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
negative: buildStyleSet(ramps.red, 0.1, 0.5),
};
}
function middleLayer(ramps: RampSet): Layer {
return {
base: buildStyleSet(ramps.neutral, 0.1, 1),
variant: buildStyleSet(ramps.neutral, 0.1, 0.7),
on: buildStyleSet(ramps.neutral, 0, 1),
accent: buildStyleSet(ramps.blue, 0.1, 0.5),
positive: buildStyleSet(ramps.green, 0.1, 0.5),
warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
negative: buildStyleSet(ramps.red, 0.1, 0.5),
};
}
function highestLayer(ramps: RampSet): Layer {
return {
base: buildStyleSet(ramps.neutral, 0, 1),
variant: buildStyleSet(ramps.neutral, 0, 0.7),
on: buildStyleSet(ramps.neutral, 0.1, 1),
accent: buildStyleSet(ramps.blue, 0.1, 0.5),
positive: buildStyleSet(ramps.green, 0.1, 0.5),
warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
negative: buildStyleSet(ramps.red, 0.1, 0.5),
};
}
function buildStyleSet(
ramp: Scale,
backgroundBase: number,
foregroundBase: number,
step: number = 0.08,
): StyleSet {
let styleDefinitions = buildStyleDefinition(backgroundBase, foregroundBase, step);
function colorString(indexOrColor: number | Color): string {
if (typeof indexOrColor === "number") {
return ramp(indexOrColor).hex();
} else {
return indexOrColor.hex();
}
}
function buildStyle(style: Styles): Style {
return {
background: colorString(styleDefinitions.background[style]),
border: colorString(styleDefinitions.border[style]),
foreground: colorString(styleDefinitions.foreground[style]),
};
}
return {
default: buildStyle("default"),
hovered: buildStyle("hovered"),
pressed: buildStyle("pressed"),
active: buildStyle("active"),
disabled: buildStyle("disabled"),
inverted: buildStyle("inverted"),
};
}
function buildStyleDefinition(bgBase: number, fgBase: number, step: number = 0.08) {
return {
background: {
default: bgBase,
hovered: bgBase + step,
pressed: bgBase + step * 1.5,
active: bgBase + step * 2.2,
disabled: bgBase,
inverted: fgBase + step * 6,
},
border: {
default: bgBase + step * 1,
hovered: bgBase + step,
pressed: bgBase + step,
active: bgBase + step * 3,
disabled: bgBase + step * 0.5,
inverted: bgBase - step * 3,
},
foreground: {
default: fgBase,
hovered: fgBase,
pressed: fgBase,
active: fgBase + step * 6,
disabled: bgBase + step * 4,
inverted: bgBase + step * 2,
},
};
}

View file

@ -0,0 +1,30 @@
import chroma from "chroma-js";
import { colorRamp, createColorScheme } from "../common/ramps";
const name = "zed-pro";
const ramps = {
neutral: chroma
.scale([
"#101010",
"#1C1C1C",
"#212121",
"#2D2D2D",
"#B9B9B9",
"#DADADA",
"#E6E6E6",
"#FFFFFF",
])
.domain([0, 0.1, 0.2, 0.3, 0.7, 0.8, 0.9, 1]),
red: colorRamp(chroma("#DC604F")),
orange: colorRamp(chroma("#DE782F")),
yellow: colorRamp(chroma("#E0B750")),
green: colorRamp(chroma("#2A643D")),
cyan: colorRamp(chroma("#215050")),
blue: colorRamp(chroma("#2F6DB7")),
violet: colorRamp(chroma("#5874C1")),
magenta: colorRamp(chroma("#DE9AB8")),
};
export const dark = createColorScheme(`${name}-dark`, false, ramps);
export const light = createColorScheme(`${name}-light`, true, ramps);

View file

@ -1,5 +1,5 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
import { colorRamp, createColorScheme } from "./common/ramps";
const name = "one";
const author = "Chris Kempson (http://chriskempson.com)";
@ -24,16 +24,9 @@ const base0E = "#c678dd";
const base0F = "#be5046";
const ramps = {
neutral: chroma.scale([
base00,
base01,
base02,
base03,
base04,
base05,
base06,
base07,
]),
neutral: chroma
.scale([base00, base01, base02, base03, base04, base05, base06, base07])
.domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
red: colorRamp(chroma(base08)),
orange: colorRamp(chroma(base09)),
yellow: colorRamp(chroma(base0A)),
@ -44,4 +37,4 @@ const ramps = {
magenta: colorRamp(chroma(base0F)),
};
export const dark = createTheme(`${name}-dark`, false, ramps);
export const dark = createColorScheme(`${name}-dark`, false, ramps);

View file

@ -1,5 +1,5 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
import { colorRamp, createColorScheme } from "./common/ramps";
const name = "one";
const author = "Daniel Pfeifer (http://github.com/purpleKarrot)";
@ -24,16 +24,9 @@ const base0E = "#a626a4";
const base0F = "#986801";
const ramps = {
neutral: chroma.scale([
base00,
base01,
base02,
base03,
base04,
base05,
base06,
base07,
]),
neutral: chroma
.scale([base00, base01, base02, base03, base04, base05, base06, base07])
.domain([0, 0.05, 0.77, 1]),
red: colorRamp(chroma(base08)),
orange: colorRamp(chroma(base09)),
yellow: colorRamp(chroma(base0A)),
@ -44,4 +37,4 @@ const ramps = {
magenta: colorRamp(chroma(base0F)),
};
export const light = createTheme(`${name}-light`, true, ramps);
export const light = createColorScheme(`${name}-light`, true, ramps);

View file

@ -1,19 +1,21 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
import { colorRamp, createColorScheme } from "./common/ramps";
const name = "rosé-pine-dawn";
const ramps = {
neutral: chroma.scale([
"#575279",
"#797593",
"#9893A5",
"#B5AFB8",
"#D3CCCC",
"#F2E9E1",
"#FFFAF3",
"#FAF4ED",
]),
neutral: chroma
.scale([
"#575279",
"#797593",
"#9893A5",
"#B5AFB8",
"#D3CCCC",
"#F2E9E1",
"#FFFAF3",
"#FAF4ED",
])
.domain([0, 0.35, 0.45, 0.65, 0.7, 0.8, 0.9, 1]),
red: colorRamp(chroma("#B4637A")),
orange: colorRamp(chroma("#D7827E")),
yellow: colorRamp(chroma("#EA9D34")),
@ -24,4 +26,4 @@ const ramps = {
magenta: colorRamp(chroma("#79549F")),
};
export const light = createTheme(`${name}`, true, ramps);
export const light = createColorScheme(`${name}`, true, ramps);

View file

@ -1,19 +1,21 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
import { colorRamp, createColorScheme } from "./common/ramps";
const name = "rosé-pine-moon";
const ramps = {
neutral: chroma.scale([
"#232136",
"#2A273F",
"#393552",
"#3E3A53",
"#56526C",
"#6E6A86",
"#908CAA",
"#E0DEF4",
]),
neutral: chroma
.scale([
"#232136",
"#2A273F",
"#393552",
"#3E3A53",
"#56526C",
"#6E6A86",
"#908CAA",
"#E0DEF4",
])
.domain([0, 0.3, 0.55, 1]),
red: colorRamp(chroma("#EB6F92")),
orange: colorRamp(chroma("#EBBCBA")),
yellow: colorRamp(chroma("#F6C177")),
@ -24,4 +26,4 @@ const ramps = {
magenta: colorRamp(chroma("#AB6FE9")),
};
export const dark = createTheme(`${name}`, false, ramps);
export const dark = createColorScheme(`${name}`, false, ramps);

View file

@ -1,5 +1,5 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
import { colorRamp, createColorScheme } from "./common/ramps";
const name = "rosé-pine";
@ -24,4 +24,4 @@ const ramps = {
magenta: colorRamp(chroma("#AB6FE9")),
};
export const dark = createTheme(`${name}`, false, ramps);
export const dark = createColorScheme(`${name}`, false, ramps);

View file

@ -1,5 +1,5 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
import { colorRamp, createColorScheme } from "./common/ramps";
const name = "sandcastle";
@ -17,11 +17,11 @@ const ramps = {
red: colorRamp(chroma("#B4637A")),
orange: colorRamp(chroma("#a07e3b")),
yellow: colorRamp(chroma("#a07e3b")),
green: colorRamp(chroma("#528b8b")),
green: colorRamp(chroma("#83a598")),
cyan: colorRamp(chroma("#83a598")),
blue: colorRamp(chroma("#83a598")),
blue: colorRamp(chroma("#528b8b")),
violet: colorRamp(chroma("#d75f5f")),
magenta: colorRamp(chroma("#a87322")),
};
export const dark = createTheme(`${name}`, false, ramps);
export const dark = createColorScheme(`${name}`, false, ramps);

View file

@ -1,19 +1,21 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
import { colorRamp, createColorScheme } from "./common/ramps";
const name = "solarized";
const ramps = {
neutral: chroma.scale([
"#002b36",
"#073642",
"#586e75",
"#657b83",
"#839496",
"#93a1a1",
"#eee8d5",
"#fdf6e3",
]),
neutral: chroma
.scale([
"#002b36",
"#073642",
"#586e75",
"#657b83",
"#839496",
"#93a1a1",
"#eee8d5",
"#fdf6e3",
])
.domain([0, 0.2, 0.38, 0.45, 0.65, 0.7, 0.85, 1]),
red: colorRamp(chroma("#dc322f")),
orange: colorRamp(chroma("#cb4b16")),
yellow: colorRamp(chroma("#b58900")),
@ -24,5 +26,5 @@ const ramps = {
magenta: colorRamp(chroma("#d33682")),
};
export const dark = createTheme(`${name}-dark`, false, ramps);
export const light = createTheme(`${name}-light`, true, ramps);
export const dark = createColorScheme(`${name}-dark`, false, ramps);
export const light = createColorScheme(`${name}-light`, true, ramps);

View file

@ -1,19 +1,21 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
import { colorRamp, createColorScheme } from "./common/ramps";
const name = "sulphurpool";
const ramps = {
neutral: chroma.scale([
"#202746",
"#293256",
"#5e6687",
"#6b7394",
"#898ea4",
"#979db4",
"#dfe2f1",
"#f5f7ff",
]),
neutral: chroma
.scale([
"#202746",
"#293256",
"#5e6687",
"#6b7394",
"#898ea4",
"#979db4",
"#dfe2f1",
"#f5f7ff",
])
.domain([0, 0.2, 0.38, 0.45, 0.65, 0.7, 0.85, 1]),
red: colorRamp(chroma("#c94922")),
orange: colorRamp(chroma("#c76b29")),
yellow: colorRamp(chroma("#c08b30")),
@ -24,5 +26,5 @@ const ramps = {
magenta: colorRamp(chroma("#9c637a")),
};
export const dark = createTheme(`${name}-dark`, false, ramps);
export const light = createTheme(`${name}-light`, true, ramps);
export const dark = createColorScheme(`${name}-dark`, false, ramps);
export const light = createColorScheme(`${name}-light`, true, ramps);

View file

@ -1,19 +1,21 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
import { colorRamp, createColorScheme } from "./common/ramps";
const name = "summercamp";
const ramps = {
neutral: chroma.scale([
"#1c1810",
"#2a261c",
"#3a3527",
"#3a3527",
"#5f5b45",
"#736e55",
"#bab696",
"#f8f5de",
]),
neutral: chroma
.scale([
"#1c1810",
"#2a261c",
"#3a3527",
"#3a3527",
"#5f5b45",
"#736e55",
"#bab696",
"#f8f5de",
])
.domain([0, 0.2, 0.38, 0.4, 0.65, 0.7, 0.85, 1]),
red: colorRamp(chroma("#e35142")),
orange: colorRamp(chroma("#fba11b")),
yellow: colorRamp(chroma("#f2ff27")),
@ -24,4 +26,4 @@ const ramps = {
magenta: colorRamp(chroma("#F69BE7")),
};
export const dark = createTheme(`${name}`, false, ramps);
export const dark = createColorScheme(`${name}`, false, ramps);

View file

@ -1,28 +0,0 @@
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
const name = "summerfruit";
const ramps = {
neutral: chroma.scale([
"#151515",
"#202020",
"#303030",
"#505050",
"#B0B0B0",
"#D0D0D0",
"#E0E0E0",
"#FFFFFF",
]),
red: colorRamp(chroma("#FF0086")),
orange: colorRamp(chroma("#FD8900")),
yellow: colorRamp(chroma("#ABA800")),
green: colorRamp(chroma("#00C918")),
cyan: colorRamp(chroma("#1FAAAA")),
blue: colorRamp(chroma("#3777E6")),
violet: colorRamp(chroma("#AD00A1")),
magenta: colorRamp(chroma("#CC6633")),
};
export const dark = createTheme(`${name}-dark`, false, ramps);
export const light = createTheme(`${name}-light`, true, ramps);

View file

@ -3,7 +3,7 @@
**/
import chroma from "chroma-js";
import { colorRamp, createTheme } from "./common/base16";
import { colorRamp, createColorScheme } from "./common/ramps";
/**
* Theme Name
@ -56,14 +56,14 @@ const ramps = {
};
/**
* Theme Variants
* Color Scheme Variants
*
* Currently we only support (and require) dark and light themes
* Eventually you will be able to have only a light or dark theme,
* and define other variants here.
*
* createTheme([name], [isLight], [arrayOfRamps])
* createColorScheme([name], [isLight], [arrayOfRamps])
**/
export const dark = createTheme(`${name}-dark`, false, ramps);
export const light = createTheme(`${name}-light`, true, ramps);
export const dark = createColorScheme(`${name}-dark`, false, ramps);
export const light = createColorScheme(`${name}-light`, true, ramps);