Build scripts and flow resituated for better organization & Implementing cross compilation utilities.
This commit is contained in:
parent
f98b36833c
commit
bae246f08d
|
|
@ -0,0 +1,2 @@
|
|||
[alias]
|
||||
xtask = "run --release --package xtask --"
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
.DS_Store
|
||||
target/
|
||||
build/
|
||||
dist/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
[workspace]
|
||||
members = ["core", "viewport", "windows"]
|
||||
members = ["core", "viewport", "windows", "linux", "xtask"]
|
||||
# Excludes `linux` (deps don't build on macOS/Windows) and `xtask` (build-tool,
|
||||
# not part of the app). The Linux script invokes `cargo build -p acord-linux`
|
||||
# directly when running on a Linux host.
|
||||
default-members = ["core", "viewport", "windows"]
|
||||
resolver = "2"
|
||||
|
||||
[profile.release]
|
||||
|
|
|
|||
60
README.md
60
README.md
|
|
@ -1,33 +1,53 @@
|
|||
# Acord
|
||||
|
||||
|
||||
## Quickstart MacOS
|
||||
## Quickstart
|
||||
|
||||
- Download a pre-compiled binary from [Releases](.././../releases)
|
||||
|
||||
- To build, install/update/replace the release binary which will be propogated into your Applications folder, execute this script:
|
||||
- Or build from source — one command, picks the right script for your platform:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
DEST="/Applications/Acord.app"
|
||||
|
||||
echo "Building release..."
|
||||
"$ROOT/build.sh"
|
||||
|
||||
# Kill running instance before replacing
|
||||
pkill -f "Acord.app/Contents/MacOS/Acord" 2>/dev/null || true
|
||||
sleep 0.5
|
||||
|
||||
echo "Installing to $DEST..."
|
||||
rm -rf "$DEST"
|
||||
cp -R "$ROOT/build/bin/Acord.app" "$DEST"
|
||||
|
||||
echo "Installed: $DEST"
|
||||
cargo xtask install
|
||||
```
|
||||
|
||||
On macOS this drops a release `.app` into `/Applications`. On Linux it installs the binary into `~/.local/bin`, drops a `.desktop` entry into `~/.local/share/applications`, and registers the icon. On Windows it builds the release exe (no install step yet).
|
||||
|
||||
Other commands:
|
||||
|
||||
```bash
|
||||
cargo xtask build # release build only
|
||||
cargo xtask debug # debug build, foreground launch
|
||||
cargo xtask build-universal # universal arm64+x86_64 binary (macOS / Windows)
|
||||
cargo xtask package --all # cross-compile + zip all six distributables
|
||||
```
|
||||
|
||||
Append `-macos`, `-windows`, or `-linux` to force a platform (e.g. `cargo xtask build-universal-windows`).
|
||||
|
||||
On Linux, both x11 and wayland backends are linked into the binary by default. Force one with `ACORD_FEATURES=wayland cargo xtask build` (handy for flatpak or stripped distros).
|
||||
|
||||
### Releasing
|
||||
|
||||
`cargo xtask package --all` produces six zips in `dist/` from a single macOS host:
|
||||
|
||||
```
|
||||
dist/acord-macos-aarch64.zip # Acord.app — drag to /Applications
|
||||
dist/acord-macos-x86_64.zip
|
||||
dist/acord-windows-aarch64.zip # folder with Acord.exe + assets
|
||||
dist/acord-windows-x86_64.zip
|
||||
dist/acord-linux-aarch64.zip # folder with Acord + install.sh
|
||||
dist/acord-linux-x86_64.zip
|
||||
```
|
||||
|
||||
Cross-compile uses [`cargo-zigbuild`](https://github.com/rust-cross/cargo-zigbuild) (zig as the cross-linker) for the windows/linux targets — no Docker, no VMs. One-time setup:
|
||||
|
||||
```bash
|
||||
brew install zig librsvg
|
||||
cargo install cargo-zigbuild
|
||||
```
|
||||
|
||||
Or build a single target: `cargo xtask package --target windows-aarch64`.
|
||||
|
||||
Here's the 'sales' pitch:
|
||||
|
||||
"Hi there, do you enjoy casually solving project euler problems and are tired of using the spotlight bar as your primary calculator?" - Then this might be your kinda thing.
|
||||
|
|
|
|||
Binary file not shown.
18
install.sh
18
install.sh
|
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
DEST="/Applications/Acord.app"
|
||||
|
||||
echo "Building release..."
|
||||
"$ROOT/build.sh"
|
||||
|
||||
# Kill running instance before replacing
|
||||
pkill -f "Acord.app/Contents/MacOS/Acord" 2>/dev/null || true
|
||||
sleep 0.5
|
||||
|
||||
echo "Installing to $DEST..."
|
||||
rm -rf "$DEST"
|
||||
cp -R "$ROOT/build/bin/Acord.app" "$DEST"
|
||||
|
||||
echo "Installed: $DEST"
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "acord-linux"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "acord"
|
||||
path = "src/main.rs"
|
||||
|
||||
# x11 and wayland are independent winit backends. Default builds enable both
|
||||
# so a single binary runs on either display server. The build script can pass
|
||||
# `--no-default-features --features x11` (or `wayland`) to force one — useful
|
||||
# for flatpak builds or stripped-down distros.
|
||||
[features]
|
||||
default = ["x11", "wayland"]
|
||||
x11 = ["winit/x11"]
|
||||
wayland = ["winit/wayland"]
|
||||
|
||||
[dependencies]
|
||||
acord-core = { path = "../core" }
|
||||
acord-viewport = { path = "../viewport" }
|
||||
# rwh_06 = raw-window-handle 0.6 trait impls (HasWindowHandle/HasDisplayHandle).
|
||||
# Required because we lift the surface handle out of the winit Window before
|
||||
# handing it to wgpu via viewport_create.
|
||||
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
|
||||
arboard = "3"
|
||||
rfd = "0.15"
|
||||
raw-window-handle = "0.6"
|
||||
wgpu = "27"
|
||||
iced_wgpu = "0.14"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
dirs = "6"
|
||||
image = "0.25"
|
||||
|
|
@ -0,0 +1,451 @@
|
|||
use std::ffi::CString;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::{LogicalSize, PhysicalPosition};
|
||||
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
use winit::keyboard::{Key, ModifiersState, NamedKey};
|
||||
use winit::window::{Window, WindowAttributes, WindowId};
|
||||
|
||||
use acord_viewport::{
|
||||
viewport_create, viewport_destroy, viewport_render, viewport_resize,
|
||||
viewport_set_text, viewport_get_text, viewport_set_theme, viewport_set_lang,
|
||||
viewport_set_line_indicator, viewport_set_gutter_rainbow,
|
||||
viewport_send_command, viewport_free_string,
|
||||
ViewportHandle,
|
||||
};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::shortcuts::{match_shortcut, MenuAction};
|
||||
|
||||
pub struct App {
|
||||
window: Option<Window>,
|
||||
handle: *mut ViewportHandle,
|
||||
config: Config,
|
||||
cursor_pos: PhysicalPosition<f64>,
|
||||
scale: f32,
|
||||
modifiers: ModifiersState,
|
||||
current_file: Option<PathBuf>,
|
||||
last_autosave_attempt: Instant,
|
||||
last_autosaved_hash: Option<u64>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
window: None,
|
||||
handle: std::ptr::null_mut(),
|
||||
config: Config::load(),
|
||||
cursor_pos: PhysicalPosition::new(0.0, 0.0),
|
||||
scale: 1.0,
|
||||
modifiers: ModifiersState::empty(),
|
||||
current_file: None,
|
||||
last_autosave_attempt: Instant::now(),
|
||||
last_autosaved_hash: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_settings(&self) {
|
||||
if self.handle.is_null() { return; }
|
||||
let theme = match self.config.theme_mode() {
|
||||
"dark" => "kicad",
|
||||
"light" => "latte",
|
||||
_ => "kicad",
|
||||
};
|
||||
let c_theme = CString::new(theme).unwrap();
|
||||
viewport_set_theme(self.handle, c_theme.as_ptr());
|
||||
|
||||
let ind = CString::new(self.config.line_indicator()).unwrap();
|
||||
viewport_set_line_indicator(self.handle, ind.as_ptr());
|
||||
viewport_set_gutter_rainbow(self.handle, self.config.gutter_rainbow());
|
||||
}
|
||||
|
||||
fn dispatch_menu(&mut self, action: MenuAction, event_loop: &ActiveEventLoop) {
|
||||
if self.handle.is_null() { return; }
|
||||
match action {
|
||||
MenuAction::Quit => event_loop.exit(),
|
||||
MenuAction::Bold => { viewport_send_command(self.handle, 1); }
|
||||
MenuAction::Italic => { viewport_send_command(self.handle, 2); }
|
||||
MenuAction::InsertTable => { viewport_send_command(self.handle, 3); }
|
||||
MenuAction::Evaluate => { viewport_send_command(self.handle, 5); }
|
||||
MenuAction::ZoomIn => { viewport_send_command(self.handle, 7); }
|
||||
MenuAction::ZoomOut => { viewport_send_command(self.handle, 8); }
|
||||
MenuAction::ZoomReset => { viewport_send_command(self.handle, 9); }
|
||||
MenuAction::LiveMode => { viewport_send_command(self.handle, 11); }
|
||||
MenuAction::EditorMode => { viewport_send_command(self.handle, 12); }
|
||||
MenuAction::ViewMode => { viewport_send_command(self.handle, 13); }
|
||||
MenuAction::Find => { viewport_send_command(self.handle, 14); }
|
||||
MenuAction::Open => self.open_file(),
|
||||
MenuAction::Save => self.save_file(),
|
||||
MenuAction::SaveAs => self.save_file_as(),
|
||||
MenuAction::NewNote => self.new_note(),
|
||||
MenuAction::Settings => {
|
||||
let cfg = config_path();
|
||||
// Prefer xdg-open; fall back to $EDITOR or nano.
|
||||
let opened = std::process::Command::new("xdg-open").arg(&cfg).spawn().is_ok();
|
||||
if !opened {
|
||||
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".into());
|
||||
let _ = std::process::Command::new(editor).arg(&cfg).spawn();
|
||||
}
|
||||
}
|
||||
MenuAction::ExportCrate => { /* TODO: wire crate export */ }
|
||||
}
|
||||
}
|
||||
|
||||
fn open_file(&mut self) {
|
||||
let dialog = rfd::FileDialog::new()
|
||||
.add_filter("Markdown", &["md", "markdown"])
|
||||
.add_filter("All Files", &["*"]);
|
||||
if let Some(path) = dialog.pick_file() {
|
||||
if let Ok(text) = std::fs::read_to_string(&path) {
|
||||
let c = CString::new(text).unwrap_or_default();
|
||||
viewport_set_text(self.handle, c.as_ptr());
|
||||
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("md");
|
||||
let c_ext = CString::new(ext).unwrap();
|
||||
viewport_set_lang(self.handle, c_ext.as_ptr());
|
||||
if let Some(w) = &self.window {
|
||||
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("Acord");
|
||||
w.set_title(&format!("{name} - Acord"));
|
||||
}
|
||||
self.current_file = Some(path);
|
||||
self.last_autosaved_hash = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_file(&mut self) {
|
||||
match self.current_file.clone() {
|
||||
Some(path) => self.write_to(&path),
|
||||
None => self.save_file_as(),
|
||||
}
|
||||
}
|
||||
|
||||
fn save_file_as(&mut self) {
|
||||
let dialog = rfd::FileDialog::new()
|
||||
.add_filter("Markdown", &["md"])
|
||||
.add_filter("All Files", &["*"])
|
||||
.set_file_name("note.md");
|
||||
if let Some(path) = dialog.save_file() {
|
||||
self.write_to(&path);
|
||||
self.current_file = Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to(&mut self, path: &std::path::Path) {
|
||||
let text_ptr = viewport_get_text(self.handle);
|
||||
if text_ptr.is_null() { return; }
|
||||
let text = unsafe { std::ffi::CStr::from_ptr(text_ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
viewport_free_string(text_ptr);
|
||||
if std::fs::write(path, &text).is_ok() {
|
||||
self.last_autosaved_hash = Some(text_hash(&text));
|
||||
}
|
||||
}
|
||||
|
||||
fn new_note(&mut self) {
|
||||
let empty = CString::new("").unwrap();
|
||||
viewport_set_text(self.handle, empty.as_ptr());
|
||||
if let Some(w) = &self.window {
|
||||
w.set_title("Acord");
|
||||
}
|
||||
self.current_file = None;
|
||||
self.last_autosaved_hash = None;
|
||||
}
|
||||
|
||||
fn try_autosave(&mut self) {
|
||||
if self.handle.is_null() { return; }
|
||||
let text_ptr = viewport_get_text(self.handle);
|
||||
if text_ptr.is_null() { return; }
|
||||
let text = unsafe { std::ffi::CStr::from_ptr(text_ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
viewport_free_string(text_ptr);
|
||||
|
||||
let hash = text_hash(&text);
|
||||
if Some(hash) == self.last_autosaved_hash { return; }
|
||||
|
||||
let path = self.current_file.clone().unwrap_or_else(|| {
|
||||
self.config.notes_dir().join("Untitled.md")
|
||||
});
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
if std::fs::write(&path, &text).is_ok() {
|
||||
self.last_autosaved_hash = Some(hash);
|
||||
}
|
||||
}
|
||||
|
||||
fn winit_button(button: MouseButton) -> u8 {
|
||||
match button {
|
||||
MouseButton::Left => 0,
|
||||
MouseButton::Right => 1,
|
||||
MouseButton::Middle => 2,
|
||||
MouseButton::Other(n) => n as u8,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.window.is_some() { return; }
|
||||
|
||||
let mut attrs = WindowAttributes::default()
|
||||
.with_title("Acord")
|
||||
.with_inner_size(LogicalSize::new(1100.0, 750.0));
|
||||
|
||||
if let Some(icon) = load_window_icon() {
|
||||
attrs = attrs.with_window_icon(Some(icon));
|
||||
}
|
||||
|
||||
let window = event_loop.create_window(attrs).expect("create window");
|
||||
self.scale = window.scale_factor() as f32;
|
||||
|
||||
let size = window.inner_size();
|
||||
let w = size.width as f32 / self.scale;
|
||||
let h = size.height as f32 / self.scale;
|
||||
|
||||
// Pass the raw display+window handle to the viewport. wgpu picks the
|
||||
// X11 or Wayland backend automatically based on which RawDisplayHandle
|
||||
// variant winit hands us, so a single binary works on both.
|
||||
use raw_window_handle::HasWindowHandle;
|
||||
let wh = window.window_handle().expect("window handle");
|
||||
let raw = wh.as_raw();
|
||||
|
||||
let surface_handle = match raw {
|
||||
#[cfg(target_os = "linux")]
|
||||
raw_window_handle::RawWindowHandle::Xlib(h) => h.window as *mut std::ffi::c_void,
|
||||
#[cfg(target_os = "linux")]
|
||||
raw_window_handle::RawWindowHandle::Xcb(h) => h.window.get() as *mut std::ffi::c_void,
|
||||
#[cfg(target_os = "linux")]
|
||||
raw_window_handle::RawWindowHandle::Wayland(h) => h.surface.as_ptr(),
|
||||
_ => std::ptr::null_mut(),
|
||||
};
|
||||
|
||||
self.handle = viewport_create(surface_handle, w, h, self.scale);
|
||||
self.sync_settings();
|
||||
self.window = Some(window);
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||
if self.handle.is_null() { return; }
|
||||
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
if !self.handle.is_null() {
|
||||
viewport_destroy(self.handle);
|
||||
self.handle = std::ptr::null_mut();
|
||||
}
|
||||
event_loop.exit();
|
||||
}
|
||||
|
||||
WindowEvent::Resized(size) => {
|
||||
let w = size.width as f32 / self.scale;
|
||||
let h = size.height as f32 / self.scale;
|
||||
viewport_resize(self.handle, w, h, self.scale);
|
||||
}
|
||||
|
||||
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||
self.scale = scale_factor as f32;
|
||||
if let Some(win) = &self.window {
|
||||
let size = win.inner_size();
|
||||
let w = size.width as f32 / self.scale;
|
||||
let h = size.height as f32 / self.scale;
|
||||
viewport_resize(self.handle, w, h, self.scale);
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::RedrawRequested => {
|
||||
viewport_render(self.handle);
|
||||
}
|
||||
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
self.cursor_pos = position;
|
||||
let x = position.x as f32 / self.scale;
|
||||
let y = position.y as f32 / self.scale;
|
||||
acord_viewport::viewport_mouse_event(self.handle, x, y, 255, false);
|
||||
}
|
||||
|
||||
WindowEvent::MouseInput { state, button, .. } => {
|
||||
let x = self.cursor_pos.x as f32 / self.scale;
|
||||
let y = self.cursor_pos.y as f32 / self.scale;
|
||||
let pressed = state == ElementState::Pressed;
|
||||
acord_viewport::viewport_mouse_event(
|
||||
self.handle, x, y, Self::winit_button(button), pressed,
|
||||
);
|
||||
}
|
||||
|
||||
WindowEvent::MouseWheel { delta, .. } => {
|
||||
let x = self.cursor_pos.x as f32 / self.scale;
|
||||
let y = self.cursor_pos.y as f32 / self.scale;
|
||||
let (dx, dy) = match delta {
|
||||
MouseScrollDelta::LineDelta(dx, dy) => (dx * 20.0, dy * 20.0),
|
||||
MouseScrollDelta::PixelDelta(d) => (d.x as f32, d.y as f32),
|
||||
};
|
||||
acord_viewport::viewport_scroll_event(self.handle, x, y, dx, -dy);
|
||||
}
|
||||
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
let pressed = event.state == ElementState::Pressed;
|
||||
|
||||
// App-level shortcut? Fire on press only and short-circuit so the
|
||||
// viewport's text_editor doesn't also see the keystroke (otherwise
|
||||
// Ctrl+S would type 's' as well as save).
|
||||
if pressed {
|
||||
if let Some(action) = match_shortcut(self.modifiers, &event.logical_key) {
|
||||
self.dispatch_menu(action, event_loop);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let text_str = event.text.as_ref().map(|s| s.to_string());
|
||||
let text_c = text_str.as_deref().and_then(|s| CString::new(s).ok());
|
||||
let text_ptr = text_c.as_ref()
|
||||
.map(|c| c.as_ptr())
|
||||
.unwrap_or(std::ptr::null());
|
||||
|
||||
let keycode = winit_key_to_code(&event.logical_key);
|
||||
let mod_flags = encode_modifiers(self.modifiers);
|
||||
|
||||
acord_viewport::viewport_key_event(
|
||||
self.handle, keycode, mod_flags, pressed, text_ptr,
|
||||
);
|
||||
}
|
||||
|
||||
WindowEvent::ModifiersChanged(mods) => {
|
||||
self.modifiers = mods.state();
|
||||
if !self.handle.is_null() {
|
||||
let state = mods.state();
|
||||
let h = unsafe { &mut *self.handle };
|
||||
use iced_wgpu::core::keyboard;
|
||||
use iced_wgpu::core::Event;
|
||||
h.events.push(Event::Keyboard(
|
||||
keyboard::Event::ModifiersChanged(decode_winit_modifiers(state)),
|
||||
));
|
||||
h.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
if self.last_autosave_attempt.elapsed() >= Duration::from_millis(500) {
|
||||
self.last_autosave_attempt = Instant::now();
|
||||
self.try_autosave();
|
||||
}
|
||||
if let Some(w) = &self.window {
|
||||
if !self.handle.is_null() {
|
||||
w.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn text_hash(s: &str) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
s.hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// Maps winit logical keys to the macOS-style virtual keycodes the bridge
|
||||
/// expects. Character keys go through `text` instead, so 0 is fine for those.
|
||||
fn winit_key_to_code(key: &Key) -> u32 {
|
||||
match key {
|
||||
Key::Named(n) => match n {
|
||||
NamedKey::Enter => 36,
|
||||
NamedKey::Tab => 48,
|
||||
NamedKey::Backspace => 51,
|
||||
NamedKey::Escape => 53,
|
||||
NamedKey::Delete => 117,
|
||||
NamedKey::ArrowLeft => 123,
|
||||
NamedKey::ArrowRight => 124,
|
||||
NamedKey::ArrowDown => 125,
|
||||
NamedKey::ArrowUp => 126,
|
||||
NamedKey::Home => 115,
|
||||
NamedKey::End => 119,
|
||||
NamedKey::PageUp => 116,
|
||||
NamedKey::PageDown => 121,
|
||||
NamedKey::F1 => 122,
|
||||
NamedKey::F2 => 120,
|
||||
NamedKey::F3 => 99,
|
||||
NamedKey::F4 => 118,
|
||||
NamedKey::F5 => 96,
|
||||
NamedKey::F6 => 97,
|
||||
NamedKey::F7 => 98,
|
||||
NamedKey::F8 => 100,
|
||||
NamedKey::F9 => 101,
|
||||
NamedKey::F10 => 109,
|
||||
NamedKey::F11 => 103,
|
||||
NamedKey::F12 => 111,
|
||||
_ => 0,
|
||||
},
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_modifiers(state: ModifiersState) -> u32 {
|
||||
let mut flags = 0u32;
|
||||
if state.shift_key() { flags |= 1 << 17; }
|
||||
if state.control_key() { flags |= 1 << 18; }
|
||||
if state.alt_key() { flags |= 1 << 19; }
|
||||
if state.super_key() { flags |= 1 << 20; }
|
||||
// Mirror Ctrl→LOGO so iced text_editor's Cmd+C/V/X/Z/A bindings fire on
|
||||
// Ctrl. Same trick the Windows shell uses; both action-modifier-on-Ctrl
|
||||
// platforms need it.
|
||||
if state.control_key() { flags |= 1 << 20; }
|
||||
flags
|
||||
}
|
||||
|
||||
fn decode_winit_modifiers(state: ModifiersState) -> iced_wgpu::core::keyboard::Modifiers {
|
||||
let mut m = iced_wgpu::core::keyboard::Modifiers::empty();
|
||||
if state.shift_key() { m |= iced_wgpu::core::keyboard::Modifiers::SHIFT; }
|
||||
if state.control_key() { m |= iced_wgpu::core::keyboard::Modifiers::CTRL; }
|
||||
if state.alt_key() { m |= iced_wgpu::core::keyboard::Modifiers::ALT; }
|
||||
if state.control_key() { m |= iced_wgpu::core::keyboard::Modifiers::LOGO; }
|
||||
m
|
||||
}
|
||||
|
||||
fn config_path() -> std::path::PathBuf {
|
||||
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
|
||||
if !xdg.is_empty() {
|
||||
return std::path::PathBuf::from(xdg).join("acord").join("config.json");
|
||||
}
|
||||
}
|
||||
if let Some(cfg) = dirs::config_dir() {
|
||||
return cfg.join("acord").join("config.json");
|
||||
}
|
||||
dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".acord")
|
||||
.join("config.json")
|
||||
}
|
||||
|
||||
/// Loads `icon.png` next to the exe. Returns None on any failure — winit
|
||||
/// silently uses the WM default in that case.
|
||||
fn load_window_icon() -> Option<winit::window::Icon> {
|
||||
let exe_dir = std::env::current_exe().ok()?.parent()?.to_path_buf();
|
||||
let png_path = exe_dir.join("icon.png");
|
||||
let bytes = if png_path.exists() {
|
||||
std::fs::read(&png_path).ok()?
|
||||
} else {
|
||||
// Fall back to repo-root assets when running via cargo run.
|
||||
let svg_path = std::path::PathBuf::from("assets/Acord.svg").canonicalize().ok()?;
|
||||
let output = std::process::Command::new("rsvg-convert")
|
||||
.args(["--width", "256", "--height", "256"])
|
||||
.arg(&svg_path)
|
||||
.output()
|
||||
.ok()?;
|
||||
if !output.status.success() { return None; }
|
||||
output.stdout
|
||||
};
|
||||
|
||||
let img = image::load_from_memory(&bytes).ok()?.into_rgba8();
|
||||
let (w, h) = img.dimensions();
|
||||
winit::window::Icon::from_rgba(img.into_raw(), w, h).ok()
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Config {
|
||||
path: PathBuf,
|
||||
data: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Config {
|
||||
pub fn load() -> Self {
|
||||
let dir = config_dir();
|
||||
std::fs::create_dir_all(&dir).ok();
|
||||
let notes = dir.join("notes");
|
||||
std::fs::create_dir_all(¬es).ok();
|
||||
|
||||
let path = dir.join("config.json");
|
||||
let data = std::fs::read_to_string(&path)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or_default();
|
||||
Self { path, data }
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
if let Ok(json) = serde_json::to_string_pretty(&self.data) {
|
||||
let _ = std::fs::write(&self.path, json);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str, default: &str) -> String {
|
||||
self.data.get(key).cloned().unwrap_or_else(|| default.to_string())
|
||||
}
|
||||
|
||||
pub fn set(&mut self, key: &str, value: &str) {
|
||||
self.data.insert(key.to_string(), value.to_string());
|
||||
self.save();
|
||||
}
|
||||
|
||||
pub fn theme_mode(&self) -> &str {
|
||||
self.data.get("themeMode").map(|s| s.as_str()).unwrap_or("auto")
|
||||
}
|
||||
|
||||
pub fn line_indicator(&self) -> &str {
|
||||
self.data.get("lineIndicatorMode").map(|s| s.as_str()).unwrap_or("on")
|
||||
}
|
||||
|
||||
pub fn gutter_rainbow(&self) -> bool {
|
||||
self.data.get("gutterRainbow").map(|s| s != "false").unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn notes_dir(&self) -> PathBuf {
|
||||
self.data.get("autoSaveDirectory")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| config_dir().join("notes"))
|
||||
}
|
||||
}
|
||||
|
||||
/// XDG-friendly config dir with `~/.acord` fallback for parity with the
|
||||
/// Windows shell. `$XDG_CONFIG_HOME/acord` if set, else `~/.config/acord`,
|
||||
/// else `~/.acord`.
|
||||
fn config_dir() -> PathBuf {
|
||||
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
|
||||
if !xdg.is_empty() {
|
||||
return PathBuf::from(xdg).join("acord");
|
||||
}
|
||||
}
|
||||
if let Some(cfg) = dirs::config_dir() {
|
||||
return cfg.join("acord");
|
||||
}
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".acord")
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
mod app;
|
||||
mod config;
|
||||
mod shortcuts;
|
||||
|
||||
fn main() {
|
||||
let event_loop = winit::event_loop::EventLoop::new().expect("event loop");
|
||||
let mut state = app::App::new();
|
||||
event_loop.run_app(&mut state).expect("run");
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
use winit::keyboard::{Key, ModifiersState, NamedKey, SmolStr};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
// LiveMode/EditorMode/ViewMode are dispatched but not yet bound to a shortcut
|
||||
// — Linux has no menu bar to expose them via, so they wait for either a key
|
||||
// binding decision or an iced-rendered menu inside the viewport.
|
||||
pub enum MenuAction {
|
||||
NewNote,
|
||||
Open,
|
||||
Save,
|
||||
SaveAs,
|
||||
Quit,
|
||||
Bold,
|
||||
Italic,
|
||||
InsertTable,
|
||||
Evaluate,
|
||||
LiveMode,
|
||||
EditorMode,
|
||||
ViewMode,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
ZoomReset,
|
||||
Find,
|
||||
Settings,
|
||||
ExportCrate,
|
||||
}
|
||||
|
||||
/// Matches an app-level shortcut. Returns Some(action) for combos that should
|
||||
/// fire a MenuAction; None for combos that should fall through to the
|
||||
/// viewport (cut/copy/paste/undo/redo/select-all are handled inside iced via
|
||||
/// the Ctrl→LOGO modifier alias, plain typing, navigation, etc.).
|
||||
pub fn match_shortcut(modifiers: ModifiersState, key: &Key) -> Option<MenuAction> {
|
||||
if !modifiers.control_key() {
|
||||
return None;
|
||||
}
|
||||
let shift = modifiers.shift_key();
|
||||
|
||||
match key {
|
||||
Key::Character(s) => match (shift, ascii_lower(s)) {
|
||||
(false, 'n') => Some(MenuAction::NewNote),
|
||||
(false, 'o') => Some(MenuAction::Open),
|
||||
(false, 's') => Some(MenuAction::Save),
|
||||
(true, 's') => Some(MenuAction::SaveAs),
|
||||
(false, 'q') => Some(MenuAction::Quit),
|
||||
(false, 'b') => Some(MenuAction::Bold),
|
||||
(false, 'i') => Some(MenuAction::Italic),
|
||||
(false, 't') => Some(MenuAction::InsertTable),
|
||||
(false, 'f') => Some(MenuAction::Find),
|
||||
(true, 'e') => Some(MenuAction::ExportCrate),
|
||||
(false, ',') => Some(MenuAction::Settings),
|
||||
(false, '=') | (false, '+') => Some(MenuAction::ZoomIn),
|
||||
(false, '-') => Some(MenuAction::ZoomOut),
|
||||
(true, '0') => Some(MenuAction::ZoomReset),
|
||||
_ => None,
|
||||
},
|
||||
Key::Named(NamedKey::Enter) => Some(MenuAction::Evaluate),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn ascii_lower(s: &SmolStr) -> char {
|
||||
s.chars().next().map(|c| c.to_ascii_lowercase()).unwrap_or('\0')
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux) ;;
|
||||
*) echo "wrong platform: $(uname -s) — use cargo xtask build" >&2; exit 1;;
|
||||
esac
|
||||
|
||||
# Pick the winit backend(s). Default builds enable both x11 and wayland so a
|
||||
# single binary works on either; ACORD_FEATURES overrides for cases where
|
||||
# only one backend is available (flatpak, stripped distros, debugging one
|
||||
# backend in isolation).
|
||||
detect_features() {
|
||||
if [ -n "${ACORD_FEATURES:-}" ]; then
|
||||
echo "$ACORD_FEATURES"
|
||||
return
|
||||
fi
|
||||
# No detection — both backends are linked by default. Override only when
|
||||
# you need to force one.
|
||||
echo ""
|
||||
}
|
||||
|
||||
FEATURES="$(detect_features)"
|
||||
echo "build: XDG_CURRENT_DESKTOP=${XDG_CURRENT_DESKTOP:-<unset>}, WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-<unset>}"
|
||||
|
||||
if [ -n "$FEATURES" ]; then
|
||||
echo "build: forcing features = $FEATURES"
|
||||
cargo build --release -p acord-linux --no-default-features --features "$FEATURES"
|
||||
else
|
||||
echo "build: linking both x11 and wayland backends"
|
||||
cargo build --release -p acord-linux
|
||||
fi
|
||||
|
||||
STAGE="$ROOT/build/bin"
|
||||
mkdir -p "$STAGE"
|
||||
|
||||
cp "$ROOT/target/release/acord" "$STAGE/Acord"
|
||||
chmod +x "$STAGE/Acord"
|
||||
|
||||
# Rasterize the SVG icon next to the binary so load_window_icon picks it up.
|
||||
if command -v rsvg-convert >/dev/null 2>&1 && [ -f "$ROOT/assets/Acord.svg" ]; then
|
||||
rsvg-convert --width 256 --height 256 "$ROOT/assets/Acord.svg" -o "$STAGE/icon.png"
|
||||
else
|
||||
echo "rsvg-convert not found or assets/Acord.svg missing — skipping icon"
|
||||
fi
|
||||
|
||||
[ -f "$ROOT/LICENCE" ] && cp "$ROOT/LICENCE" "$STAGE/LICENCE"
|
||||
|
||||
echo "Built: $STAGE/Acord"
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Debug build — same wiring as build.sh but unoptimised, with -g, and
|
||||
# launched in the foreground so Rust panics print straight to this terminal.
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux) ;;
|
||||
*) echo "wrong platform: $(uname -s) — use cargo xtask debug" >&2; exit 1;;
|
||||
esac
|
||||
|
||||
export RUST_BACKTRACE=1
|
||||
|
||||
if [ -n "${ACORD_FEATURES:-}" ]; then
|
||||
cargo build -p acord-linux --no-default-features --features "$ACORD_FEATURES"
|
||||
else
|
||||
cargo build -p acord-linux
|
||||
fi
|
||||
|
||||
EXE="$ROOT/target/debug/acord"
|
||||
|
||||
# Rasterize the icon next to the exe so the dev binary has a window icon too.
|
||||
if command -v rsvg-convert >/dev/null 2>&1 && [ -f "$ROOT/assets/Acord.svg" ]; then
|
||||
rsvg-convert --width 256 --height 256 "$ROOT/assets/Acord.svg" -o "$ROOT/target/debug/icon.png"
|
||||
fi
|
||||
|
||||
pkill -x acord 2>/dev/null || true
|
||||
sleep 0.3
|
||||
|
||||
echo
|
||||
echo "Launching $EXE — Rust panics will print below."
|
||||
echo "(Ctrl+C to exit, or quit Acord normally.)"
|
||||
echo "----------------------------------------------------------"
|
||||
exec "$EXE"
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux) ;;
|
||||
*) echo "wrong platform: $(uname -s) — use cargo xtask install" >&2; exit 1;;
|
||||
esac
|
||||
|
||||
bash "$ROOT/scripts/linux/build.sh"
|
||||
|
||||
# XDG-correct install: binary into ~/.local/bin (PATH on most distros), icon
|
||||
# + .desktop into ~/.local/share for the launcher menu.
|
||||
BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}"
|
||||
APP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
|
||||
ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/256x256/apps"
|
||||
|
||||
mkdir -p "$BIN_DIR" "$APP_DIR" "$ICON_DIR"
|
||||
|
||||
# Kill running instance before replacing the binary.
|
||||
pkill -x Acord 2>/dev/null || true
|
||||
sleep 0.3
|
||||
|
||||
install -m 755 "$ROOT/build/bin/Acord" "$BIN_DIR/Acord"
|
||||
|
||||
if [ -f "$ROOT/build/bin/icon.png" ]; then
|
||||
install -m 644 "$ROOT/build/bin/icon.png" "$ICON_DIR/acord.png"
|
||||
fi
|
||||
|
||||
cat > "$APP_DIR/acord.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Acord
|
||||
Comment=Native markdown editor with Cordial expressions and tables
|
||||
Exec=$BIN_DIR/Acord %F
|
||||
Icon=acord
|
||||
Terminal=false
|
||||
Categories=Utility;TextEditor;Office;
|
||||
MimeType=text/markdown;text/plain;
|
||||
EOF
|
||||
|
||||
# Update the desktop database so the launcher picks up the new entry. Quiet
|
||||
# fallback if the tool isn't installed.
|
||||
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||
update-desktop-database "$APP_DIR" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo "Installed:"
|
||||
echo " binary → $BIN_DIR/Acord"
|
||||
echo " icon → $ICON_DIR/acord.png"
|
||||
echo " desktop → $APP_DIR/acord.desktop"
|
||||
|
||||
case ":$PATH:" in
|
||||
*":$BIN_DIR:"*) ;;
|
||||
*) echo "note: $BIN_DIR is not on your PATH" >&2 ;;
|
||||
esac
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin) ;;
|
||||
*) echo "wrong platform: $(uname -s) — use cargo xtask build-universal" >&2; exit 1;;
|
||||
esac
|
||||
|
||||
BUILD="$ROOT/build"
|
||||
APP="$BUILD/bin/Acord.app"
|
||||
CONTENTS="$APP/Contents"
|
||||
|
|
@ -10,49 +17,39 @@ RESOURCES="$CONTENTS/Resources"
|
|||
SDK=$(xcrun --show-sdk-path)
|
||||
export MACOSX_DEPLOYMENT_TARGET=14.0
|
||||
|
||||
# 1. Build Rust for both architectures
|
||||
echo "Building Rust workspace (Universal)..."
|
||||
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
||||
|
||||
cargo build --release -p acord-viewport --target aarch64-apple-darwin
|
||||
cargo build --release -p acord-viewport --target x86_64-apple-darwin
|
||||
|
||||
# 2. Create Universal Rust Static Lib
|
||||
mkdir -p "$ROOT/target/universal"
|
||||
lipo -create \
|
||||
"$ROOT/target/aarch64-apple-darwin/release/libacord_viewport.a" \
|
||||
"$ROOT/target/x86_64-apple-darwin/release/libacord_viewport.a" \
|
||||
-output "$ROOT/target/universal/libacord_viewport.a"
|
||||
|
||||
# --- Icon Generation (Remains the same) ---
|
||||
# [Your existing rsvg-convert and iconutil code here]
|
||||
# TODO: regenerate AppIcon.icns from assets/Acord.svg here (see build.sh).
|
||||
|
||||
# --- Bundle structure ---
|
||||
mkdir -p "$MACOS" "$RESOURCES"
|
||||
cp "$ROOT/Info.plist" "$CONTENTS/Info.plist"
|
||||
[ -f "$BUILD/AppIcon.icns" ] && cp "$BUILD/AppIcon.icns" "$RESOURCES/AppIcon.icns"
|
||||
|
||||
# 3. Compile Swift for both architectures
|
||||
echo "Compiling Swift (Universal)..."
|
||||
SWIFT_FILES=("$ROOT"/src/*.swift)
|
||||
RUST_INCLUDES=(-import-objc-header "$ROOT/viewport/include/acord.h" -L "$ROOT/target/universal" -lacord_viewport)
|
||||
|
||||
# Compile arm64 slice
|
||||
swiftc -target arm64-apple-macosx14.0 -sdk "$SDK" "${RUST_INCLUDES[@]}" \
|
||||
-framework Cocoa -framework SwiftUI -framework Metal -framework MetalKit -O \
|
||||
-o "$MACOS/Acord_arm64" "${SWIFT_FILES[@]}"
|
||||
|
||||
# Compile x86_64 slice
|
||||
swiftc -target x86_64-apple-macosx14.0 -sdk "$SDK" "${RUST_INCLUDES[@]}" \
|
||||
-framework Cocoa -framework SwiftUI -framework Metal -framework MetalKit -O \
|
||||
-o "$MACOS/Acord_x86" "${SWIFT_FILES[@]}"
|
||||
|
||||
# 4. Merge Swift binaries into one Universal binary
|
||||
lipo -create "$MACOS/Acord_arm64" "$MACOS/Acord_x86" -output "$MACOS/Acord"
|
||||
rm "$MACOS/Acord_arm64" "$MACOS/Acord_x86"
|
||||
|
||||
# 5. Code sign the universal bundle
|
||||
codesign --force --sign - --deep "$APP"
|
||||
|
||||
echo "Successfully built Universal App: $APP"
|
||||
|
||||
echo "Built Universal App: $APP"
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin) ;;
|
||||
*) echo "wrong platform: $(uname -s) — use cargo xtask build" >&2; exit 1;;
|
||||
esac
|
||||
|
||||
BUILD="$ROOT/build"
|
||||
APP="$BUILD/bin/Acord.app"
|
||||
CONTENTS="$APP/Contents"
|
||||
|
|
@ -9,25 +16,22 @@ MACOS="$CONTENTS/MacOS"
|
|||
RESOURCES="$CONTENTS/Resources"
|
||||
|
||||
SDK=$(xcrun --show-sdk-path)
|
||||
|
||||
RUST_LIB="$ROOT/target/release"
|
||||
|
||||
export MACOSX_DEPLOYMENT_TARGET=14.0
|
||||
export ZERO_AR_DATE=0
|
||||
|
||||
echo "Building Rust workspace (release)..."
|
||||
cd "$ROOT" && cargo build --release -p acord-viewport
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: Rust build failed"
|
||||
exit 1
|
||||
fi
|
||||
cargo build --release -p acord-viewport
|
||||
|
||||
if [ ! -f "$RUST_LIB/libacord_viewport.a" ]; then
|
||||
echo "ERROR: libacord_viewport.a not found at $RUST_LIB"
|
||||
echo "ERROR: libacord_viewport.a not found at $RUST_LIB" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUST_FLAGS=(-import-objc-header "$ROOT/viewport/include/acord.h" -L "$RUST_LIB" -lacord_viewport)
|
||||
|
||||
# --- App icon from SVG via rsvg-convert ---
|
||||
# App icon from SVG via rsvg-convert.
|
||||
SVG="$ROOT/assets/Acord.svg"
|
||||
if [ -f "$SVG" ]; then
|
||||
echo "Generating app icon..."
|
||||
|
|
@ -46,19 +50,17 @@ if [ -f "$SVG" ]; then
|
|||
cp "$ICONSET/icon_512.png" "$ICONSET/icon_256x256@2x.png"
|
||||
cp "$ICONSET/icon_512.png" "$ICONSET/icon_512x512.png"
|
||||
cp "$ICONSET/icon_1024.png" "$ICONSET/icon_512x512@2x.png"
|
||||
rm -f "$ICONSET"/icon_*.png.tmp "$ICONSET"/icon_16.png "$ICONSET"/icon_32.png "$ICONSET"/icon_64.png "$ICONSET"/icon_128.png "$ICONSET"/icon_256.png "$ICONSET"/icon_512.png "$ICONSET"/icon_1024.png
|
||||
rm -f "$ICONSET"/icon_16.png "$ICONSET"/icon_32.png "$ICONSET"/icon_64.png \
|
||||
"$ICONSET"/icon_128.png "$ICONSET"/icon_256.png "$ICONSET"/icon_512.png \
|
||||
"$ICONSET"/icon_1024.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
|
||||
[ -f "$BUILD/AppIcon.icns" ] && cp "$BUILD/AppIcon.icns" "$RESOURCES/AppIcon.icns"
|
||||
|
||||
# --- Compile Swift ---
|
||||
echo "Compiling Swift (release)..."
|
||||
swiftc \
|
||||
-target arm64-apple-macosx14.0 \
|
||||
|
|
@ -75,9 +77,7 @@ swiftc \
|
|||
-o "$MACOS/Acord" \
|
||||
"$ROOT"/src/*.swift
|
||||
|
||||
# --- Code sign ---
|
||||
codesign --force --sign - "$APP"
|
||||
|
||||
echo "Built: $APP"
|
||||
|
||||
open /Users/pszsh/External/Repositories/Acord/build/bin/Acord.app
|
||||
open "$APP"
|
||||
|
|
@ -5,7 +5,14 @@ set -euo pipefail
|
|||
# launched in the foreground so Rust panics print straight to this terminal
|
||||
# (the panic hook in viewport/src/lib.rs flushes stderr before SIGABRT).
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin) ;;
|
||||
*) echo "wrong platform: $(uname -s) — use cargo xtask debug" >&2; exit 1;;
|
||||
esac
|
||||
|
||||
BUILD="$ROOT/build"
|
||||
APP="$BUILD/bin/Acord.app"
|
||||
CONTENTS="$APP/Contents"
|
||||
|
|
@ -20,23 +27,19 @@ export ZERO_AR_DATE=0
|
|||
export RUST_BACKTRACE=1
|
||||
|
||||
echo "Building Rust workspace (debug)..."
|
||||
cd "$ROOT" && cargo build -p acord-viewport
|
||||
cargo build -p acord-viewport
|
||||
|
||||
if [ ! -f "$RUST_LIB/libacord_viewport.a" ]; then
|
||||
echo "ERROR: libacord_viewport.a not found at $RUST_LIB"
|
||||
echo "ERROR: libacord_viewport.a not found at $RUST_LIB" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUST_FLAGS=(-import-objc-header "$ROOT/viewport/include/acord.h" -L "$RUST_LIB" -lacord_viewport)
|
||||
|
||||
# --- 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
|
||||
[ -f "$BUILD/AppIcon.icns" ] && cp "$BUILD/AppIcon.icns" "$RESOURCES/AppIcon.icns"
|
||||
|
||||
# --- Compile Swift (debug) ---
|
||||
echo "Compiling Swift (debug)..."
|
||||
swiftc \
|
||||
-target arm64-apple-macosx14.0 \
|
||||
|
|
@ -55,7 +58,6 @@ swiftc \
|
|||
|
||||
codesign --force --sign - "$APP"
|
||||
|
||||
# --- Kill existing, launch in foreground so stderr lands here ---
|
||||
pkill -f "Acord.app/Contents/MacOS/Acord" 2>/dev/null || true
|
||||
sleep 0.3
|
||||
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin) ;;
|
||||
*) echo "wrong platform: $(uname -s) — use cargo xtask install" >&2; exit 1;;
|
||||
esac
|
||||
|
||||
DEST="/Applications/Acord.app"
|
||||
|
||||
bash "$ROOT/scripts/macos/build.sh"
|
||||
|
||||
# Kill running instance before replacing.
|
||||
pkill -f "Acord.app/Contents/MacOS/Acord" 2>/dev/null || true
|
||||
sleep 0.5
|
||||
|
||||
echo "Installing to $DEST..."
|
||||
rm -rf "$DEST"
|
||||
cp -R "$ROOT/build/bin/Acord.app" "$DEST"
|
||||
|
||||
echo "Installed: $DEST"
|
||||
|
|
@ -0,0 +1,287 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Cross-compile + zip distributables from a single macOS host.
|
||||
#
|
||||
# Six targets:
|
||||
# macos-aarch64 macos-x86_64
|
||||
# windows-aarch64 windows-x86_64
|
||||
# linux-aarch64 linux-x86_64
|
||||
#
|
||||
# Output: dist/acord-<target>.zip per target.
|
||||
#
|
||||
# Tooling:
|
||||
# - rustup, swiftc, zip, codesign — assumed present on a dev mac
|
||||
# - rsvg-convert — brew install librsvg (for the macOS app icon)
|
||||
# - zig + cargo-zigbuild — used for windows/linux cross-compile
|
||||
# brew install zig
|
||||
# cargo install cargo-zigbuild
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin) ;;
|
||||
*) echo "package.sh: macOS host only (need swiftc + codesign)" >&2; exit 1;;
|
||||
esac
|
||||
|
||||
ALL_TARGETS=(
|
||||
macos-aarch64
|
||||
macos-x86_64
|
||||
windows-aarch64
|
||||
windows-x86_64
|
||||
linux-aarch64
|
||||
linux-x86_64
|
||||
)
|
||||
|
||||
usage() {
|
||||
cat >&2 <<EOF
|
||||
usage: cargo xtask package --all
|
||||
cargo xtask package --target <name> [--target <name> ...]
|
||||
|
||||
targets: ${ALL_TARGETS[*]}
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
TARGETS=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--all) TARGETS=("${ALL_TARGETS[@]}"); shift ;;
|
||||
--target) [ $# -ge 2 ] || usage; TARGETS+=("$2"); shift 2 ;;
|
||||
-h|--help) usage ;;
|
||||
*) echo "unknown arg: $1" >&2; usage ;;
|
||||
esac
|
||||
done
|
||||
[ ${#TARGETS[@]} -eq 0 ] && usage
|
||||
|
||||
need() { command -v "$1" >/dev/null 2>&1 || { echo "ERROR: $1 not found. $2" >&2; exit 1; }; }
|
||||
|
||||
need rustup "install rustup from https://rustup.rs"
|
||||
need swiftc "install Xcode Command Line Tools (xcode-select --install)"
|
||||
need zip "comes with macOS"
|
||||
|
||||
NEEDS_ZIG=0
|
||||
for t in "${TARGETS[@]}"; do
|
||||
case "$t" in windows-*|linux-*) NEEDS_ZIG=1 ;; esac
|
||||
done
|
||||
if [ $NEEDS_ZIG -eq 1 ]; then
|
||||
need zig "brew install zig"
|
||||
need cargo-zigbuild "cargo install cargo-zigbuild"
|
||||
fi
|
||||
|
||||
PKG="$ROOT/build/package"
|
||||
DIST="$ROOT/dist"
|
||||
mkdir -p "$PKG" "$DIST"
|
||||
|
||||
# Generate a 256px PNG once, reused by Windows + Linux. macOS uses a separate
|
||||
# .icns set generated below.
|
||||
ICON_PNG="$ROOT/build/icon.png"
|
||||
if [ ! -f "$ICON_PNG" ] || [ "$ROOT/assets/Acord.svg" -nt "$ICON_PNG" ]; then
|
||||
if command -v rsvg-convert >/dev/null 2>&1 && [ -f "$ROOT/assets/Acord.svg" ]; then
|
||||
rsvg-convert --width 256 --height 256 "$ROOT/assets/Acord.svg" -o "$ICON_PNG"
|
||||
fi
|
||||
fi
|
||||
|
||||
ensure_icns() {
|
||||
local icns="$ROOT/build/AppIcon.icns"
|
||||
if [ -f "$icns" ] && [ "$ROOT/assets/Acord.svg" -ot "$icns" ]; then return; fi
|
||||
[ -f "$ROOT/assets/Acord.svg" ] || return 0
|
||||
command -v rsvg-convert >/dev/null 2>&1 || return 0
|
||||
command -v iconutil >/dev/null 2>&1 || return 0
|
||||
|
||||
local iconset="$ROOT/build/AppIcon.iconset"
|
||||
rm -rf "$iconset"
|
||||
mkdir -p "$iconset"
|
||||
for size in 16 32 64 128 256 512 1024; do
|
||||
rsvg-convert --width="$size" --height="$size" \
|
||||
"$ROOT/assets/Acord.svg" -o "$iconset/icon_${size}.png"
|
||||
done
|
||||
cp "$iconset/icon_16.png" "$iconset/icon_16x16.png"
|
||||
cp "$iconset/icon_32.png" "$iconset/icon_16x16@2x.png"
|
||||
cp "$iconset/icon_32.png" "$iconset/icon_32x32.png"
|
||||
cp "$iconset/icon_64.png" "$iconset/icon_32x32@2x.png"
|
||||
cp "$iconset/icon_128.png" "$iconset/icon_128x128.png"
|
||||
cp "$iconset/icon_256.png" "$iconset/icon_128x128@2x.png"
|
||||
cp "$iconset/icon_256.png" "$iconset/icon_256x256.png"
|
||||
cp "$iconset/icon_512.png" "$iconset/icon_256x256@2x.png"
|
||||
cp "$iconset/icon_512.png" "$iconset/icon_512x512.png"
|
||||
cp "$iconset/icon_1024.png" "$iconset/icon_512x512@2x.png"
|
||||
rm -f "$iconset"/icon_16.png "$iconset"/icon_32.png "$iconset"/icon_64.png \
|
||||
"$iconset"/icon_128.png "$iconset"/icon_256.png "$iconset"/icon_512.png \
|
||||
"$iconset"/icon_1024.png
|
||||
iconutil -c icns "$iconset" -o "$icns"
|
||||
rm -rf "$iconset"
|
||||
}
|
||||
|
||||
zip_target() {
|
||||
local target="$1" path="$2"
|
||||
local out="$DIST/acord-${target}.zip"
|
||||
rm -f "$out"
|
||||
(cd "$(dirname "$path")" && zip -r -q "$out" "$(basename "$path")")
|
||||
echo " → $out ($(du -h "$out" | cut -f1))"
|
||||
}
|
||||
|
||||
build_macos() {
|
||||
local arch="$1" rust_target swift_target
|
||||
case "$arch" in
|
||||
aarch64) rust_target=aarch64-apple-darwin; swift_target=arm64-apple-macosx14.0 ;;
|
||||
x86_64) rust_target=x86_64-apple-darwin; swift_target=x86_64-apple-macosx14.0 ;;
|
||||
esac
|
||||
|
||||
rustup target add "$rust_target" >/dev/null 2>&1 || true
|
||||
ensure_icns
|
||||
|
||||
echo "==> macOS $arch ($rust_target / $swift_target)"
|
||||
|
||||
export MACOSX_DEPLOYMENT_TARGET=14.0
|
||||
export ZERO_AR_DATE=0
|
||||
cargo build --release -p acord-viewport --target "$rust_target"
|
||||
|
||||
local rust_lib="$ROOT/target/$rust_target/release"
|
||||
[ -f "$rust_lib/libacord_viewport.a" ] \
|
||||
|| { echo "ERROR: libacord_viewport.a missing for $rust_target" >&2; exit 1; }
|
||||
|
||||
local stage="$PKG/macos-${arch}"
|
||||
local app="$stage/Acord.app"
|
||||
rm -rf "$stage"
|
||||
mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources"
|
||||
cp "$ROOT/Info.plist" "$app/Contents/Info.plist"
|
||||
[ -f "$ROOT/build/AppIcon.icns" ] && cp "$ROOT/build/AppIcon.icns" "$app/Contents/Resources/AppIcon.icns"
|
||||
|
||||
local sdk
|
||||
sdk=$(xcrun --show-sdk-path)
|
||||
swiftc \
|
||||
-target "$swift_target" \
|
||||
-sdk "$sdk" \
|
||||
-import-objc-header "$ROOT/viewport/include/acord.h" \
|
||||
-L "$rust_lib" -lacord_viewport \
|
||||
-framework Cocoa -framework SwiftUI \
|
||||
-framework Metal -framework MetalKit \
|
||||
-framework QuartzCore -framework CoreGraphics -framework CoreFoundation \
|
||||
-O \
|
||||
-o "$app/Contents/MacOS/Acord" \
|
||||
"$ROOT"/src/*.swift
|
||||
|
||||
codesign --force --sign - "$app"
|
||||
zip_target "macos-${arch}" "$app"
|
||||
}
|
||||
|
||||
build_windows() {
|
||||
local arch="$1" rust_target
|
||||
case "$arch" in
|
||||
aarch64) rust_target=aarch64-pc-windows-msvc ;;
|
||||
x86_64) rust_target=x86_64-pc-windows-msvc ;;
|
||||
esac
|
||||
|
||||
rustup target add "$rust_target" >/dev/null 2>&1 || true
|
||||
|
||||
echo "==> Windows $arch ($rust_target via cargo-zigbuild)"
|
||||
|
||||
cargo zigbuild --release -p acord-windows --target "$rust_target"
|
||||
|
||||
local stage="$PKG/windows-${arch}/Acord"
|
||||
rm -rf "$stage"
|
||||
mkdir -p "$stage"
|
||||
|
||||
cp "$ROOT/target/$rust_target/release/acord.exe" "$stage/Acord.exe"
|
||||
[ -f "$ICON_PNG" ] && cp "$ICON_PNG" "$stage/icon.png"
|
||||
[ -f "$ROOT/LICENCE" ] && cp "$ROOT/LICENCE" "$stage/LICENCE"
|
||||
[ -f "$ROOT/README.md" ] && cp "$ROOT/README.md" "$stage/README.md"
|
||||
|
||||
zip_target "windows-${arch}" "$stage"
|
||||
}
|
||||
|
||||
build_linux() {
|
||||
local arch="$1" rust_target
|
||||
case "$arch" in
|
||||
aarch64) rust_target=aarch64-unknown-linux-gnu ;;
|
||||
x86_64) rust_target=x86_64-unknown-linux-gnu ;;
|
||||
esac
|
||||
|
||||
rustup target add "$rust_target" >/dev/null 2>&1 || true
|
||||
|
||||
echo "==> Linux $arch (${rust_target}.2.17 via cargo-zigbuild — both x11+wayland linked)"
|
||||
|
||||
# The .2.17 suffix targets glibc 2.17 (CentOS 7 baseline) for max distro
|
||||
# compatibility. zigbuild handles the symbol versioning via zig cc.
|
||||
cargo zigbuild --release -p acord-linux --target "${rust_target}.2.17"
|
||||
|
||||
local stage="$PKG/linux-${arch}/acord"
|
||||
rm -rf "$stage"
|
||||
mkdir -p "$stage"
|
||||
|
||||
cp "$ROOT/target/$rust_target/release/acord" "$stage/Acord"
|
||||
chmod +x "$stage/Acord"
|
||||
[ -f "$ICON_PNG" ] && cp "$ICON_PNG" "$stage/icon.png"
|
||||
[ -f "$ROOT/LICENCE" ] && cp "$ROOT/LICENCE" "$stage/LICENCE"
|
||||
[ -f "$ROOT/README.md" ] && cp "$ROOT/README.md" "$stage/README.md"
|
||||
|
||||
# Self-contained installer the user runs after unzipping. Outer EOF is
|
||||
# single-quoted so $BIN_DIR / $HERE / etc. survive into the install
|
||||
# script verbatim and expand only when the user runs it.
|
||||
cat > "$stage/install.sh" <<'INSTALLER_EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}"
|
||||
APP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
|
||||
ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/256x256/apps"
|
||||
mkdir -p "$BIN_DIR" "$APP_DIR" "$ICON_DIR"
|
||||
|
||||
pkill -x Acord 2>/dev/null || true
|
||||
sleep 0.2
|
||||
|
||||
install -m 755 "$HERE/Acord" "$BIN_DIR/Acord"
|
||||
[ -f "$HERE/icon.png" ] && install -m 644 "$HERE/icon.png" "$ICON_DIR/acord.png"
|
||||
|
||||
cat > "$APP_DIR/acord.desktop" <<DESKTOP
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Acord
|
||||
Comment=Native markdown editor with Cordial expressions and tables
|
||||
Exec=$BIN_DIR/Acord %F
|
||||
Icon=acord
|
||||
Terminal=false
|
||||
Categories=Utility;TextEditor;Office;
|
||||
MimeType=text/markdown;text/plain;
|
||||
DESKTOP
|
||||
|
||||
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||
update-desktop-database "$APP_DIR" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo "Installed:"
|
||||
echo " binary → $BIN_DIR/Acord"
|
||||
echo " icon → $ICON_DIR/acord.png"
|
||||
echo " desktop → $APP_DIR/acord.desktop"
|
||||
|
||||
case ":$PATH:" in
|
||||
*":$BIN_DIR:"*) ;;
|
||||
*) echo "note: $BIN_DIR is not on your PATH" >&2 ;;
|
||||
esac
|
||||
INSTALLER_EOF
|
||||
chmod +x "$stage/install.sh"
|
||||
|
||||
zip_target "linux-${arch}" "$stage"
|
||||
}
|
||||
|
||||
echo "packaging: ${TARGETS[*]}"
|
||||
echo
|
||||
|
||||
for t in "${TARGETS[@]}"; do
|
||||
case "$t" in
|
||||
macos-aarch64) build_macos aarch64 ;;
|
||||
macos-x86_64) build_macos x86_64 ;;
|
||||
windows-aarch64) build_windows aarch64 ;;
|
||||
windows-x86_64) build_windows x86_64 ;;
|
||||
linux-aarch64) build_linux aarch64 ;;
|
||||
linux-x86_64) build_linux x86_64 ;;
|
||||
*) echo "unknown target: $t (valid: ${ALL_TARGETS[*]})" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo
|
||||
echo "done. dist:"
|
||||
ls -lh "$DIST"
|
||||
|
|
@ -1,27 +1,23 @@
|
|||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
|
||||
$root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
||||
Set-Location $root
|
||||
|
||||
$buildDir = Join-Path $root "build\bin"
|
||||
New-Item -ItemType Directory -Force -Path $buildDir
|
||||
New-Item -ItemType Directory -Force -Path $buildDir | Out-Null
|
||||
|
||||
# 1. Add and Build Rust Targets
|
||||
Write-Host "Building Rust architectures..."
|
||||
rustup target add x86_64-pc-windows-msvc aarch64-pc-windows-msvc
|
||||
|
||||
# Build x64
|
||||
cargo build --release -p acord-windows --target x86_64-pc-windows-msvc
|
||||
# Build ARM64
|
||||
cargo build --release -p acord-windows --target aarch64-pc-windows-msvc
|
||||
|
||||
# 2. Organize Binaries
|
||||
$exeX64 = Join-Path $root "target\x86_64-pc-windows-msvc\release\acord.exe"
|
||||
$exeArm64 = Join-Path $root "target\aarch64-pc-windows-msvc\release\acord.exe"
|
||||
|
||||
Copy-Item $exeX64 -Destination (Join-Path $buildDir "acord_x64.exe")
|
||||
Copy-Item $exeArm64 -Destination (Join-Path $buildDir "acord_arm64.exe")
|
||||
|
||||
# 3. Handle Icons (SVG to PNG)
|
||||
$svg = Join-Path $root "assets\Acord.svg"
|
||||
$png = Join-Path $buildDir "icon.png"
|
||||
|
||||
|
|
@ -35,4 +31,3 @@ if (Test-Path $svg) {
|
|||
}
|
||||
|
||||
Write-Host "Built binaries to: $buildDir"
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
||||
Set-Location $root
|
||||
|
||||
Write-Host "Building Rust workspace (release)..."
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
||||
Set-Location $root
|
||||
|
||||
Write-Host "Building Rust workspace (debug)..."
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "xtask"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "xtask"
|
||||
path = "src/main.rs"
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, ExitCode};
|
||||
|
||||
const KNOWN_PLATFORMS: &[&str] = &["macos", "windows", "linux"];
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
let cmd = args.first().map(String::as_str).unwrap_or("");
|
||||
|
||||
if cmd.is_empty() || cmd == "help" || cmd == "--help" || cmd == "-h" {
|
||||
print_help();
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
|
||||
let extra_args: Vec<&String> = args.iter().skip(1).collect();
|
||||
let (action, platform) = parse(cmd);
|
||||
|
||||
let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.expect("xtask manifest must have a parent")
|
||||
.to_path_buf();
|
||||
|
||||
let (script, runner) = match platform.as_str() {
|
||||
"windows" => (
|
||||
repo_root.join(format!("scripts/windows/{action}.ps1")),
|
||||
vec![
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
],
|
||||
),
|
||||
"linux" | "macos" => (
|
||||
repo_root.join(format!("scripts/{platform}/{action}.sh")),
|
||||
vec!["bash"],
|
||||
),
|
||||
other => {
|
||||
eprintln!("unknown platform: {other}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
|
||||
if !script.exists() {
|
||||
eprintln!("script not found: {}", script.display());
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
|
||||
let extra_display = if extra_args.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(
|
||||
" {}",
|
||||
extra_args.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" "),
|
||||
)
|
||||
};
|
||||
eprintln!("→ {} {}{}", runner.join(" "), script.display(), extra_display);
|
||||
|
||||
let mut command = Command::new(runner[0]);
|
||||
for arg in &runner[1..] {
|
||||
command.arg(arg);
|
||||
}
|
||||
command.arg(&script);
|
||||
for a in &extra_args {
|
||||
command.arg(a.as_str());
|
||||
}
|
||||
command.current_dir(&repo_root);
|
||||
|
||||
match command.status() {
|
||||
Ok(status) if status.success() => ExitCode::SUCCESS,
|
||||
Ok(status) => ExitCode::from(status.code().unwrap_or(1) as u8),
|
||||
Err(e) => {
|
||||
eprintln!("failed to run {}: {e}", script.display());
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(cmd: &str) -> (String, String) {
|
||||
if let Some(idx) = cmd.rfind('-') {
|
||||
let suffix = &cmd[idx + 1..];
|
||||
if KNOWN_PLATFORMS.contains(&suffix) {
|
||||
return (cmd[..idx].to_string(), suffix.to_string());
|
||||
}
|
||||
}
|
||||
(cmd.to_string(), current_platform().to_string())
|
||||
}
|
||||
|
||||
fn current_platform() -> &'static str {
|
||||
match env::consts::OS {
|
||||
"linux" => "linux",
|
||||
"macos" => "macos",
|
||||
"windows" => "windows",
|
||||
other => {
|
||||
eprintln!("unsupported OS: {other}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
eprintln!("usage: cargo xtask <command>");
|
||||
eprintln!();
|
||||
eprintln!("commands:");
|
||||
eprintln!(" build release build for the current platform");
|
||||
eprintln!(" install release build + install (macOS: /Applications)");
|
||||
eprintln!(" debug debug build + foreground launch");
|
||||
eprintln!(" build-universal universal binary for the current platform");
|
||||
eprintln!(" package cross-compile + zip distributables");
|
||||
eprintln!(" --all all six targets");
|
||||
eprintln!(" --target <name> e.g. macos-aarch64, windows-x86_64");
|
||||
eprintln!();
|
||||
eprintln!("append -macos / -windows / -linux to any command to force a platform.");
|
||||
eprintln!(" e.g. cargo xtask build-universal-macos");
|
||||
}
|
||||
Loading…
Reference in New Issue