Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e3c2109
feat: add native Schist Cloud integration
IAmJSD Sep 4, 2026
c2c01d6
fix: align cloud capabilities and downloads with provider spec
IAmJSD Sep 4, 2026
eaebfe0
feat: enable Schist Cloud in the hosted WASM editor
IAmJSD Sep 4, 2026
6fd0afd
refactor: share the document engine between editor and cloud worker
IAmJSD Sep 5, 2026
7c81311
feat: give the cloud gallery the local gallery's chrome, grouping and…
IAmJSD Sep 6, 2026
ba1fdaf
fix: list cloud folders and buckets with the local ones, connect quietly
IAmJSD Sep 6, 2026
d1ce7e8
Show server-provided bucket counts before loading photos
IAmJSD Sep 6, 2026
6757514
Show server-provided folder counts before loading photos
IAmJSD Sep 6, 2026
f18a42e
Keep local and cloud gallery controls consistent
IAmJSD Sep 6, 2026
7b20d65
Remove the cloud filter dialog
IAmJSD Sep 6, 2026
d8e5035
Keep the Schist Cloud library count visible in every view
IAmJSD Sep 6, 2026
8495b2e
Show the add-folder and new-bucket choice menus in the cloud room too
IAmJSD Sep 6, 2026
cea1221
Give cloud buckets the local bucket dialog
IAmJSD Sep 6, 2026
d082e7e
Give cloud bucket rows the local bucket menu
IAmJSD Sep 6, 2026
f006cef
Integrate cloud People and screening after upstream rebase
IAmJSD Sep 6, 2026
0711d5a
Resolve strict lint checks for cloud integration
IAmJSD Sep 6, 2026
a6f37c5
Keep the sidebar whole in the cloud room: menus, map, people
IAmJSD Sep 6, 2026
d7f60ec
Add the asset location field to test fixtures
IAmJSD Sep 6, 2026
425aeaa
Upload and download whole folders from the sidebar menus; reuse one H…
IAmJSD Sep 7, 2026
30fa935
Plot the whole cloud scope on the world map, not one page
IAmJSD Sep 7, 2026
5f7f7a5
Upload drops as compressed batches with a progress bar
IAmJSD Sep 7, 2026
4d8891c
Skip invalid upload files without abandoning the remaining batch
IAmJSD Sep 7, 2026
37f6d31
Upload large cloud files in resumable chunks
IAmJSD Sep 7, 2026
13456e2
Warn before uploading selections that exceed cloud storage
IAmJSD Sep 7, 2026
0e4d426
Find faces automatically without People opt-in dialogs
IAmJSD Sep 7, 2026
bf92594
Show spinners while cloud photo snapshots are loading
IAmJSD Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
314 changes: 313 additions & 1 deletion Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ exclude = ["examples/plugins/sepia-filter", "examples/plugins/pgm-codec"]
members = [
"crates/app",
"crates/core",
"crates/document",
"crates/people-worker",
"crates/layer-fx",
"crates/neural",
"crates/psd-descriptor",
Expand All @@ -29,6 +31,7 @@ members = [
"crates/mcp",
"crates/gallery",
"crates/ui",
"crates/cloud",
"plugins/codecs-common",
"plugins/tools-basic",
"plugins/tools-paint",
Expand All @@ -52,6 +55,7 @@ repository = "https://github.com/Infrawrench/schist"
[workspace.dependencies]
# internal
schist-core = { path = "crates/core" }
schist-document = { path = "crates/document" }
schist-plugin-api = { path = "crates/plugin-api" }
schist-pixel-ops = { path = "crates/pixel-ops" }
schist-compositor = { path = "crates/compositor" }
Expand Down
43 changes: 43 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,47 @@ check-gpu-fx:
$(CARGO) test -p schist-compositor-gpu --test fx_parity --test fx_wiring
check-readme: check-text check-gpu-fx
$(CARGO) clippy -p schist-core -p schist-text-engine -p schist-tools-type -p schist-codec-affinity -p schist-fx -p schist-compositor-gpu --all-targets -- -D warnings
# Native cloud client and editor integration checks.
.PHONY: check-cloud
check-cloud:
$(CARGO) test -p schist-cloud
$(CARGO) test -p schist-app cloud_lifecycle_tests
$(CARGO) check -p schist-app

# Requires wasm-bindgen-test-runner and a browser WebDriver (e.g. CHROMEDRIVER).
.PHONY: check-cloud-wasm
check-cloud-wasm:
$(CARGO) check -p schist-app --target wasm32-unknown-unknown
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner $(CARGO) test -p schist-cloud -p schist-document --target wasm32-unknown-unknown --lib

# Shared desktop/cloud document engine; no GPUI or network client.
.PHONY: document-worker check-document format-document
document-worker:
$(CARGO) build $(PROFILE_FLAG) -p schist-document --bin schist-document-worker
check-document:
$(CARGO) test -p schist-document
format-document:
$(CARGO) fmt -p schist-document -p schist-cloud -p schist-codecs-common

# The cloud adapter uses the same face detector, recogniser and crop as desktop.
.PHONY: people-worker check-people
people-worker:
$(CARGO) build -p schist-people-worker $(if $(filter release,$(PROFILE)),--release,)
check-people:
$(CARGO) test -p schist-gallery people
$(CARGO) test -p schist-neural faces
$(CARGO) check -p schist-people-worker

.PHONY: format-cloud
format-cloud:
$(CARGO) fmt -p schist-cloud -p schist-app -p schist-document -p schist-people-worker

.PHONY: check-gallery check-cloud-browser
check-gallery:
$(CARGO) test -p schist-app
check-cloud-browser:
$(CARGO) check -p schist-app --target wasm32-unknown-unknown

.PHONY: lint-cloud
lint-cloud:
$(CARGO) clippy -p schist-cloud -p schist-app -p schist-document -p schist-people-worker --all-targets -- -D warnings
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ file, never its output.

## What it does

**Schist Cloud.** Sign in from the welcome screen or File menu, with
`schist.app` as the default provider. Live remote folders, searchable buckets,
filters, uploads and collaborative editing share a MessagePack WebSocket.
See [docs/cloud.md](docs/cloud.md) for setup and the provider contract.

**Documents.** PSD and PSB read *and* write — layers, nested groups, masks,
all 27 blend modes, adjustment layers, layer effects, vector shapes,
8/16/32-bit, RGB, greyscale, CMYK, Lab and Indexed, RLE and zip-compressed
Expand Down
12 changes: 8 additions & 4 deletions crates/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ name = "schist"
path = "src/main.rs"

[dependencies]
# Deflate for the gallery's ZIPs and the cloud's gzip batch uploads;
# already in the tree behind the PNG codec, so this adds no download.
flate2 = "1"
schist-cloud = { path = "../cloud" }
web-time = "1"
gpui.workspace = true
anyhow.workspace = true
log.workspace = true
Expand Down Expand Up @@ -76,11 +81,10 @@ async-trait.workspace = true
# Gallery thumbnails: one windowless image of a file. Native-only with the
# rest of the gallery — a browser tab has no folders to browse.
schist-preview.workspace = true
# The gallery's "save bucket as ZIP" writes the archive by hand; these
# are its checksums and its deflate. flate2 is already in the tree
# behind the PNG codec, so this adds no download.
# The gallery's "save bucket as ZIP" writes the archive by hand; this
# is its checksum. Its deflate, flate2, sits in the common dependencies
# since cloud batch uploads gzip on every target.
crc32fast.workspace = true
flate2 = "1"
# The daily ping: its dedup ID, and the CPU model and RAM it reports.
rand = "0.10"
sysinfo = { version = "0.31", default-features = false, features = ["system"] }
Expand Down
1 change: 1 addition & 0 deletions crates/app/assets/icons/loading.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions crates/app/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ actions!(
/// `panels` because [`RunAppItem`] carries one.
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum AppItem {
CloudSignIn,
CloudGenerate,
CloudBrowse,
CloudSignOut,
CloudUpload,
New,
Open,
Close,
Expand Down
1 change: 1 addition & 0 deletions crates/app/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ macro_rules! icons {
}

icons!(
"loading",
"character",
"type-align-left",
"type-align-center",
Expand Down
19 changes: 14 additions & 5 deletions crates/app/src/dialogs/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,20 @@ pub(super) fn batch_dialog(
}

// Output.
let targets: Vec<(SharedString, BatchTarget)> =
[BatchTarget::Edit, BatchTarget::Beside, BatchTarget::Folder]
.into_iter()
.map(|t| (SharedString::from(t.label()), t))
.collect();
// Copies fetched from Schist Cloud live in a scratch folder, so the
// only place results can usefully go is a folder of the user's own.
let from_cloud = photos
.first()
.is_some_and(|p| p.starts_with(crate::workspace::cloud::batch_dir()));
let choices: &[BatchTarget] = if from_cloud {
&[BatchTarget::Folder]
} else {
&[BatchTarget::Edit, BatchTarget::Beside, BatchTarget::Folder]
};
let targets: Vec<(SharedString, BatchTarget)> = choices
.iter()
.map(|&t| (SharedString::from(t.label()), t))
.collect();
body = body.child(section("Output")).child(ui::field_row(
"Save as",
ui::dropdown(
Expand Down
8 changes: 8 additions & 0 deletions crates/app/src/dialogs/close.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ pub(super) fn confirm_close_tab(
ws.close_tab_after_save();
ws.save_current(window, cx);
if ws.has_pending_save() {
if ws
.doc
.as_ref()
.is_some_and(|doc| ws.cloud.docs.contains_key(&doc.id))
{
// The cloud acknowledgement resumes the pending quit.
return;
}
// Still waiting on a file prompt. Do not hold a
// quit open across it; the tab closes when the
// save lands.
Expand Down
13 changes: 7 additions & 6 deletions crates/app/src/dialogs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ pub fn render(ws: &mut Workspace, cx: &mut Context<Workspace>) -> Option<gpui::A
// action while it builds, so Enter can fire it.
ui::reset_default_action();
let body = match modal {
Modal::CloudGenerate => crate::workspace::cloud_generation::dialog(ws, cx),
Modal::Cloud { kind, fields } => crate::workspace::cloud_view::dialog(ws, kind, fields, cx),
Modal::ImageSize {
width,
height,
Expand Down Expand Up @@ -223,10 +225,6 @@ pub fn render(ws: &mut Workspace, cx: &mut Context<Workspace>) -> Option<gpui::A
#[cfg(not(target_arch = "wasm32"))]
Modal::SearchModels => crate::workspace::search_models_dialog(cx).into_any_element(),
#[cfg(not(target_arch = "wasm32"))]
Modal::PeopleModels => crate::workspace::people_models_dialog(cx).into_any_element(),
#[cfg(target_arch = "wasm32")]
Modal::PeopleModels => return None,
#[cfg(not(target_arch = "wasm32"))]
Modal::PersonName { index, name } => {
crate::workspace::person_name_dialog(ws, index, name, cx).into_any_element()
}
Expand Down Expand Up @@ -264,8 +262,11 @@ pub fn render(ws: &mut Workspace, cx: &mut Context<Workspace>) -> Option<gpui::A
query,
photos,
editing,
} => crate::workspace::bucket_name_dialog(ws, name, query, photos.len(), editing, cx)
.into_any_element(),
cloud,
} => {
crate::workspace::bucket_name_dialog(ws, name, query, photos.len(), editing, cloud, cx)
.into_any_element()
}
#[cfg(target_arch = "wasm32")]
Modal::BucketName { .. } => return None,
m @ Modal::NewDocument { .. } => new_document_dialog(&state, m, cx).into_any_element(),
Expand Down
67 changes: 28 additions & 39 deletions crates/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,49 +49,13 @@ use gpui::{
px, size, App, AppContext as _, Application, AsyncApp, Bounds, TitlebarOptions, WindowBounds,
WindowHandle, WindowOptions,
};
use schist_plugin_api::{CodecPlugin, PluginManifest, PluginRegistry};
use schist_plugin_api::{PluginManifest, PluginRegistry};
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use workspace::Workspace;

/// PSD/PSB import and export via `schist-codec-psd`.
struct PsdCodec;

impl CodecPlugin for PsdCodec {
fn id(&self) -> &'static str {
"codec.psd"
}
fn name(&self) -> &'static str {
"Photoshop PSD"
}
fn extensions(&self) -> &'static [&'static str] {
&["psd", "psb"]
}
fn probe(&self, bytes: &[u8]) -> bool {
schist_codec_psd::is_psd(bytes)
}
fn import(&self, bytes: &[u8]) -> anyhow::Result<schist_core::Document> {
Ok(schist_codec_psd::read_psd(bytes)?)
}
fn can_export(&self) -> bool {
true
}
fn export(&self, doc: &schist_core::Document) -> anyhow::Result<Vec<u8>> {
Ok(schist_codec_psd::write_psd(doc)?)
}
}

struct PsdPlugin;

impl PluginManifest for PsdPlugin {
fn id(&self) -> &'static str {
"schist.codec-psd"
}
fn register(&self, registry: &mut PluginRegistry) {
registry.register_codec(Box::new(PsdCodec));
}
}
pub use schist_codecs_common::{PsdCodec, PsdPlugin};

/// Whether an opt-in diagnostic is on: the preference, or the environment
/// variable that overrides it for one run.
Expand Down Expand Up @@ -232,6 +196,21 @@ fn path_from_url(url: &str) -> Option<PathBuf> {
}

fn main() {
#[cfg(not(target_arch = "wasm32"))]
if let Some(callback) = std::env::args()
.skip(1)
.find(|arg| arg.starts_with("schist://"))
{
let dir = schist_gallery::state_dir()
.unwrap_or_else(std::env::temp_dir)
.join("schist/cloud");
if let Err(error) = schist_cloud::auth::forward_callback(&callback, &dir) {
eprintln!("Cloud sign-in callback failed: {error}");
std::process::exit(1);
}
return;
}

// `schist --mcp-bridge <addr>` is not a GUI launch at all: it is the
// stdio pump an agent harness spawns as its "MCP server", forwarding
// into the running app's loopback endpoint. Handled before anything
Expand Down Expand Up @@ -308,6 +287,15 @@ fn main() {
app.on_open_urls({
let requests = requests.clone();
move |urls| {
#[cfg(not(target_arch = "wasm32"))]
for callback in urls.iter().filter(|url| url.starts_with("schist://")) {
let dir = schist_gallery::state_dir()
.unwrap_or_else(std::env::temp_dir)
.join("schist/cloud");
if let Err(error) = schist_cloud::auth::forward_callback(callback, &dir) {
log::error!("Cloud callback failed: {error}");
}
}
let paths: Vec<PathBuf> = urls.iter().filter_map(|u| path_from_url(u)).collect();
if paths.is_empty() {
return;
Expand Down Expand Up @@ -379,7 +367,8 @@ fn main() {
ws.recover_all(recoveries, cx);
}
if let Some(path) = std::env::args().nth(1) {
ws.load_file(path.into(), cx);
let path = path_from_url(&path).unwrap_or_else(|| path.into());
ws.load_file(path, cx);
} else if ws.tab_count() == 0 {
// Picasa boot: a launch with nothing to
// open lands in the gallery, empty or
Expand Down
6 changes: 6 additions & 0 deletions crates/app/src/native_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ fn signature(ws: &Workspace) -> String {
out.push('\u{1f}');
out.push_str(&recent.to_string_lossy());
}
#[cfg(not(target_arch = "wasm32"))]
out.push_str(if ws.cloud.account.is_some() {
"cloud-in"
} else {
"cloud-out"
});
out
}

Expand Down
15 changes: 15 additions & 0 deletions crates/app/src/panels/menu_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ pub(crate) fn run_app_item(
cx: &mut Context<Workspace>,
) {
match item {
AppItem::CloudGenerate => {
ws.cloud_generate_open(cx);
}
AppItem::CloudSignIn => {
ws.cloud_sign_in(cx);
}
AppItem::CloudBrowse => {
ws.cloud_browse(schist_cloud::Scope::Library, cx);
}
AppItem::CloudSignOut => {
ws.cloud_sign_out(cx);
}
AppItem::CloudUpload => {
ws.cloud_upload_document(cx);
}
AppItem::New => ws.open_new_file_picker(cx),
AppItem::Open => crate::keymap::open_file_dialog(ws, window, cx),
AppItem::Close => ws.request_close_tab(ws.active_tab(), cx),
Expand Down
16 changes: 16 additions & 0 deletions crates/app/src/panels/menus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub(crate) fn menus(ws: &Workspace) -> Vec<(&'static str, Vec<MenuEntry>)> {
vec![
App("New", New, Some("cmd-n")),
App("Open…", Open, Some("cmd-o")),
Sub("Schist Cloud", cloud_entries(ws)),
App("Browse Gallery…", OpenGallery, Some("cmd-shift-g")),
App("Close", Close, Some("cmd-w")),
App("Save", Save, Some("cmd-s")),
Expand Down Expand Up @@ -319,6 +320,7 @@ fn gallery_menus(ws: &Workspace) -> Vec<(&'static str, Vec<MenuEntry>)> {
let mut file = vec![
App("New", New, Some("cmd-n")),
App("Open…", Open, Some("cmd-o")),
Sub("Schist Cloud", cloud_entries(ws)),
];
let recents = recent_entries(ws);
if !recents.is_empty() {
Expand Down Expand Up @@ -656,3 +658,17 @@ pub(super) const FILTER_GROUPS: &[(&str, &[&str])] = &[
],
),
];
fn cloud_entries(ws: &Workspace) -> Vec<MenuEntry> {
use AppItem::*;
use MenuEntry::*;
if ws.cloud.account.is_some() {
vec![
App("Browse Schist Cloud", CloudBrowse, None),
App("Generate images…", CloudGenerate, None),
App("Upload document to Schist Cloud…", CloudUpload, None),
App("Sign out of Schist Cloud", CloudSignOut, None),
]
} else {
vec![App("Sign into Schist Cloud…", CloudSignIn, None)]
}
}
Loading
Loading