initial state

This commit is contained in:
jess 2026-04-04 21:49:37 -07:00
commit 1f727923d4
22 changed files with 1820 additions and 0 deletions

32
Info.plist Normal file
View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>Swiftly</string>
<key>CFBundleIdentifier</key>
<string>org.else-if.swiftly</string>
<key>CFBundleName</key>
<string>Swiftly</string>
<key>CFBundleDisplayName</key>
<string>Swiftly</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSSupportsAutomaticTermination</key>
<false/>
<key>NSSupportsSuddenTermination</key>
<false/>
</dict>
</plist>

12
LICENCE Normal file
View File

@ -0,0 +1,12 @@
This is free to use, without conditions.
There is no licence here on purpose. Individuals, students, hobbyists — take what
you need, make it yours, don't think twice. You'd flatter me.
The absence of a licence is deliberate. A licence is a legal surface. Words can be
reinterpreted, and corporations employ lawyers whose job is exactly that. Silence is
harder to exploit than language. If a company wants to use this, the lack of explicit
permission makes it just inconvenient enough to matter.
This won't change the balance of power. But it shifts the weight, even slightly, away
from the system that co-opts open work for closed profit. That's enough for me.

59
build.sh Executable file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
BUILD="$ROOT/build"
APP="$BUILD/bin/Swiftly.app"
CONTENTS="$APP/Contents"
MACOS="$CONTENTS/MacOS"
RESOURCES="$CONTENTS/Resources"
SDK=$(xcrun --show-sdk-path)
RUST_LIB="$ROOT/core/target/release"
echo "Building Rust core (release)..."
cd "$ROOT/core" && cargo build --release
cd "$ROOT"
RUST_FLAGS="-import-objc-header $ROOT/core/include/swiftly.h -L $RUST_LIB -lswiftly_core"
# --- App icon from SVG ---
SVG="$ROOT/static/vectors/icon.svg"
if [ -f "$SVG" ]; then
echo "Generating app icon..."
ICONSET="$BUILD/AppIcon.iconset"
mkdir -p "$ICONSET"
for SIZE in 16 32 64 128 256 512; do
rsvg-convert -w $SIZE -h $SIZE "$SVG" -o "$ICONSET/icon_${SIZE}x${SIZE}.png"
done
for SIZE in 16 32 128 256; do
DOUBLE=$((SIZE * 2))
rsvg-convert -w $DOUBLE -h $DOUBLE "$SVG" -o "$ICONSET/icon_${SIZE}x${SIZE}@2x.png"
done
rsvg-convert -w 1024 -h 1024 "$SVG" -o "$ICONSET/icon_512x512@2x.png"
iconutil -c icns "$ICONSET" -o "$BUILD/AppIcon.icns"
rm -rf "$ICONSET"
fi
# --- Bundle structure ---
mkdir -p "$MACOS" "$RESOURCES"
cp "$ROOT/Info.plist" "$CONTENTS/Info.plist"
if [ -f "$BUILD/AppIcon.icns" ]; then
cp "$BUILD/AppIcon.icns" "$RESOURCES/AppIcon.icns"
fi
# --- Compile Swift ---
echo "Compiling Swift (release)..."
swiftc \
-target arm64-apple-macosx14.0 \
-sdk "$SDK" \
$RUST_FLAGS \
-framework Cocoa \
-framework SwiftUI \
-O \
-o "$MACOS/Swiftly" \
"$ROOT"/src/*.swift
# --- Code sign ---
codesign --force --deep --sign - "$APP"
echo "Built: $APP"

20
core/Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "swiftly-core"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["staticlib", "rlib"]
[dependencies]
cord-expr = { path = "../../Cord/crates/cord-expr" }
cord-trig = { path = "../../Cord/crates/cord-trig" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4"] }
[build-dependencies]
cbindgen = "0.27"
[profile.release]
panic = "abort"

21
core/build.rs Normal file
View File

@ -0,0 +1,21 @@
fn main() {
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let config = cbindgen::Config::from_file("cbindgen.toml")
.unwrap_or_default();
match cbindgen::Builder::new()
.with_crate(&crate_dir)
.with_config(config)
.generate()
{
Ok(bindings) => {
let path = format!("{}/include/swiftly.h", crate_dir);
bindings.write_to_file(&path);
println!("cargo:warning=cbindgen: wrote {}", path);
}
Err(e) => {
println!("cargo:warning=cbindgen: {}", e);
}
}
}

17
core/cbindgen.toml Normal file
View File

@ -0,0 +1,17 @@
language = "C"
header = "/* Generated by cbindgen — do not edit */"
include_guard = "SWIFTLY_H"
include_version = false
tab_width = 4
documentation = false
style = "both"
[export]
include = []
exclude = []
[fn]
prefix = ""
[parse]
parse_deps = false

37
core/include/swiftly.h Normal file
View File

@ -0,0 +1,37 @@
/* Generated by cbindgen — do not edit */
#ifndef SWIFTLY_H
#define SWIFTLY_H
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
typedef struct SwiftlyDoc SwiftlyDoc;
struct SwiftlyDoc *swiftly_doc_new(void);
void swiftly_doc_free(struct SwiftlyDoc *doc);
void swiftly_doc_set_text(struct SwiftlyDoc *doc, const char *text);
char *swiftly_doc_get_text(const struct SwiftlyDoc *doc);
char *swiftly_doc_evaluate(struct SwiftlyDoc *doc);
char *swiftly_eval_line(const char *text);
bool swiftly_doc_save(const struct SwiftlyDoc *doc, const char *path);
struct SwiftlyDoc *swiftly_doc_load(const char *path);
char *swiftly_cache_save(const struct SwiftlyDoc *doc);
struct SwiftlyDoc *swiftly_cache_load(const char *uuid);
char *swiftly_list_notes(void);
void swiftly_free_string(char *s);
#endif /* SWIFTLY_H */

135
core/src/doc.rs Normal file
View File

@ -0,0 +1,135 @@
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum LineKind {
Markdown,
Cordial,
Eval,
Comment,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassifiedLine {
pub index: usize,
pub kind: LineKind,
pub content: String,
}
pub fn classify_line(index: usize, raw: &str) -> ClassifiedLine {
let trimmed = raw.trim();
let kind = if trimmed.starts_with("/=") {
LineKind::Eval
} else if trimmed.starts_with("//") {
LineKind::Comment
} else if is_cordial(trimmed) {
LineKind::Cordial
} else {
LineKind::Markdown
};
ClassifiedLine {
index,
kind,
content: raw.to_string(),
}
}
fn is_cordial(line: &str) -> bool {
if line.starts_with("let ") {
return true;
}
// variable assignment: identifier = expr (but not ==)
if let Some(eq_pos) = line.find('=') {
if eq_pos > 0 {
let before = &line[..eq_pos];
let after_eq = line.as_bytes().get(eq_pos + 1);
if after_eq != Some(&b'=') && !before.ends_with('!') && !before.ends_with('<') && !before.ends_with('>') {
let candidate = before.trim();
if is_assignment_target(candidate) {
return true;
}
}
}
}
false
}
fn is_assignment_target(s: &str) -> bool {
// simple variable: `x`
if is_ident(s) {
return true;
}
// function def: `f(x)` or `f(x, y)`
if let Some(paren) = s.find('(') {
let name = &s[..paren];
if is_ident(name) && s.ends_with(')') {
return true;
}
}
false
}
fn is_ident(s: &str) -> bool {
if s.is_empty() { return false; }
let mut chars = s.chars();
let first = chars.next().unwrap();
if !first.is_alphabetic() && first != '_' { return false; }
chars.all(|c| c.is_alphanumeric() || c == '_')
}
pub fn classify_document(text: &str) -> Vec<ClassifiedLine> {
text.lines()
.enumerate()
.map(|(i, line)| classify_line(i, line))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn markdown_line() {
let c = classify_line(0, "# Hello World");
assert_eq!(c.kind, LineKind::Markdown);
}
#[test]
fn eval_line() {
let c = classify_line(0, "/= 2 + 3");
assert_eq!(c.kind, LineKind::Eval);
}
#[test]
fn comment_line() {
let c = classify_line(0, "// this is a comment");
assert_eq!(c.kind, LineKind::Comment);
}
#[test]
fn let_binding() {
let c = classify_line(0, "let x = 5");
assert_eq!(c.kind, LineKind::Cordial);
}
#[test]
fn variable_assignment() {
let c = classify_line(0, "x = 5");
assert_eq!(c.kind, LineKind::Cordial);
}
#[test]
fn function_def() {
let c = classify_line(0, "f(x) = x^2");
assert_eq!(c.kind, LineKind::Cordial);
}
#[test]
fn plain_text() {
let c = classify_line(0, "Some notes about the project");
assert_eq!(c.kind, LineKind::Markdown);
}
}

39
core/src/document.rs Normal file
View File

@ -0,0 +1,39 @@
use crate::doc::{classify_document, ClassifiedLine};
use crate::eval::{evaluate_document, DocumentResult};
pub struct SwiftlyDoc {
pub text: String,
pub uuid: String,
lines: Vec<ClassifiedLine>,
}
impl SwiftlyDoc {
pub fn new() -> Self {
SwiftlyDoc {
text: String::new(),
uuid: uuid::Uuid::new_v4().to_string(),
lines: Vec::new(),
}
}
pub fn with_uuid(uuid: String) -> Self {
SwiftlyDoc {
text: String::new(),
uuid,
lines: Vec::new(),
}
}
pub fn set_text(&mut self, text: &str) {
self.text = text.to_string();
self.lines = classify_document(text);
}
pub fn classified_lines(&self) -> &[ClassifiedLine] {
&self.lines
}
pub fn evaluate(&self) -> DocumentResult {
evaluate_document(&self.text)
}
}

147
core/src/eval.rs Normal file
View File

@ -0,0 +1,147 @@
use serde::Serialize;
use crate::doc::{classify_document, LineKind};
#[derive(Debug, Clone, Serialize)]
pub struct EvalResult {
pub line: usize,
pub result: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct EvalError {
pub line: usize,
pub error: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct DocumentResult {
pub results: Vec<EvalResult>,
pub errors: Vec<EvalError>,
}
pub fn evaluate_document(text: &str) -> DocumentResult {
let classified = classify_document(text);
let mut cordial_lines: Vec<String> = Vec::new();
let mut results = Vec::new();
let mut errors = Vec::new();
for cl in &classified {
match cl.kind {
LineKind::Cordial => {
cordial_lines.push(cl.content.clone());
}
LineKind::Eval => {
let expr = cl.content.trim().strip_prefix("/=").unwrap_or("").trim();
if expr.is_empty() {
errors.push(EvalError {
line: cl.index,
error: "empty expression".into(),
});
continue;
}
let mut eval_program = cordial_lines.clone();
eval_program.push(expr.to_string());
let program = eval_program.join("\n");
match cord_expr::parse_expr(&program) {
Ok(graph) => {
let val = cord_trig::eval::evaluate(&graph, 0.0, 0.0, 0.0);
results.push(EvalResult {
line: cl.index,
result: format_value(val),
});
}
Err(e) => {
errors.push(EvalError {
line: cl.index,
error: e,
});
}
}
}
LineKind::Comment | LineKind::Markdown => {}
}
}
DocumentResult { results, errors }
}
pub fn evaluate_line(text: &str) -> Result<String, String> {
let graph = cord_expr::parse_expr(text)?;
let val = cord_trig::eval::evaluate(&graph, 0.0, 0.0, 0.0);
Ok(format_value(val))
}
fn format_value(val: f64) -> String {
if val == val.trunc() && val.abs() < 1e15 {
format!("{}", val as i64)
} else {
let s = format!("{:.10}", val);
let s = s.trim_end_matches('0');
let s = s.trim_end_matches('.');
s.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_eval() {
let result = evaluate_line("2 + 3").unwrap();
assert_eq!(result, "5");
}
#[test]
fn eval_with_variables() {
let doc = "let a = 5\nlet b = 3\n/= a + b";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].result, "8");
assert_eq!(result.results[0].line, 2);
}
#[test]
fn eval_with_markdown() {
let doc = "# Title\nlet val = 10\nSome text\n/= val * 2";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].result, "20");
}
#[test]
fn eval_trig() {
let result = evaluate_line("sin(0)").unwrap();
assert_eq!(result, "0");
}
#[test]
fn eval_function_def() {
let doc = "f(a) = a * a\n/= f(5)";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].result, "25");
}
#[test]
fn multiple_evals() {
let doc = "let a = 3\n/= a\nlet b = 7\n/= a + b";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 2);
assert_eq!(result.results[0].result, "3");
assert_eq!(result.results[1].result, "10");
}
#[test]
fn format_integer() {
assert_eq!(format_value(42.0), "42");
}
#[test]
fn format_float() {
let s = format_value(3.14);
assert!(s.starts_with("3.14"));
}
}

143
core/src/ffi.rs Normal file
View File

@ -0,0 +1,143 @@
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::path::Path;
use crate::document::SwiftlyDoc;
use crate::eval;
use crate::persist;
fn cstr_to_str<'a>(ptr: *const c_char) -> Option<&'a str> {
if ptr.is_null() { return None; }
unsafe { CStr::from_ptr(ptr).to_str().ok() }
}
fn str_to_cstr(s: &str) -> *mut c_char {
CString::new(s).unwrap_or_default().into_raw()
}
#[no_mangle]
pub extern "C" fn swiftly_doc_new() -> *mut SwiftlyDoc {
Box::into_raw(Box::new(SwiftlyDoc::new()))
}
#[no_mangle]
pub extern "C" fn swiftly_doc_free(doc: *mut SwiftlyDoc) {
if doc.is_null() { return; }
unsafe { drop(Box::from_raw(doc)); }
}
#[no_mangle]
pub extern "C" fn swiftly_doc_set_text(doc: *mut SwiftlyDoc, text: *const c_char) {
let doc = match unsafe { doc.as_mut() } {
Some(d) => d,
None => return,
};
let text = match cstr_to_str(text) {
Some(s) => s,
None => return,
};
doc.set_text(text);
}
#[no_mangle]
pub extern "C" fn swiftly_doc_get_text(doc: *const SwiftlyDoc) -> *mut c_char {
let doc = match unsafe { doc.as_ref() } {
Some(d) => d,
None => return std::ptr::null_mut(),
};
str_to_cstr(&doc.text)
}
#[no_mangle]
pub extern "C" fn swiftly_doc_evaluate(doc: *mut SwiftlyDoc) -> *mut c_char {
let doc = match unsafe { doc.as_mut() } {
Some(d) => d,
None => return str_to_cstr("[]"),
};
let result = doc.evaluate();
let json = serde_json::to_string(&result).unwrap_or_else(|_| "[]".into());
str_to_cstr(&json)
}
#[no_mangle]
pub extern "C" fn swiftly_eval_line(text: *const c_char) -> *mut c_char {
let text = match cstr_to_str(text) {
Some(s) => s,
None => return str_to_cstr(""),
};
match eval::evaluate_line(text) {
Ok(result) => str_to_cstr(&result),
Err(e) => str_to_cstr(&format!("error: {}", e)),
}
}
#[no_mangle]
pub extern "C" fn swiftly_doc_save(doc: *const SwiftlyDoc, path: *const c_char) -> bool {
let doc = match unsafe { doc.as_ref() } {
Some(d) => d,
None => return false,
};
let path = match cstr_to_str(path) {
Some(s) => s,
None => return false,
};
persist::save_to_file(&doc.text, Path::new(path)).is_ok()
}
#[no_mangle]
pub extern "C" fn swiftly_doc_load(path: *const c_char) -> *mut SwiftlyDoc {
let path = match cstr_to_str(path) {
Some(s) => s,
None => return std::ptr::null_mut(),
};
match persist::load_from_file(Path::new(path)) {
Ok(text) => {
let mut doc = SwiftlyDoc::new();
doc.set_text(&text);
Box::into_raw(Box::new(doc))
}
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn swiftly_cache_save(doc: *const SwiftlyDoc) -> *mut c_char {
let doc = match unsafe { doc.as_ref() } {
Some(d) => d,
None => return std::ptr::null_mut(),
};
let uuid = doc.uuid.clone();
match persist::cache_save(&uuid, &doc.text) {
Ok(_) => str_to_cstr(&uuid),
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn swiftly_cache_load(uuid: *const c_char) -> *mut SwiftlyDoc {
let uuid = match cstr_to_str(uuid) {
Some(s) => s,
None => return std::ptr::null_mut(),
};
match persist::cache_load(uuid) {
Ok(text) => {
let mut doc = SwiftlyDoc::with_uuid(uuid.to_string());
doc.set_text(&text);
Box::into_raw(Box::new(doc))
}
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn swiftly_list_notes() -> *mut c_char {
let notes = persist::list_notes();
let json = serde_json::to_string(&notes).unwrap_or_else(|_| "[]".into());
str_to_cstr(&json)
}
#[no_mangle]
pub extern "C" fn swiftly_free_string(s: *mut c_char) {
if s.is_null() { return; }
unsafe { drop(CString::from_raw(s)); }
}

5
core/src/lib.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod doc;
pub mod document;
pub mod eval;
pub mod persist;
pub mod ffi;

168
core/src/persist.rs Normal file
View File

@ -0,0 +1,168 @@
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoteMeta {
pub uuid: String,
pub title: String,
pub path: String,
pub modified: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateIndex {
pub notes: HashMap<String, NoteMeta>,
}
impl StateIndex {
pub fn new() -> Self {
StateIndex {
notes: HashMap::new(),
}
}
pub fn load() -> io::Result<Self> {
let path = state_path();
if !path.exists() {
return Ok(Self::new());
}
let data = fs::read_to_string(&path)?;
serde_json::from_str(&data).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
pub fn save(&self) -> io::Result<()> {
let path = state_path();
ensure_dir(path.parent().unwrap())?;
let data = serde_json::to_string_pretty(self)?;
fs::write(&path, data)
}
pub fn upsert(&mut self, meta: NoteMeta) {
self.notes.insert(meta.uuid.clone(), meta);
}
pub fn remove(&mut self, uuid: &str) {
self.notes.remove(uuid);
}
pub fn list(&self) -> Vec<&NoteMeta> {
let mut notes: Vec<&NoteMeta> = self.notes.values().collect();
notes.sort_by(|a, b| b.modified.cmp(&a.modified));
notes
}
}
fn swiftly_dir() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
PathBuf::from(home).join(".swiftly")
}
fn cache_dir() -> PathBuf {
swiftly_dir().join("cache")
}
fn state_path() -> PathBuf {
swiftly_dir().join("state.json")
}
fn ensure_dir(dir: &Path) -> io::Result<()> {
if !dir.exists() {
fs::create_dir_all(dir)?;
}
Ok(())
}
fn now_epoch() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn title_from_text(text: &str) -> String {
for line in text.lines() {
let trimmed = line.trim();
if trimmed.is_empty() { continue; }
let title = trimmed.trim_start_matches('#').trim();
if !title.is_empty() {
return title.chars().take(80).collect();
}
}
"Untitled".into()
}
pub fn save_to_file(text: &str, path: &Path) -> io::Result<()> {
if let Some(parent) = path.parent() {
ensure_dir(parent)?;
}
fs::write(path, text)
}
pub fn load_from_file(path: &Path) -> io::Result<String> {
fs::read_to_string(path)
}
pub fn cache_save(uuid: &str, text: &str) -> io::Result<PathBuf> {
let dir = cache_dir();
ensure_dir(&dir)?;
let filename = format!("{}.sw", uuid);
let path = dir.join(&filename);
fs::write(&path, text)?;
let mut index = StateIndex::load().unwrap_or_else(|_| StateIndex::new());
index.upsert(NoteMeta {
uuid: uuid.to_string(),
title: title_from_text(text),
path: path.to_string_lossy().into_owned(),
modified: now_epoch(),
});
index.save()?;
Ok(path)
}
pub fn cache_load(uuid: &str) -> io::Result<String> {
let filename = format!("{}.sw", uuid);
let path = cache_dir().join(filename);
fs::read_to_string(path)
}
pub fn list_notes() -> Vec<NoteMeta> {
StateIndex::load()
.unwrap_or_else(|_| StateIndex::new())
.list()
.into_iter()
.cloned()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn title_extraction() {
assert_eq!(title_from_text("# My Note\nSome content"), "My Note");
assert_eq!(title_from_text("Hello world"), "Hello world");
assert_eq!(title_from_text(""), "Untitled");
assert_eq!(title_from_text("\n\n## Section\nstuff"), "Section");
}
#[test]
fn state_index_round_trip() {
let mut idx = StateIndex::new();
idx.upsert(NoteMeta {
uuid: "abc".into(),
title: "Test".into(),
path: "/tmp/test.sw".into(),
modified: 1000,
});
assert_eq!(idx.list().len(), 1);
idx.remove("abc");
assert_eq!(idx.list().len(), 0);
}
}

51
debug.sh Executable file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
BUILD="$ROOT/build"
APP="$BUILD/bin/Swiftly.app"
CONTENTS="$APP/Contents"
MACOS="$CONTENTS/MacOS"
RESOURCES="$CONTENTS/Resources"
LOGFILE="$HOME/swiftly/debug.log"
SDK=$(xcrun --show-sdk-path)
# --- Rust core (re-enable when backend is ready) ---
# RUST_LIB="$ROOT/core/target/debug"
# if [ -d "$ROOT/core" ]; then
# echo "Building Rust core (debug)..."
# cd "$ROOT/core" && cargo build
# cd "$ROOT"
# RUST_FLAGS="-import-objc-header $ROOT/core/include/swiftly.h -L $RUST_LIB -lswiftly_core"
# fi
RUST_FLAGS=""
# --- Bundle structure ---
mkdir -p "$MACOS" "$RESOURCES"
cp "$ROOT/Info.plist" "$CONTENTS/Info.plist"
# --- Compile Swift (debug) ---
echo "Compiling Swift (debug)..."
swiftc \
-target arm64-apple-macosx14.0 \
-sdk "$SDK" \
$RUST_FLAGS \
-framework Cocoa \
-framework SwiftUI \
-Onone -g \
-o "$MACOS/Swiftly" \
"$ROOT"/src/*.swift
# --- Code sign ---
codesign --force --deep --sign - "$APP"
# --- Kill existing, launch, tail log ---
pkill -f "Swiftly.app" 2>/dev/null || true
sleep 0.5
mkdir -p "$(dirname "$LOGFILE")"
open "$APP"
echo "Swiftly launched."
echo "Tail log: tail -f $LOGFILE"

163
src/AppDelegate.swift Normal file
View File

@ -0,0 +1,163 @@
import Cocoa
import SwiftUI
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var appState: AppState!
func applicationDidFinishLaunching(_ notification: Notification) {
appState = AppState()
let contentView = ContentView(state: appState)
let hostingView = NSHostingView(rootView: contentView)
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 1200, height: 800),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false
)
window.title = "Swiftly"
window.contentView = hostingView
window.center()
window.setFrameAutosaveName("SwiftlyMainWindow")
window.makeKeyAndOrderFront(nil)
setupMenuBar()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
func applicationWillTerminate(_ notification: Notification) {
appState.saveNote()
}
// MARK: - Menu bar
private func setupMenuBar() {
let mainMenu = NSMenu()
mainMenu.addItem(buildAppMenu())
mainMenu.addItem(buildFileMenu())
mainMenu.addItem(buildEditMenu())
mainMenu.addItem(buildViewMenu())
mainMenu.addItem(buildWindowMenu())
NSApp.mainMenu = mainMenu
}
private func buildAppMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu()
menu.addItem(withTitle: "About Swiftly", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
menu.addItem(.separator())
menu.addItem(withTitle: "Quit Swiftly", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
item.submenu = menu
return item
}
private func buildFileMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu(title: "File")
let newItem = NSMenuItem(title: "New Note", action: #selector(newNote), keyEquivalent: "n")
newItem.target = self
menu.addItem(newItem)
let openItem = NSMenuItem(title: "Open...", action: #selector(openNote), keyEquivalent: "o")
openItem.target = self
menu.addItem(openItem)
menu.addItem(.separator())
let saveItem = NSMenuItem(title: "Save", action: #selector(saveNote), keyEquivalent: "s")
saveItem.target = self
menu.addItem(saveItem)
let saveAsItem = NSMenuItem(title: "Save As...", action: #selector(saveNoteAs), keyEquivalent: "S")
saveAsItem.target = self
menu.addItem(saveAsItem)
item.submenu = menu
return item
}
private func buildEditMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu(title: "Edit")
menu.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z")
menu.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "Z")
menu.addItem(.separator())
menu.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")
menu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
menu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
menu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
menu.addItem(.separator())
let findItem = NSMenuItem(title: "Find...", action: #selector(NSTextView.performFindPanelAction(_:)), keyEquivalent: "f")
findItem.tag = Int(NSTextFinder.Action.showFindInterface.rawValue)
menu.addItem(findItem)
item.submenu = menu
return item
}
private func buildViewMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu(title: "View")
let toggleItem = NSMenuItem(title: "Toggle Sidebar", action: #selector(toggleSidebar), keyEquivalent: "\\")
toggleItem.keyEquivalentModifierMask = .command
toggleItem.target = self
menu.addItem(toggleItem)
item.submenu = menu
return item
}
private func buildWindowMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu(title: "Window")
menu.addItem(withTitle: "Minimize", action: #selector(NSWindow.miniaturize(_:)), keyEquivalent: "m")
menu.addItem(withTitle: "Zoom", action: #selector(NSWindow.zoom(_:)), keyEquivalent: "")
item.submenu = menu
NSApp.windowsMenu = menu
return item
}
// MARK: - Actions
@objc private func newNote() {
appState.newNote()
}
@objc private func openNote() {
let panel = NSOpenPanel()
panel.allowedContentTypes = [.plainText]
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.allowsMultipleSelection = false
panel.beginSheetModal(for: window) { [weak self] response in
guard response == .OK, let url = panel.url else { return }
self?.appState.loadNoteFromFile(url)
}
}
@objc private func saveNote() {
appState.saveNote()
}
@objc private func saveNoteAs() {
let panel = NSSavePanel()
panel.allowedContentTypes = [.plainText]
panel.nameFieldStringValue = "note.txt"
panel.beginSheetModal(for: window) { [weak self] response in
guard response == .OK, let url = panel.url else { return }
self?.appState.saveNoteToFile(url)
}
}
@objc private func toggleSidebar() {
NotificationCenter.default.post(name: .toggleSidebar, object: nil)
}
}

90
src/AppState.swift Normal file
View File

@ -0,0 +1,90 @@
import Foundation
import Combine
class AppState: ObservableObject {
@Published var documentText: String = "" {
didSet {
if documentText != oldValue {
modified = true
bridge.setText(currentNoteID, text: documentText)
}
}
}
@Published var evalResults: [Int: String] = [:]
@Published var noteList: [NoteInfo] = []
@Published var currentNoteID: UUID
@Published var modified: Bool = false
private let bridge = RustBridge.shared
init() {
let id = bridge.newDocument()
self.currentNoteID = id
refreshNoteList()
}
func newNote() {
saveCurrentIfNeeded()
let id = bridge.newDocument()
currentNoteID = id
documentText = ""
evalResults = [:]
modified = false
refreshNoteList()
}
func loadNote(_ id: UUID) {
saveCurrentIfNeeded()
if bridge.cacheLoad(id) {
currentNoteID = id
documentText = bridge.getText(id)
modified = false
evaluate()
}
}
func saveNote() {
bridge.setText(currentNoteID, text: documentText)
let _ = bridge.cacheSave(currentNoteID)
modified = false
refreshNoteList()
}
func saveNoteToFile(_ url: URL) {
let _ = bridge.saveNote(currentNoteID, path: url.path)
modified = false
}
func loadNoteFromFile(_ url: URL) {
if let (id, text) = bridge.loadNote(path: url.path) {
currentNoteID = id
documentText = text
modified = false
let _ = bridge.cacheSave(id)
evaluate()
refreshNoteList()
}
}
func deleteNote(_ id: UUID) {
bridge.deleteNote(id)
if id == currentNoteID {
newNote()
}
refreshNoteList()
}
func evaluate() {
evalResults = bridge.evaluate(currentNoteID)
}
func refreshNoteList() {
noteList = bridge.listNotes()
}
private func saveCurrentIfNeeded() {
if modified {
saveNote()
}
}
}

26
src/ContentView.swift Normal file
View File

@ -0,0 +1,26 @@
import SwiftUI
struct ContentView: View {
@ObservedObject var state: AppState
@State private var sidebarVisible: Bool = true
var body: some View {
HSplitView {
if sidebarVisible {
SidebarView(state: state)
.frame(minWidth: 180, idealWidth: 250, maxWidth: 350)
}
EditorView(state: state)
.frame(minWidth: 400)
}
.frame(minWidth: 700, minHeight: 400)
.background(Color(ns: Theme.current.base))
.onReceive(NotificationCenter.default.publisher(for: .toggleSidebar)) { _ in
withAnimation { sidebarVisible.toggle() }
}
}
}
extension Notification.Name {
static let toggleSidebar = Notification.Name("toggleSidebar")
}

225
src/EditorView.swift Normal file
View File

@ -0,0 +1,225 @@
import SwiftUI
import AppKit
struct EditorView: View {
@ObservedObject var state: AppState
var body: some View {
EditorTextView(text: $state.documentText, evalResults: state.evalResults, onEvaluate: {
state.evaluate()
})
.background(Color(ns: Theme.current.base))
}
}
struct EditorTextView: NSViewRepresentable {
@Binding var text: String
var evalResults: [Int: String]
var onEvaluate: () -> Void
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeNSView(context: Context) -> NSScrollView {
let scrollView = NSScrollView()
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = false
scrollView.autohidesScrollers = true
scrollView.borderType = .noBorder
let textView = LineNumberTextView()
textView.isEditable = true
textView.isSelectable = true
textView.allowsUndo = true
textView.isRichText = false
textView.usesFindBar = true
textView.isIncrementalSearchingEnabled = true
textView.font = Theme.editorFont
textView.textColor = Theme.current.text
textView.backgroundColor = Theme.current.base
textView.insertionPointColor = Theme.current.text
textView.selectedTextAttributes = [
.backgroundColor: Theme.current.surface1
]
textView.isAutomaticQuoteSubstitutionEnabled = false
textView.isAutomaticDashSubstitutionEnabled = false
textView.isAutomaticTextReplacementEnabled = false
textView.isAutomaticSpellingCorrectionEnabled = false
textView.smartInsertDeleteEnabled = false
textView.autoresizingMask = [.width]
textView.isVerticallyResizable = true
textView.isHorizontallyResizable = false
textView.textContainer?.containerSize = NSSize(
width: scrollView.contentSize.width,
height: CGFloat.greatestFiniteMagnitude
)
textView.textContainer?.widthTracksTextView = true
textView.maxSize = NSSize(
width: CGFloat.greatestFiniteMagnitude,
height: CGFloat.greatestFiniteMagnitude
)
scrollView.documentView = textView
let ruler = LineNumberRulerView(textView: textView)
ruler.evalResults = evalResults
scrollView.verticalRulerView = ruler
scrollView.hasVerticalRuler = true
scrollView.rulersVisible = true
textView.string = text
textView.delegate = context.coordinator
context.coordinator.textView = textView
context.coordinator.rulerView = ruler
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
guard let textView = scrollView.documentView as? LineNumberTextView else { return }
if textView.string != text {
let selectedRanges = textView.selectedRanges
textView.string = text
textView.selectedRanges = selectedRanges
}
textView.font = Theme.editorFont
textView.textColor = Theme.current.text
textView.backgroundColor = Theme.current.base
textView.insertionPointColor = Theme.current.text
if let ruler = scrollView.verticalRulerView as? LineNumberRulerView {
ruler.evalResults = evalResults
ruler.needsDisplay = true
}
}
class Coordinator: NSObject, NSTextViewDelegate {
var parent: EditorTextView
weak var textView: NSTextView?
weak var rulerView: LineNumberRulerView?
init(_ parent: EditorTextView) {
self.parent = parent
}
func textDidChange(_ notification: Notification) {
guard let tv = textView else { return }
parent.text = tv.string
rulerView?.needsDisplay = true
}
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
textView.insertNewlineIgnoringFieldEditor(nil)
DispatchQueue.main.async { [weak self] in
self?.parent.onEvaluate()
}
return true
}
return false
}
}
}
class LineNumberTextView: NSTextView {
override func drawInsertionPoint(in rect: NSRect, color: NSColor, turnedOn flag: Bool) {
var widened = rect
widened.size.width = 2
super.drawInsertionPoint(in: widened, color: color, turnedOn: flag)
}
}
class LineNumberRulerView: NSRulerView {
var evalResults: [Int: String] = [:]
private weak var editorTextView: NSTextView?
init(textView: NSTextView) {
self.editorTextView = textView
super.init(scrollView: textView.enclosingScrollView!, orientation: .verticalRuler)
self.clientView = textView
self.ruleThickness = 50
NotificationCenter.default.addObserver(
self,
selector: #selector(textDidChange(_:)),
name: NSText.didChangeNotification,
object: textView
)
}
required init(coder: NSCoder) {
fatalError()
}
@objc private func textDidChange(_ notification: Notification) {
needsDisplay = true
}
override func drawHashMarksAndLabels(in rect: NSRect) {
guard let tv = editorTextView,
let layoutManager = tv.layoutManager,
let textContainer = tv.textContainer else { return }
let palette = Theme.current
palette.mantle.setFill()
rect.fill()
let visibleRect = scrollView!.contentView.bounds
let visibleGlyphRange = layoutManager.glyphRange(
forBoundingRect: visibleRect, in: textContainer
)
let visibleCharRange = layoutManager.characterRange(
forGlyphRange: visibleGlyphRange, actualGlyphRange: nil
)
let text = tv.string as NSString
var lineNumber = 1
var index = 0
while index < visibleCharRange.location {
if text.character(at: index) == 0x0A { lineNumber += 1 }
index += 1
}
let attrs: [NSAttributedString.Key: Any] = [
.font: Theme.gutterFont,
.foregroundColor: palette.overlay0
]
let resultAttrs: [NSAttributedString.Key: Any] = [
.font: Theme.gutterFont,
.foregroundColor: palette.teal
]
var charIndex = visibleCharRange.location
while charIndex < NSMaxRange(visibleCharRange) {
let lineRange = text.lineRange(for: NSRange(location: charIndex, length: 0))
let glyphRange = layoutManager.glyphRange(forCharacterRange: lineRange, actualCharacterRange: nil)
var lineRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
lineRect.origin.y += tv.textContainerInset.height - visibleRect.origin.y
if let result = evalResults[lineNumber - 1] {
let resultStr = NSAttributedString(string: result, attributes: resultAttrs)
let resultSize = resultStr.size()
let resultPoint = NSPoint(
x: ruleThickness - resultSize.width - 4,
y: lineRect.origin.y
)
resultStr.draw(at: resultPoint)
} else {
let numStr = NSAttributedString(string: "\(lineNumber)", attributes: attrs)
let size = numStr.size()
let point = NSPoint(
x: ruleThickness - size.width - 8,
y: lineRect.origin.y
)
numStr.draw(at: point)
}
lineNumber += 1
charIndex = NSMaxRange(lineRange)
}
}
}

208
src/RustBridge.swift Normal file
View File

@ -0,0 +1,208 @@
import Foundation
struct NoteInfo: Identifiable {
let id: UUID
var title: String
var lastModified: Date
}
class RustBridge {
static let shared = RustBridge()
private var notes: [UUID: String] = [:]
private var noteMeta: [UUID: NoteInfo] = [:]
private init() {}
// MARK: - Document operations (stubbed)
func newDocument() -> UUID {
let id = UUID()
notes[id] = ""
let info = NoteInfo(id: id, title: "Untitled", lastModified: Date())
noteMeta[id] = info
return id
}
func freeDocument(_ id: UUID) {
notes.removeValue(forKey: id)
noteMeta.removeValue(forKey: id)
}
func setText(_ id: UUID, text: String) {
notes[id] = text
if var info = noteMeta[id] {
info.title = titleFromText(text)
info.lastModified = Date()
noteMeta[id] = info
}
}
func getText(_ id: UUID) -> String {
return notes[id] ?? ""
}
func evaluate(_ id: UUID) -> [Int: String] {
// Stub: parse lines for /= prefix, return placeholder results
guard let text = notes[id] else { return [:] }
var results: [Int: String] = [:]
let lines = text.components(separatedBy: "\n")
for (i, line) in lines.enumerated() {
let trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed.hasPrefix("/=") {
let expr = String(trimmed.dropFirst(2)).trimmingCharacters(in: .whitespaces)
results[i] = stubbedEval(expr)
}
}
return results
}
func evaluateLine(_ line: String) -> String {
let trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed.hasPrefix("/=") {
let expr = String(trimmed.dropFirst(2)).trimmingCharacters(in: .whitespaces)
return stubbedEval(expr)
}
return ""
}
// MARK: - File I/O (stubbed with UserDefaults)
func saveNote(_ id: UUID, path: String) -> Bool {
guard let text = notes[id] else { return false }
do {
try text.write(toFile: path, atomically: true, encoding: .utf8)
return true
} catch {
return false
}
}
func loadNote(path: String) -> (UUID, String)? {
guard let text = try? String(contentsOfFile: path, encoding: .utf8) else {
return nil
}
let id = UUID()
notes[id] = text
let info = NoteInfo(id: id, title: titleFromText(text), lastModified: Date())
noteMeta[id] = info
return (id, text)
}
// MARK: - Cache (stubbed with local storage)
func cacheDir() -> URL {
let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let dir = appSupport.appendingPathComponent("Swiftly", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir
}
func cacheSave(_ id: UUID) -> Bool {
guard let text = notes[id], let info = noteMeta[id] else { return false }
let dir = cacheDir().appendingPathComponent(id.uuidString, isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let textFile = dir.appendingPathComponent("content.txt")
let metaFile = dir.appendingPathComponent("meta.json")
do {
try text.write(to: textFile, atomically: true, encoding: .utf8)
let meta: [String: String] = [
"title": info.title,
"lastModified": ISO8601DateFormatter().string(from: info.lastModified)
]
let data = try JSONSerialization.data(withJSONObject: meta)
try data.write(to: metaFile, options: .atomic)
return true
} catch {
return false
}
}
func cacheLoad(_ id: UUID) -> Bool {
let dir = cacheDir().appendingPathComponent(id.uuidString, isDirectory: true)
let textFile = dir.appendingPathComponent("content.txt")
let metaFile = dir.appendingPathComponent("meta.json")
guard let text = try? String(contentsOf: textFile, encoding: .utf8),
let metaData = try? Data(contentsOf: metaFile),
let meta = try? JSONSerialization.jsonObject(with: metaData) as? [String: String] else {
return false
}
notes[id] = text
let formatter = ISO8601DateFormatter()
let date = formatter.date(from: meta["lastModified"] ?? "") ?? Date()
noteMeta[id] = NoteInfo(id: id, title: meta["title"] ?? "Untitled", lastModified: date)
return true
}
func listNotes() -> [NoteInfo] {
let dir = cacheDir()
guard let entries = try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil) else {
return []
}
var result: [NoteInfo] = []
let formatter = ISO8601DateFormatter()
for entry in entries {
guard entry.hasDirectoryPath,
let uuid = UUID(uuidString: entry.lastPathComponent) else { continue }
let metaFile = entry.appendingPathComponent("meta.json")
guard let data = try? Data(contentsOf: metaFile),
let meta = try? JSONSerialization.jsonObject(with: data) as? [String: String] else { continue }
let date = formatter.date(from: meta["lastModified"] ?? "") ?? Date()
let info = NoteInfo(id: uuid, title: meta["title"] ?? "Untitled", lastModified: date)
result.append(info)
if noteMeta[uuid] == nil {
noteMeta[uuid] = info
}
}
return result.sorted { $0.lastModified > $1.lastModified }
}
func deleteNote(_ id: UUID) {
notes.removeValue(forKey: id)
noteMeta.removeValue(forKey: id)
let dir = cacheDir().appendingPathComponent(id.uuidString, isDirectory: true)
try? FileManager.default.removeItem(at: dir)
}
// MARK: - Internal
private func titleFromText(_ text: String) -> String {
let firstLine = text.components(separatedBy: "\n").first ?? ""
let trimmed = firstLine.trimmingCharacters(in: .whitespaces)
if trimmed.isEmpty { return "Untitled" }
let clean = trimmed.replacingOccurrences(of: "^#+\\s*", with: "", options: .regularExpression)
let maxLen = 60
if clean.count > maxLen {
return String(clean.prefix(maxLen)) + "..."
}
return clean
}
private func stubbedEval(_ expr: String) -> String {
// Stub: attempt basic arithmetic, otherwise return placeholder
let components = expr.components(separatedBy: .whitespaces).filter { !$0.isEmpty }
if components.count == 3,
let a = Double(components[0]),
let b = Double(components[2]) {
let op = components[1]
switch op {
case "+": return formatNumber(a + b)
case "-": return formatNumber(a - b)
case "*": return formatNumber(a * b)
case "/": return b != 0 ? formatNumber(a / b) : "div/0"
default: break
}
}
if components.count == 1, let n = Double(components[0]) {
return formatNumber(n)
}
return "..."
}
private func formatNumber(_ n: Double) -> String {
if n == n.rounded() && abs(n) < 1e15 {
return String(format: "%.0f", n)
}
return String(format: "%.6g", n)
}
}

83
src/SidebarView.swift Normal file
View File

@ -0,0 +1,83 @@
import SwiftUI
struct SidebarView: View {
@ObservedObject var state: AppState
private let dateFormatter: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .short
f.timeStyle = .short
return f
}()
var body: some View {
VStack(spacing: 0) {
HStack {
Text("Notes")
.font(.headline)
.foregroundColor(Color(ns: Theme.current.text))
Spacer()
Button(action: { state.newNote() }) {
Image(systemName: "plus")
.foregroundColor(Color(ns: Theme.current.text))
}
.buttonStyle(.plain)
.help("New Note")
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
Divider()
if state.noteList.isEmpty {
VStack {
Spacer()
Text("No notes yet")
.foregroundColor(Color(ns: Theme.current.overlay1))
.font(.system(size: 13))
Spacer()
}
} else {
List(state.noteList) { note in
NoteRow(
note: note,
isSelected: note.id == state.currentNoteID,
dateFormatter: dateFormatter
)
.contentShape(Rectangle())
.onTapGesture { state.loadNote(note.id) }
.contextMenu {
Button("Delete") { state.deleteNote(note.id) }
}
.listRowBackground(
note.id == state.currentNoteID
? Color(ns: Theme.current.surface1)
: Color.clear
)
}
.listStyle(.plain)
}
}
.background(Color(ns: Theme.current.mantle))
.onAppear { state.refreshNoteList() }
}
}
struct NoteRow: View {
let note: NoteInfo
let isSelected: Bool
let dateFormatter: DateFormatter
var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(note.title)
.font(.system(size: 13, weight: isSelected ? .semibold : .regular))
.foregroundColor(Color(ns: Theme.current.text))
.lineLimit(1)
Text(dateFormatter.string(from: note.lastModified))
.font(.system(size: 11))
.foregroundColor(Color(ns: Theme.current.subtext0))
}
.padding(.vertical, 2)
}
}

131
src/Theme.swift Normal file
View File

@ -0,0 +1,131 @@
import Cocoa
import SwiftUI
struct CatppuccinPalette {
let base: NSColor
let mantle: NSColor
let crust: NSColor
let surface0: NSColor
let surface1: NSColor
let surface2: NSColor
let overlay0: NSColor
let overlay1: NSColor
let overlay2: NSColor
let text: NSColor
let subtext0: NSColor
let subtext1: NSColor
let red: NSColor
let maroon: NSColor
let peach: NSColor
let yellow: NSColor
let green: NSColor
let teal: NSColor
let sky: NSColor
let sapphire: NSColor
let blue: NSColor
let lavender: NSColor
let mauve: NSColor
let pink: NSColor
let flamingo: NSColor
let rosewater: NSColor
}
struct Theme {
static let mocha = CatppuccinPalette(
base: NSColor(red: 0.118, green: 0.118, blue: 0.180, alpha: 1),
mantle: NSColor(red: 0.094, green: 0.094, blue: 0.149, alpha: 1),
crust: NSColor(red: 0.071, green: 0.071, blue: 0.118, alpha: 1),
surface0: NSColor(red: 0.188, green: 0.188, blue: 0.259, alpha: 1),
surface1: NSColor(red: 0.271, green: 0.271, blue: 0.353, alpha: 1),
surface2: NSColor(red: 0.353, green: 0.353, blue: 0.439, alpha: 1),
overlay0: NSColor(red: 0.427, green: 0.427, blue: 0.522, alpha: 1),
overlay1: NSColor(red: 0.506, green: 0.506, blue: 0.600, alpha: 1),
overlay2: NSColor(red: 0.584, green: 0.584, blue: 0.682, alpha: 1),
text: NSColor(red: 0.804, green: 0.839, blue: 0.957, alpha: 1),
subtext0: NSColor(red: 0.651, green: 0.686, blue: 0.820, alpha: 1),
subtext1: NSColor(red: 0.725, green: 0.761, blue: 0.886, alpha: 1),
red: NSColor(red: 0.953, green: 0.545, blue: 0.659, alpha: 1),
maroon: NSColor(red: 0.922, green: 0.600, blue: 0.659, alpha: 1),
peach: NSColor(red: 0.980, green: 0.702, blue: 0.529, alpha: 1),
yellow: NSColor(red: 0.976, green: 0.886, blue: 0.686, alpha: 1),
green: NSColor(red: 0.651, green: 0.890, blue: 0.631, alpha: 1),
teal: NSColor(red: 0.596, green: 0.878, blue: 0.816, alpha: 1),
sky: NSColor(red: 0.537, green: 0.863, blue: 0.922, alpha: 1),
sapphire: NSColor(red: 0.455, green: 0.784, blue: 0.890, alpha: 1),
blue: NSColor(red: 0.537, green: 0.706, blue: 0.980, alpha: 1),
lavender: NSColor(red: 0.710, green: 0.745, blue: 0.996, alpha: 1),
mauve: NSColor(red: 0.796, green: 0.651, blue: 0.969, alpha: 1),
pink: NSColor(red: 0.961, green: 0.710, blue: 0.898, alpha: 1),
flamingo: NSColor(red: 0.949, green: 0.710, blue: 0.765, alpha: 1),
rosewater: NSColor(red: 0.961, green: 0.761, blue: 0.765, alpha: 1)
)
static let latte = CatppuccinPalette(
base: NSColor(red: 0.937, green: 0.929, blue: 0.961, alpha: 1),
mantle: NSColor(red: 0.906, green: 0.898, blue: 0.941, alpha: 1),
crust: NSColor(red: 0.863, green: 0.855, blue: 0.910, alpha: 1),
surface0: NSColor(red: 0.800, green: 0.796, blue: 0.863, alpha: 1),
surface1: NSColor(red: 0.737, green: 0.733, blue: 0.816, alpha: 1),
surface2: NSColor(red: 0.667, green: 0.663, blue: 0.757, alpha: 1),
overlay0: NSColor(red: 0.604, green: 0.596, blue: 0.706, alpha: 1),
overlay1: NSColor(red: 0.533, green: 0.529, blue: 0.647, alpha: 1),
overlay2: NSColor(red: 0.467, green: 0.463, blue: 0.592, alpha: 1),
text: NSColor(red: 0.298, green: 0.286, blue: 0.416, alpha: 1),
subtext0: NSColor(red: 0.376, green: 0.365, blue: 0.494, alpha: 1),
subtext1: NSColor(red: 0.337, green: 0.325, blue: 0.455, alpha: 1),
red: NSColor(red: 0.822, green: 0.294, blue: 0.345, alpha: 1),
maroon: NSColor(red: 0.906, green: 0.345, blue: 0.388, alpha: 1),
peach: NSColor(red: 0.996, green: 0.541, blue: 0.243, alpha: 1),
yellow: NSColor(red: 0.875, green: 0.627, blue: 0.086, alpha: 1),
green: NSColor(red: 0.251, green: 0.624, blue: 0.247, alpha: 1),
teal: NSColor(red: 0.090, green: 0.604, blue: 0.502, alpha: 1),
sky: NSColor(red: 0.016, green: 0.639, blue: 0.757, alpha: 1),
sapphire: NSColor(red: 0.125, green: 0.561, blue: 0.737, alpha: 1),
blue: NSColor(red: 0.118, green: 0.404, blue: 0.878, alpha: 1),
lavender: NSColor(red: 0.451, green: 0.420, blue: 0.878, alpha: 1),
mauve: NSColor(red: 0.529, green: 0.329, blue: 0.890, alpha: 1),
pink: NSColor(red: 0.918, green: 0.341, blue: 0.604, alpha: 1),
flamingo: NSColor(red: 0.867, green: 0.369, blue: 0.424, alpha: 1),
rosewater: NSColor(red: 0.863, green: 0.443, blue: 0.439, alpha: 1)
)
static var current: CatppuccinPalette {
let appearance = NSApp.effectiveAppearance
let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
return isDark ? mocha : latte
}
static let editorFont = NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)
static let gutterFont = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular)
static let sidebarFont = NSFont.systemFont(ofSize: 13, weight: .regular)
static let sidebarDateFont = NSFont.systemFont(ofSize: 11, weight: .regular)
struct SyntaxColors {
let keyword: NSColor
let number: NSColor
let string: NSColor
let comment: NSColor
let `operator`: NSColor
let function: NSColor
let result: NSColor
}
static var syntax: SyntaxColors {
let p = current
return SyntaxColors(
keyword: p.mauve,
number: p.peach,
string: p.green,
comment: p.overlay1,
operator: p.sky,
function: p.blue,
result: p.teal
)
}
}
extension Color {
init(ns: NSColor) {
self.init(nsColor: ns)
}
}

8
src/main.swift Normal file
View File

@ -0,0 +1,8 @@
import Cocoa
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps: true)
app.run()