use crate::{
agent,
data::{ColType, Column, Data, Edit, Page, Table},
memory,
model::{
article::{self, Article},
project::Project,
session::ChatSession,
settings::{self, Feature, Settings},
state::{self, State},
update,
watch::{self, Watch},
},
};
use artifact::{
board::Board,
project::{Project as _, fs},
};
use bezel::{
gpui::{App, ClipboardItem, Context, EntityId, EventEmitter, Window},
theme::{self, Brand, Tint, Vibrancy, appearance::AppearanceMode},
ui::{icons::Icon, input},
};
use cacp::schema::SessionConfigOptionValue;
use editor::Mode;
use futures::{StreamExt as _, channel::mpsc};
use mcp::rail::{self, Change};
use std::{
collections::{BTreeMap, HashMap},
path::{Path, PathBuf},
};
mod articles;
mod boards;
mod layouts;
pub use layouts::Showing;
mod order;
mod projects;
mod sessions;
mod tables;
const UNTITLED: &str = "Untitled";
const COLUMN: &str = "Column";
pub struct Reloaded;
pub struct Resident {
pub projects: usize,
pub articles: usize,
pub editors: usize,
pub covers: usize,
pub sessions: usize,
pub items: usize,
}
pub struct Workspace {
pub settings: Settings,
pub projects: Vec<Project>,
pub active: Option<usize>,
pub appearance: AppearanceMode,
pub opaque: Option<bool>,
pub cursor_blink: bool,
next_id: u64,
pub text_size: f32,
pub article_font_size: Option<f32>,
pub terminal_font_size: f32,
pub file_font_size: f32,
pub tint: Tint,
pub wide_pages: bool,
pub indent_project_rows: bool,
pub wrap_code: bool,
pub meter: bool,
agent_icons: HashMap<String, Icon>,
last: BTreeMap<PathBuf, state::Entry>,
pub(super) order: BTreeMap<PathBuf, Vec<state::Entry>>,
pub(super) pinned: BTreeMap<PathBuf, Vec<state::Entry>>,
pub layouts: Vec<artifact::layout::Layout>,
pub layout: Option<usize>,
}
impl Workspace {
pub fn new(settings: Settings, state: State, cx: &mut Context<Self>) -> Self {
let projects: Vec<Project> = state
.projects
.into_iter()
.map(|path| {
let expanded = !state.collapsed.contains(&path);
let mut project = Project::new(path);
project.expanded = expanded;
project
})
.collect();
let active = (!projects.is_empty()).then_some(state.active);
let restore: Vec<usize> = (0..projects.len()).collect();
let look = settings.appearance;
bezel::ui::scroll::set_visibility(look.scrollbars.into(), cx);
editor::set_text_size(
cx,
editor::TextSize {
step: 1.,
min: settings::CONTENT_TEXT_SIZE.0,
max: settings::CONTENT_TEXT_SIZE.1,
},
);
crate::model::typography::set_terminal_size(look.terminal_font_size, cx);
crate::model::typography::set_file_size(look.file_font_size, cx);
let mut this = Self {
settings,
projects,
active,
appearance: look.mode,
opaque: look.opaque,
cursor_blink: look.cursor_blink,
text_size: look.text_size,
article_font_size: look.article_font_size,
terminal_font_size: look.terminal_font_size,
file_font_size: look.file_font_size,
tint: Tint::new(look.hue, look.chroma),
wide_pages: look.wide_pages,
indent_project_rows: look.indent_project_rows,
wrap_code: look.wrap_code,
meter: false,
next_id: 0,
agent_icons: HashMap::new(),
last: state.last,
order: state.order,
pinned: state.pinned,
layouts: crate::model::layouts::all(),
layout: None,
};
for ix in restore {
this.restore_sessions(ix);
this.watch_project(ix, cx);
}
this.open_last_entry(cx);
this.load_agent_icons(cx);
this.refresh_door();
this.take_rail(cx);
if let Ok(prompt) = std::env::var("CYDONIA_TEST_PROMPT") {
let id = this.active_id().or_else(|| {
let entry = this.settings.agents.first().cloned()?;
this.new_session(entry, None, cx)
});
if let Some(id) = id {
this.send(id, prompt, cx);
}
}
this
}
fn save(&self) {
rail::set_open(self.paths());
state::save(&State {
projects: self.paths(),
active: self.active.unwrap_or_default(),
collapsed: self
.projects
.iter()
.filter(|project| !project.expanded)
.map(|project| project.path.clone())
.collect(),
last: self.last.clone(),
order: self.order.clone(),
pinned: self.pinned.clone(),
});
}
fn save_appearance(&self) {
let _ = settings::set_appearance(&settings::Appearance {
mode: self.appearance,
opaque: self.opaque,
cursor_blink: self.cursor_blink,
text_size: self.text_size,
article_font_size: self.article_font_size,
terminal_font_size: self.terminal_font_size,
file_font_size: self.file_font_size,
hue: self.tint.hue,
chroma: self.tint.chroma,
wide_pages: self.wide_pages,
indent_project_rows: self.indent_project_rows,
scrollbars: self.settings.appearance.scrollbars,
sidebar_scrollbars: self.settings.appearance.sidebar_scrollbars,
wrap_code: self.wrap_code,
});
}
fn load_agent_icons(&mut self, cx: &mut Context<Self>) {
let configured = self.settings.agents.clone();
cx.spawn(async move |this, cx| {
let icons = cx
.background_executor()
.spawn(async move { agent::icons(&configured) })
.await;
let _ = this.update(cx, |workspace, cx| {
workspace.agent_icons = icons;
cx.notify();
});
})
.detach();
}
pub fn agent_icon(&self, name: &str) -> Option<Icon> {
self.agent_icons.get(name).cloned()
}
pub fn preferred_agent(&self) -> Option<settings::Agent> {
self.active_session()
.and_then(|chat| self.agent_for(&chat.entry))
.or_else(|| self.settings.agents.first())
.cloned()
}
pub(super) fn agent_for(&self, held: &settings::Agent) -> Option<&settings::Agent> {
named(&self.settings.agents, held.id.as_deref(), &held.name)
}
pub fn reload_settings(&mut self, cx: &mut Context<Self>) {
if let Ok(settings) = settings::load() {
self.settings = settings;
self.readopt_agents();
}
cx.notify();
}
fn readopt_agents(&mut self) {
let agents = &self.settings.agents;
for project in &mut self.projects {
for chat in &mut project.sessions {
readopt(agents, &mut chat.entry);
}
}
}
pub fn set_appearance(&mut self, mode: AppearanceMode, cx: &mut Context<Self>) {
self.appearance = mode;
bezel::theme::appearance::set_mode(mode, cx);
self.save_appearance();
cx.notify();
}
pub fn set_opaque(&mut self, opaque: bool, cx: &mut Context<Self>) {
self.opaque = Some(opaque);
apply_transparency(self.opaque, cx);
self.save_appearance();
cx.notify();
}
pub fn set_feature(&mut self, feature: Feature, on: bool, cx: &mut Context<Self>) {
if settings::set_feature(feature, on).is_err() {
return;
}
feature.set(&mut self.settings.features, on);
self.refresh_door();
cx.notify();
}
pub fn set_shortcut(&mut self, key: &str, chord: Option<&str>, cx: &mut Context<Self>) {
if settings::set_shortcut(key, chord).is_err() {
return;
}
self.settings.shortcuts.set(key, chord);
cx.notify();
}
pub fn set_emacs_shortcuts(&mut self, on: bool, cx: &mut Context<Self>) {
if settings::set_emacs_shortcuts(on).is_err() {
return;
}
self.settings.shortcuts.emacs = on;
cx.notify();
}
fn refresh_door(&self) {
agent::serve::serve(self.settings.mcp.serve && self.settings.features.sessions);
agent::serve::set_write(self.settings.mcp.write);
}
pub fn set_mcp_serve(&mut self, on: bool, cx: &mut Context<Self>) {
if settings::set_mcp("serve", on).is_err() {
return;
}
self.settings.mcp.serve = on;
self.refresh_door();
cx.notify();
}
pub fn set_mcp_write(&mut self, on: bool, cx: &mut Context<Self>) {
if settings::set_mcp("write", on).is_err() {
return;
}
self.settings.mcp.write = on;
self.refresh_door();
cx.notify();
}
pub fn mcp_url(&self) -> Option<String> {
agent::serve::url()
}
pub fn set_notify_turns(&mut self, on: bool, cx: &mut Context<Self>) {
if settings::set_notify_turns(on).is_err() {
return;
}
self.settings.notify_turns = on;
cx.notify();
}
pub fn set_auto_update(&mut self, on: bool, cx: &mut Context<Self>) {
if settings::set_auto_update(on).is_err() {
return;
}
self.settings.auto_update = on;
if let Some(updater) = update::of(cx) {
updater.update(cx, |updater, cx| updater.set_auto(on, cx));
}
cx.notify();
}
pub fn set_cover_memory(&mut self, mb: u64, window: &mut Window, cx: &mut Context<Self>) {
if settings::set_cover_memory(mb).is_err() {
return;
}
self.settings.cover_memory = mb;
memory::covers(cx).update(cx, |covers, cx| {
covers.set_limit(mb * 1_000_000, window, cx);
});
cx.notify();
}
pub fn set_watch_bounce(&mut self, ms: u64, cx: &mut Context<Self>) {
let ms = ms.clamp(watch::BOUNCE_RANGE.0, watch::BOUNCE_RANGE.1);
if settings::set_watch_bounce(ms).is_err() {
return;
}
self.settings.watch_bounce = ms;
cx.notify();
}
pub fn set_cursor_blink(&mut self, blink: bool, cx: &mut Context<Self>) {
self.cursor_blink = blink;
input::set_caret_blink(blink, cx);
self.save_appearance();
cx.notify();
}
pub fn set_text_size(&mut self, points: f32, cx: &mut Context<Self>) {
self.text_size = points;
theme::set_base_text_size(points, cx);
if self.article_font_size.is_none() {
self.apply_article_font_size(cx);
}
self.save_appearance();
cx.notify();
}
pub fn article_font_size(&self) -> f32 {
self.article_font_size.unwrap_or(self.text_size)
}
fn apply_article_font_size(&self, cx: &mut Context<Self>) {
let points = self.article_font_size();
for editor in self
.projects
.iter()
.flat_map(|project| &project.articles)
.filter_map(|article| article.editor.as_ref())
{
editor.update(cx, |editor, cx| editor.set_text_size(points, cx));
}
}
pub fn set_article_font_size(&mut self, points: f32, cx: &mut Context<Self>) {
self.article_font_size = Some(settings::clamp_content_text_size(points));
self.apply_article_font_size(cx);
self.save_appearance();
cx.notify();
}
pub fn set_file_font_size(&mut self, points: f32, cx: &mut Context<Self>) {
self.file_font_size = settings::clamp_content_text_size(points);
crate::model::typography::set_file_size(self.file_font_size, cx);
self.save_appearance();
cx.notify();
}
pub fn set_terminal_font_size(&mut self, points: f32, cx: &mut Context<Self>) {
self.terminal_font_size = settings::clamp_content_text_size(points);
crate::model::typography::set_terminal_size(self.terminal_font_size, cx);
self.save_appearance();
cx.notify();
}
pub fn set_wide_pages(&mut self, wide: bool, cx: &mut Context<Self>) {
self.wide_pages = wide;
self.save_appearance();
cx.notify();
}
pub fn set_scrollbars(
&mut self,
value: settings::Scrollbars,
sidebar: bool,
cx: &mut Context<Self>,
) {
let look = &mut self.settings.appearance;
if sidebar {
look.sidebar_scrollbars = value;
} else {
look.scrollbars = value;
}
bezel::ui::scroll::set_visibility(look.scrollbars.into(), cx);
self.save_appearance();
cx.refresh_windows();
cx.notify();
}
pub fn set_indent_project_rows(&mut self, indent: bool, cx: &mut Context<Self>) {
self.indent_project_rows = indent;
self.save_appearance();
cx.notify();
}
pub fn set_wrap_code(&mut self, wrap: bool, cx: &mut Context<Self>) {
self.wrap_code = wrap;
apply_wrap_code(wrap, cx);
self.save_appearance();
cx.notify();
}
pub fn set_tint(&mut self, tint: Tint, cx: &mut Context<Self>) {
self.tint = tint;
apply_tint(tint, cx);
self.save_appearance();
cx.notify();
}
pub fn resident(&self, cx: &App) -> Resident {
let articles = || self.projects.iter().flat_map(|project| &project.articles);
let sessions = || self.projects.iter().flat_map(|project| &project.sessions);
Resident {
projects: self.projects.len(),
articles: articles().count(),
editors: articles()
.filter(|article| article.editor.is_some())
.count(),
covers: memory::covers(cx).read(cx).len(),
sessions: sessions().count(),
items: sessions().map(|chat| chat.items.len()).sum(),
}
}
}
impl EventEmitter<Reloaded> for Workspace {}
pub fn readopt(agents: &[settings::Agent], entry: &mut settings::Agent) {
if let Some(found) = named(agents, entry.id.as_deref(), &entry.name) {
*entry = found.clone();
}
}
pub fn named<'a>(
agents: &'a [settings::Agent],
id: Option<&str>,
name: &str,
) -> Option<&'a settings::Agent> {
id.and_then(|id| agents.iter().find(|agent| agent.id.as_deref() == Some(id)))
.or_else(|| agents.iter().find(|agent| agent.name == name))
}
pub fn apply_tint(tint: Tint, cx: &mut App) {
theme::set_brand(
Brand {
tint,
..theme::brand(cx)
},
cx,
);
}
pub fn vibrancy(opaque: Option<bool>) -> Vibrancy {
match opaque {
Some(true) => Vibrancy::Off,
None | Some(false) => Vibrancy::Auto,
}
}
pub fn apply_wrap_code(wrap: bool, cx: &mut App) {
markdown::set_layout(cx, markdown::Layout { wrap_code: wrap });
}
pub fn apply_transparency(opaque: Option<bool>, cx: &mut App) {
theme::set_brand(
Brand {
vibrancy: vibrancy(opaque),
..theme::brand(cx)
},
cx,
);
}