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

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)
);
}