diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml new file mode 100644 index 0000000..e91c97e --- /dev/null +++ b/.forgejo/workflows/mirror.yml @@ -0,0 +1,21 @@ +name: Mirror to GitHub + +on: + push: + branches: ['**'] + tags: ['**'] + +jobs: + mirror: + runs-on: [self-hosted, hestia] + steps: + - name: Mirror to GitHub + run: | + set -euo pipefail + git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git + cd repo.git + # Mirror only branches and tags (not refs/pull/*, which GitHub rejects); + # --prune deletes GitHub refs that no longer exist on Forgejo. + git push --prune \ + "https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/breadpad.git" \ + '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' diff --git a/.forgejo/workflows/package.yml b/.forgejo/workflows/package.yml new file mode 100644 index 0000000..fdfdef9 --- /dev/null +++ b/.forgejo/workflows/package.yml @@ -0,0 +1,40 @@ +name: Build and publish package + +on: + push: + tags: ['v*'] + +jobs: + package: + runs-on: [self-hosted, hestia] + container: + image: archlinux:latest + steps: + # Note: no actions/checkout — the archlinux image has no Node, which JS + # actions require. Everything runs as shell steps and clones manually. + - name: Build and publish + env: + PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + pacman -Syu --noconfirm base-devel git rust cargo gtk4 gtk4-layer-shell + useradd -m builder + git config --global --add safe.directory '*' + git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src + cd /home/builder/src + git archive --format=tar.gz --prefix="breadpad-${VERSION}/" HEAD \ + > packaging/arch/breadpad-${VERSION}.tar.gz + SHA=$(sha256sum packaging/arch/breadpad-${VERSION}.tar.gz | awk '{print $1}') + sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/arch/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/arch/PKGBUILD + chown -R builder:builder /home/builder/src + # --nocheck: packaging builds the artifact; tests belong in a CI job. + su builder -c "cd /home/builder/src/packaging/arch && makepkg -f --noconfirm --nocheck" + PKG=$(find /home/builder/src/packaging/arch -name '*.pkg.tar.zst' | head -1) + curl -fsS -X PUT \ + -H "Authorization: token ${PUBLISH_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@${PKG}" \ + "https://git.breadway.dev/api/packages/Breadway/arch/os" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e8c3b99..e492838 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ permissions: env: DL_DIR: /srv/breadway-dl - ECOSYSTEM_DIR: /home/breadway/Projects/bread-ecosystem + ECOSYSTEM_DIR: /tmp/bread-ecosystem-ci jobs: build: @@ -36,16 +36,12 @@ jobs: done cp breadpad.example.toml "${PKG_DIR}/" cp bakery.toml "${PKG_DIR}/bakery.toml" - ln -sfn "${PKG_DIR}" "${DL_DIR}/breadpad/latest" + ln -sfn "${VERSION}" "${DL_DIR}/breadpad/latest" - name: ensure bread-ecosystem run: | - if [[ -d "${ECOSYSTEM_DIR}/.git" ]]; then - git -C "${ECOSYSTEM_DIR}" pull --ff-only - else - mkdir -p "$(dirname "${ECOSYSTEM_DIR}")" - git clone https://github.com/Breadway/bread-ecosystem.git "${ECOSYSTEM_DIR}" - fi + rm -rf "${ECOSYSTEM_DIR}" + git clone https://github.com/Breadway/bread-ecosystem.git "${ECOSYSTEM_DIR}" - name: regenerate index.json run: bash "${ECOSYSTEM_DIR}/scripts/gen-index.sh" diff --git a/Cargo.lock b/Cargo.lock index d08b439..756f0c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,17 +304,18 @@ dependencies = [ [[package]] name = "bread-theme" -version = "0.1.0" -source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.1.0#6b5f4f475f66a645b08cb865e6dda8228d23679b" +version = "0.2.3" +source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.8#77417d552130281ff787e07d52541eb25e9d533b" dependencies = [ "dirs 5.0.1", + "gtk4", "serde", "serde_json", ] [[package]] name = "breadman" -version = "0.1.0" +version = "0.3.4" dependencies = [ "anyhow", "breadpad-shared", @@ -331,7 +332,7 @@ dependencies = [ [[package]] name = "breadpad" -version = "0.1.0" +version = "0.3.4" dependencies = [ "anyhow", "breadpad-shared", @@ -350,7 +351,7 @@ dependencies = [ [[package]] name = "breadpad-shared" -version = "0.1.0" +version = "0.3.4" dependencies = [ "anyhow", "bread-theme", @@ -376,7 +377,7 @@ dependencies = [ [[package]] name = "breadpad-test" -version = "0.1.0" +version = "0.3.4" dependencies = [ "anyhow", "breadpad-shared", diff --git a/Cargo.toml b/Cargo.toml index 5ec24f9..4092bed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.1.0" +version = "0.3.4" edition = "2021" license = "MIT" authors = ["Breadway"] @@ -24,7 +24,7 @@ chrono = { version = "0.4", features = ["serde"] } rrule = "0.12" tokio = { version = "1", features = ["full"] } zbus = { version = "4", default-features = false, features = ["tokio"] } -ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "ndarray", "tracing", "api-24", "load-dynamic"] } +ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "ndarray", "tracing", "api-24", "rocm", "load-dynamic"] } ndarray = "0.16" tokenizers = { version = "0.21", default-features = false, features = ["http", "fancy-regex"] } gtk4 = { version = "0.11", features = ["v4_12"] } diff --git a/README.md b/README.md index 02ec8a2..b39ea3d 100644 --- a/README.md +++ b/README.md @@ -190,8 +190,41 @@ enabled = true # set false to never call Ollama [reminders] default_morning = "08:00" # what "tomorrow morning" resolves to missed_grace_minutes = 60 # how long after boot to still fire a missed reminder + +[calendar] +enabled = false # turn on CalDAV sync (see below) +url = "" # CalDAV calendar collection URL +username = "" +password = "" # app password / token recommended ``` +### Calendar sync (CalDAV) + +When `[calendar].enabled = true`, reminders and dated notes are pushed to a +CalDAV calendar as events (tracked by `caldav_uid` on each note), so they show +up alongside the rest of your calendar. + +1. Find your calendar's **collection URL**. It's the per-calendar CalDAV path, + not the server root — e.g. Nextcloud: + `https://host/remote.php/dav/calendars///`. +2. Create an **app password** for breadpad (don't use your main password): + Nextcloud → Settings → Security → *Devices & sessions* → "Create new app + password". Most CalDAV servers have an equivalent. +3. Fill in `breadpad.toml` (or BOS Settings → breadpad → Calendar): + + ```toml + [calendar] + enabled = true + url = "https://host/remote.php/dav/calendars/me/breadpad/" + username = "me" + password = "xxxx-xxxx-xxxx-xxxx" + ``` +4. Restart breadpad. New dated/reminder notes sync up; the `caldav_uid` field + links each note to its event so updates and deletes stay in step. + +If the server is unreachable, breadpad logs a warning and keeps the note +locally — sync is best-effort and never blocks capture. + --- ## Usage diff --git a/bakery.toml b/bakery.toml index e1c028d..25e9033 100644 --- a/bakery.toml +++ b/bakery.toml @@ -2,6 +2,7 @@ name = "breadpad" description = "Quick-capture scratchpad and note viewer with AI classification" binaries = ["breadpad", "breadman"] system_deps = ["gtk4", "gtk4-layer-shell"] +optional_system_deps = ["rocm-hip-runtime", "ollama", "hyprland"] bread_deps = [] [config] diff --git a/breadman/src/main.rs b/breadman/src/main.rs index 0ee89bc..d46557a 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -4,7 +4,6 @@ use breadpad_shared::{ parser::parse_rule_based, scheduler::Scheduler, store::Store, - theme::{build_css, load_palette}, types::{Note, NoteType, RecurrenceRule}, }; use chrono::Local; @@ -924,19 +923,7 @@ fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState) { // ── CSS ─────────────────────────────────────────────────────────────────────── fn apply_css(_cfg: &Config) { - let palette = load_palette(); - let user_css = std::fs::read_to_string(breadpad_shared::config::style_css_path()).ok(); - let css = build_css(&palette, user_css.as_deref()); - - let provider = gtk4::CssProvider::new(); - provider.load_from_string(&css); - let Some(display) = gtk4::gdk::Display::default() else { - tracing::warn!("no default display; skipping CSS provider"); - return; - }; - gtk4::style_context_add_provider_for_display( - &display, - &provider, - gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, - ); + // Hot-reloads on `bread-theme reload` (recolours to the new pywal palette + // and re-reads the user's style.css). See breadpad_shared::theme::apply_live. + breadpad_shared::theme::apply_live(); } diff --git a/breadpad-shared/Cargo.toml b/breadpad-shared/Cargo.toml index 610c040..01470f9 100644 --- a/breadpad-shared/Cargo.toml +++ b/breadpad-shared/Cargo.toml @@ -7,7 +7,7 @@ authors.workspace = true [dependencies] -bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.1.0" } +bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] } anyhow.workspace = true tracing.workspace = true serde.workspace = true diff --git a/breadpad-shared/src/classifier.rs b/breadpad-shared/src/classifier.rs index fef87e0..e752a31 100644 --- a/breadpad-shared/src/classifier.rs +++ b/breadpad-shared/src/classifier.rs @@ -247,12 +247,18 @@ fn try_load_session( path: &std::path::Path, ) -> (Option, ExecutionProvider) { // Try ROCm (iGPU) first, fall back to CPU. - match build_onnx_session(path, ort::ep::ROCm::default().build()) { - Ok(s) => { - tracing::info!("ONNX session loaded (ROCm iGPU)"); - return (Some(s), ExecutionProvider::Gpu); + let rocm_available = { + use ort::execution_providers::ExecutionProvider as _; + ort::ep::ROCm::default().is_available().unwrap_or(false) + }; + if rocm_available { + match build_onnx_session(path, ort::ep::ROCm::default().build()) { + Ok(s) => { + tracing::info!("ONNX session loaded (ROCm iGPU)"); + return (Some(s), ExecutionProvider::Gpu); + } + Err(e) => tracing::debug!("ROCm EP unavailable: {}; trying CPU", e), } - Err(e) => tracing::debug!("ROCm EP unavailable: {}; trying CPU", e), } match build_onnx_session(path, ort::ep::CPU::default().build()) { Ok(s) => { diff --git a/breadpad-shared/src/theme.rs b/breadpad-shared/src/theme.rs index adc76dc..cb1139a 100644 --- a/breadpad-shared/src/theme.rs +++ b/breadpad-shared/src/theme.rs @@ -1,31 +1,34 @@ pub use bread_theme::{load_palette, Palette}; -/// Generate the full breadpad CSS string. The base colour variables come from -/// `bread-theme`; the widget rules below are breadpad-specific. +/// Apply breadpad/breadman's stylesheet and keep it live across palette changes. +/// [`build_css`] bundles the shared component sheet with the app's own rules from +/// the current pywal palette; `bread_theme::gtk::apply_app_css` re-runs this +/// whenever `bread-theme reload` rewrites the shared theme file, so the UI +/// recolours in place (and re-reads the user's `style.css` override too). +pub fn apply_live() { + bread_theme::gtk::apply_app_css(|| { + let palette = load_palette(); + let user_css = std::fs::read_to_string(crate::config::style_css_path()).ok(); + build_css(&palette, user_css.as_deref()) + }); +} + +/// Generate the full breadpad/breadman CSS string. The base — `@define-color` +/// palette, fonts, and generic widget styling — comes from the shared +/// `bread_theme::stylesheet`, so breadpad and breadman look identical to the +/// rest of the ecosystem. Only breadpad-specific component rules are appended. pub fn build_css(palette: &Palette, user_css: Option<&str>) -> String { - let mut css = format!( + // Shared ecosystem base (define-colors incl. accent, font, buttons, entries, + // switches, lists, cards, scrollbars). `overlay` here is color7 — consistent + // with every other bread app (breadpad previously mapped it to color0). + let mut css = bread_theme::stylesheet(palette); + + css.push_str( r#" -@define-color bg {bg}; -@define-color fg {fg}; -@define-color red {c1}; -@define-color green {c2}; -@define-color yellow {c3}; -@define-color blue {c4}; -@define-color pink {c5}; -@define-color teal {c6}; -@define-color overlay {c0}; +/* breadpad/breadman-specific components */ +window { border-radius: 8px; } -* {{ - font-family: 'Varela Round', sans-serif; -}} - -window {{ - background-color: @bg; - color: @fg; - border-radius: 8px; -}} - -.popup-entry {{ +.popup-entry { background: @bg; color: @fg; border: 2px solid @blue; @@ -33,80 +36,59 @@ window {{ padding: 12px 16px; font-size: 14px; caret-color: @fg; -}} +} -.popup-entry:focus {{ +.popup-entry:focus { outline: none; border-color: @teal; -}} +} -.type-chip {{ +.type-chip { background: @overlay; - color: @fg; + color: @on-overlay; border-radius: 999px; padding: 4px 12px; font-size: 12px; margin: 4px; -}} +} -.type-chip.active {{ +.type-chip.active { background: @blue; - color: @bg; -}} + color: @on-accent; +} -.confirm-button {{ +.confirm-button { background: @blue; - color: @bg; + color: @on-accent; border: none; border-radius: 8px; padding: 8px 16px; font-weight: bold; -}} +} -.note-card {{ +.note-card { background: shade(@bg, 1.1); border-radius: 8px; padding: 12px; margin: 8px; border-left: 3px solid @blue; -}} +} -.note-card:hover {{ +.note-card:hover { background: shade(@bg, 1.2); -}} +} -.search-entry {{ +.search-entry { background: shade(@bg, 1.1); color: @fg; border: 1px solid @overlay; border-radius: 6px; padding: 8px 12px; -}} - -.search-entry:focus {{ - border-color: @blue; - outline: none; -}} -"#, - bg = palette.background, - fg = palette.foreground, - c0 = palette.color0, - c1 = palette.color1, - c2 = palette.color2, - c3 = palette.color3, - c4 = palette.color4, - c5 = palette.color5, - c6 = palette.color6, - ); - - css.push_str(r#" -.dim-label { - color: alpha(@fg, 0.5); - font-size: 12px; } -.sidebar { - background: shade(@bg, 0.93); +.search-entry:focus { + border-color: @blue; + outline: none; } .sidebar-row { @@ -121,7 +103,7 @@ window {{ .sidebar-row:selected { background: @blue; - color: @bg; + color: @on-accent; font-weight: 500; } @@ -217,20 +199,6 @@ window {{ } .snooze-option:hover { background: shade(@bg, 1.2); } - -entry { - background: shade(@bg, 1.1); - color: @fg; - border: 1px solid @overlay; - border-radius: 6px; - caret-color: @fg; - padding: 5px 10px; -} - -entry:focus { - border-color: @blue; - outline: none; -} "#); if let Some(extra) = user_css { diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index c840557..aa924d0 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -2,10 +2,9 @@ use anyhow::Result; use breadpad_shared::{ calendar::CalDavClient, classifier::Classifier, - config::{style_css_path, Config}, + config::Config, scheduler::Scheduler, store::Store, - theme::{build_css, load_palette}, types::{Note, NoteType}, }; use gtk4::{glib, prelude::*}; @@ -765,19 +764,7 @@ fn save_note_classified( } fn apply_css(_cfg: &Config) { - let palette = load_palette(); - let user_css = std::fs::read_to_string(style_css_path()).ok(); - let css = build_css(&palette, user_css.as_deref()); - - let provider = gtk4::CssProvider::new(); - provider.load_from_string(&css); - let Some(display) = gtk4::gdk::Display::default() else { - tracing::warn!("no default display; skipping CSS provider"); - return; - }; - gtk4::style_context_add_provider_for_display( - &display, - &provider, - gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, - ); + // Hot-reloads on `bread-theme reload` (recolours to the new pywal palette + // and re-reads the user's style.css). See breadpad_shared::theme::apply_live. + breadpad_shared::theme::apply_live(); } diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD new file mode 100644 index 0000000..22511b6 --- /dev/null +++ b/packaging/arch/PKGBUILD @@ -0,0 +1,38 @@ +# Maintainer: Breadway + +pkgname=breadpad +pkgver=0.3.1 +pkgrel=1 +pkgdesc="Quick-capture scratchpad and note viewer with AI classification" +arch=('x86_64') +url="https://github.com/Breadway/breadpad" +license=('MIT') +# Some Rust deps (ring/mlua) build vendored C/asm into static archives; makepkg's +# default -flto=auto emits GCC LTO bitcode the Rust (lld) link cannot read, +# causing undefined-symbol errors. Disable LTO. +options=(!lto !debug) +depends=('gtk4' 'gtk4-layer-shell') +optdepends=( + 'ollama: local AI note classification' + 'hyprland: scratchpad window integration' +) +makedepends=('rust' 'cargo') +source=("${pkgname}-${pkgver}.tar.gz") +sha256sums=('SKIP') + +build() { + cd "${srcdir}/${pkgname}-${pkgver}" + cargo build --release --locked +} + +check() { + cd "${srcdir}/${pkgname}-${pkgver}" + cargo test --release --locked --workspace +} + +package() { + cd "${srcdir}/${pkgname}-${pkgver}" + install -Dm755 target/release/breadpad "${pkgdir}/usr/bin/breadpad" + install -Dm755 target/release/breadman "${pkgdir}/usr/bin/breadman" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +}