Merge fix/ci-arch-container: pinned Arch CI + clippy/test fixes
All checks were successful
dev release / build (push) Successful in 2m9s
All checks were successful
dev release / build (push) Successful in 2m9s
Replaces the from-source libadwaita build (repeatedly broken by version drift) with a pinned Arch container that gets everything prebuilt via pacman, adds a fast-fail check.yml for feature/fix branches, and fixes the clippy/test debt that check.yml surfaced now that it's enforced for the first time.
This commit is contained in:
commit
96a954cc15
19 changed files with 198 additions and 171 deletions
24
.forgejo/workflows/check.yml
Normal file
24
.forgejo/workflows/check.yml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
name: check
|
||||||
|
|
||||||
|
# Fast-fail lint/test on short-lived work branches, before it ever reaches
|
||||||
|
# main and triggers a dev-track release build.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['feature/**', 'fix/**']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: [self-hosted, hestia]
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
rm -rf src && mkdir src
|
||||||
|
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
||||||
|
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
||||||
|
|
||||||
|
- name: clippy
|
||||||
|
run: cd src && bash ci/build.sh cargo clippy --workspace --all-targets --locked -- -D warnings
|
||||||
|
|
||||||
|
- name: test
|
||||||
|
run: cd src && bash ci/build.sh cargo test --workspace --locked
|
||||||
|
|
@ -15,27 +15,8 @@ jobs:
|
||||||
git clone --branch main --depth 1 \
|
git clone --branch main --depth 1 \
|
||||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
||||||
|
|
||||||
- name: build inside container
|
- name: build
|
||||||
run: |
|
run: cd src && bash ci/build.sh cargo build --release --locked
|
||||||
docker run --rm \
|
|
||||||
-v "$(pwd)/src:/workspace" \
|
|
||||||
-w /workspace \
|
|
||||||
fedora:41 \
|
|
||||||
bash -c "
|
|
||||||
set -euo pipefail
|
|
||||||
dnf install -y gcc gcc-c++ pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel
|
|
||||||
|
|
||||||
# Build Libadwaita 1.7.0 from source inside container (skipping tests & docs)
|
|
||||||
git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita
|
|
||||||
meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr -Dtests=false -Ddoc=false
|
|
||||||
ninja -C /tmp/libadwaita/build
|
|
||||||
ninja -C /tmp/libadwaita/build install
|
|
||||||
|
|
||||||
# Install Rust and build
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
|
||||||
source \$HOME/.cargo/env
|
|
||||||
cargo build --release --locked
|
|
||||||
"
|
|
||||||
|
|
||||||
- name: compute dev version
|
- name: compute dev version
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@ name: beta (rc) release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: ['v*']
|
tags: ['v*-rc.*']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: ${{ contains(github.ref_name, '-rc.') }}
|
|
||||||
runs-on: [self-hosted, hestia]
|
runs-on: [self-hosted, hestia]
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
|
|
@ -16,27 +15,8 @@ jobs:
|
||||||
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
||||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
||||||
|
|
||||||
- name: build inside container
|
- name: build
|
||||||
run: |
|
run: cd src && bash ci/build.sh cargo build --release --locked
|
||||||
docker run --rm \
|
|
||||||
-v "$(pwd)/src:/workspace" \
|
|
||||||
-w /workspace \
|
|
||||||
fedora:41 \
|
|
||||||
bash -c "
|
|
||||||
set -euo pipefail
|
|
||||||
dnf install -y gcc gcc-c++ pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel
|
|
||||||
|
|
||||||
# Build Libadwaita 1.7.0 from source inside container (skipping tests & docs)
|
|
||||||
git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita
|
|
||||||
meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr -Dtests=false -Ddoc=false
|
|
||||||
ninja -C /tmp/libadwaita/build
|
|
||||||
ninja -C /tmp/libadwaita/build install
|
|
||||||
|
|
||||||
# Install Rust and build
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
|
||||||
source \$HOME/.cargo/env
|
|
||||||
cargo build --release --locked
|
|
||||||
"
|
|
||||||
|
|
||||||
- name: prepare artifacts
|
- name: prepare artifacts
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ name: release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: ["v*"]
|
tags: ['v*']
|
||||||
|
tags-ignore: ['v*-rc.*']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: ${{ !contains(github.ref_name, '-rc.') }}
|
|
||||||
runs-on: [self-hosted, hestia]
|
runs-on: [self-hosted, hestia]
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
|
|
@ -16,27 +16,8 @@ jobs:
|
||||||
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
||||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
||||||
|
|
||||||
- name: build inside container
|
- name: build
|
||||||
run: |
|
run: cd src && bash ci/build.sh cargo build --release --locked
|
||||||
docker run --rm \
|
|
||||||
-v "$(pwd)/src:/workspace" \
|
|
||||||
-w /workspace \
|
|
||||||
fedora:41 \
|
|
||||||
bash -c "
|
|
||||||
set -euo pipefail
|
|
||||||
dnf install -y gcc gcc-c++ pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel
|
|
||||||
|
|
||||||
# Build Libadwaita 1.7.0 from source inside container (skipping tests & docs)
|
|
||||||
git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita
|
|
||||||
meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr -Dtests=false -Ddoc=false
|
|
||||||
ninja -C /tmp/libadwaita/build
|
|
||||||
ninja -C /tmp/libadwaita/build install
|
|
||||||
|
|
||||||
# Install Rust and build
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
|
||||||
source \$HOME/.cargo/env
|
|
||||||
cargo build --release --locked
|
|
||||||
"
|
|
||||||
|
|
||||||
- name: prepare artifacts
|
- name: prepare artifacts
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
|
|
@ -69,13 +69,21 @@ cargo test --release --workspace
|
||||||
|
|
||||||
## CI
|
## CI
|
||||||
|
|
||||||
|
- `check.yml` — clippy + test, triggered on push to `feature/**`/`fix/**`.
|
||||||
|
Fast-fail before anything reaches `main`.
|
||||||
- `dev-release.yml` — triggered on push to `main`.
|
- `dev-release.yml` — triggered on push to `main`.
|
||||||
- `rc-release.yml` — triggered on any `vX.Y.Z-rc.N` tag push.
|
- `rc-release.yml` — triggered on any `vX.Y.Z-rc.N` tag push.
|
||||||
- `release.yml` — triggered on any other `v*` tag push, cuts the actual
|
- `release.yml` — triggered on any other `v*` tag push, cuts the actual
|
||||||
stable release.
|
stable release.
|
||||||
|
|
||||||
All CI runs on a self-hosted runner; nothing runs automatically on plain
|
All of these build inside a pinned Arch Linux container (`ci/Containerfile`,
|
||||||
commits or PRs beyond the track builds above. See
|
run via `ci/build.sh`) on a self-hosted runner — not the runner host's
|
||||||
|
native environment. Arch's repos carry current `gtk4`/`libadwaita`/
|
||||||
|
`gtk4-layer-shell` as prebuilt packages, so there's no from-source library
|
||||||
|
build to go stale. The image is rebuilt (and re-cached by Docker) only when
|
||||||
|
`ci/Containerfile` changes, so a plain push doesn't refetch or recompile
|
||||||
|
the toolchain. Nothing runs automatically on plain commits or PRs beyond
|
||||||
|
the jobs listed above. See
|
||||||
[bread-ecosystem's docs/release-channels.md](https://git.breadway.dev/Breadway/bread-ecosystem/src/branch/main/docs/release-channels.md)
|
[bread-ecosystem's docs/release-channels.md](https://git.breadway.dev/Breadway/bread-ecosystem/src/branch/main/docs/release-channels.md)
|
||||||
for the full policy, including how a new product gets wired onto these tracks.
|
for the full policy, including how a new product gets wired onto these tracks.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,13 +108,15 @@ mod args {
|
||||||
|
|
||||||
// ── AppState ──────────────────────────────────────────────────────────────────
|
// ── AppState ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type ErrorLog = Rc<RefCell<Vec<(chrono::DateTime<Local>, String)>>>;
|
||||||
|
|
||||||
/// Shared UI state, cheap to clone (all fields are Rc/Arc).
|
/// Shared UI state, cheap to clone (all fields are Rc/Arc).
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
store: Arc<Store>,
|
store: Arc<Store>,
|
||||||
notes: Rc<RefCell<Vec<Note>>>,
|
notes: Rc<RefCell<Vec<Note>>>,
|
||||||
cfg: Rc<RefCell<Config>>,
|
cfg: Rc<RefCell<Config>>,
|
||||||
errors: Rc<RefCell<Vec<(chrono::DateTime<Local>, String)>>>,
|
errors: ErrorLog,
|
||||||
active_view: Rc<RefCell<String>>,
|
active_view: Rc<RefCell<String>>,
|
||||||
stack: gtk4::Stack,
|
stack: gtk4::Stack,
|
||||||
window: gtk4::ApplicationWindow,
|
window: gtk4::ApplicationWindow,
|
||||||
|
|
@ -579,7 +581,7 @@ fn build_note_list(
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let mut sorted: Vec<Note> = notes.iter().filter(|n| !n.done).cloned().collect();
|
let mut sorted: Vec<Note> = notes.iter().filter(|n| !n.done).cloned().collect();
|
||||||
sorted.sort_by(|a, b| b.created.cmp(&a.created));
|
sorted.sort_by_key(|n| std::cmp::Reverse(n.created));
|
||||||
|
|
||||||
if sorted.is_empty() {
|
if sorted.is_empty() {
|
||||||
let action = empty_new_type.map(|nt| views::row::new_note_action(nt, state.window.clone(), state.clone()));
|
let action = empty_new_type.map(|nt| views::row::new_note_action(nt, state.window.clone(), state.clone()));
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ pub fn build(entries: &[(DateTime<chrono::Local>, String)]) -> gtk4::ScrolledWin
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let time_label = gtk4::Label::builder()
|
let time_label = gtk4::Label::builder()
|
||||||
.label(&ts.format("%H:%M:%S").to_string())
|
.label(ts.format("%H:%M:%S").to_string())
|
||||||
.width_chars(10)
|
.width_chars(10)
|
||||||
.xalign(0.0)
|
.xalign(0.0)
|
||||||
.css_classes(["dim-label"])
|
.css_classes(["dim-label"])
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ impl OllamaClient {
|
||||||
let classification: OllamaClassification = extract_json(&ollama_resp.response)
|
let classification: OllamaClassification = extract_json(&ollama_resp.response)
|
||||||
.ok_or_else(|| anyhow::anyhow!(
|
.ok_or_else(|| anyhow::anyhow!(
|
||||||
"no JSON object found in response — raw: {:?}",
|
"no JSON object found in response — raw: {:?}",
|
||||||
&ollama_resp.response
|
ollama_resp.response
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
let note_type = classification
|
let note_type = classification
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ impl Default for RemindersConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct CalendarConfig {
|
pub struct CalendarConfig {
|
||||||
#[serde(default = "default_calendar_enabled")]
|
#[serde(default = "default_calendar_enabled")]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
|
@ -150,17 +150,6 @@ pub struct CalendarConfig {
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CalendarConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
CalendarConfig {
|
|
||||||
enabled: false,
|
|
||||||
url: String::new(),
|
|
||||||
username: String::new(),
|
|
||||||
password: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,67 @@ pub fn parse_rule_based(text: &str, default_morning: &str) -> ClassificationResu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn infer_type(text: &str, has_time: bool, has_rrule: bool) -> NoteType {
|
||||||
|
let lower = text.to_lowercase();
|
||||||
|
if has_rrule || has_time {
|
||||||
|
return NoteType::Reminder;
|
||||||
|
}
|
||||||
|
if lower.contains("buy ")
|
||||||
|
|| lower.contains("pick up")
|
||||||
|
|| lower.contains("clean ")
|
||||||
|
|| lower.starts_with("call ")
|
||||||
|
|| lower.starts_with("email ")
|
||||||
|
|| lower.starts_with("fix ")
|
||||||
|
|| lower.starts_with("check ")
|
||||||
|
|| lower.starts_with("finish ")
|
||||||
|
|| lower.starts_with("write ")
|
||||||
|
|| lower.starts_with("update ")
|
||||||
|
|| lower.starts_with("prepare ")
|
||||||
|
|| lower.starts_with("schedule ")
|
||||||
|
|| lower.starts_with("organize ")
|
||||||
|
|| lower.starts_with("deploy ")
|
||||||
|
|| lower.starts_with("install ")
|
||||||
|
|| lower.starts_with("send ")
|
||||||
|
|| lower.starts_with("submit ")
|
||||||
|
|| lower.starts_with("create ")
|
||||||
|
|| lower.starts_with("setup ")
|
||||||
|
|| lower.starts_with("restore ")
|
||||||
|
|| lower.starts_with("archive ")
|
||||||
|
|| lower.starts_with("export ")
|
||||||
|
|| lower.starts_with("import ")
|
||||||
|
|| lower.starts_with("approve ")
|
||||||
|
|| lower.starts_with("configure ")
|
||||||
|
|| lower.starts_with("refactor ")
|
||||||
|
|| lower.starts_with("review ")
|
||||||
|
{
|
||||||
|
return NoteType::Todo;
|
||||||
|
}
|
||||||
|
if lower.starts_with("what if ")
|
||||||
|
|| lower.starts_with("idea:")
|
||||||
|
|| lower.contains("could ")
|
||||||
|
|| lower.contains("maybe ")
|
||||||
|
|| lower.starts_with("should we ")
|
||||||
|
{
|
||||||
|
return NoteType::Idea;
|
||||||
|
}
|
||||||
|
if lower.starts_with("why ")
|
||||||
|
|| lower.starts_with("how ")
|
||||||
|
|| (lower.starts_with("what ") && !lower.starts_with("what if "))
|
||||||
|
|| lower.starts_with("when ")
|
||||||
|
|| lower.starts_with("where ")
|
||||||
|
|| lower.starts_with("who ")
|
||||||
|
|| lower.starts_with("will ")
|
||||||
|
|| lower.starts_with("is ")
|
||||||
|
|| lower.starts_with("are ")
|
||||||
|
|| lower.starts_with("did ")
|
||||||
|
|| lower.starts_with("does ")
|
||||||
|
|| lower.ends_with('?')
|
||||||
|
{
|
||||||
|
return NoteType::Question;
|
||||||
|
}
|
||||||
|
NoteType::Note
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -490,7 +551,7 @@ mod tests {
|
||||||
let r = p("take a break in 30 minutes");
|
let r = p("take a break in 30 minutes");
|
||||||
let t = r.time.unwrap();
|
let t = r.time.unwrap();
|
||||||
let delta = (t - before).num_seconds();
|
let delta = (t - before).num_seconds();
|
||||||
assert!(delta >= 29 * 60 && delta <= 31 * 60, "delta was {}s", delta);
|
assert!((29 * 60..=31 * 60).contains(&delta), "delta was {}s", delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -498,7 +559,7 @@ mod tests {
|
||||||
let before = Utc::now();
|
let before = Utc::now();
|
||||||
let r = p("ping in 1 minute");
|
let r = p("ping in 1 minute");
|
||||||
let delta = (r.time.unwrap() - before).num_seconds();
|
let delta = (r.time.unwrap() - before).num_seconds();
|
||||||
assert!(delta >= 55 && delta <= 65, "delta was {}s", delta);
|
assert!((55..=65).contains(&delta), "delta was {}s", delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -506,7 +567,7 @@ mod tests {
|
||||||
let before = Utc::now();
|
let before = Utc::now();
|
||||||
let r = p("review PR in 2 hours");
|
let r = p("review PR in 2 hours");
|
||||||
let delta_min = (r.time.unwrap() - before).num_minutes();
|
let delta_min = (r.time.unwrap() - before).num_minutes();
|
||||||
assert!(delta_min >= 119 && delta_min <= 121, "delta was {}min", delta_min);
|
assert!((119..=121).contains(&delta_min), "delta was {}min", delta_min);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -514,7 +575,7 @@ mod tests {
|
||||||
let before = Utc::now();
|
let before = Utc::now();
|
||||||
let r = p("follow up in 3 days");
|
let r = p("follow up in 3 days");
|
||||||
let delta_h = (r.time.unwrap() - before).num_hours();
|
let delta_h = (r.time.unwrap() - before).num_hours();
|
||||||
assert!(delta_h >= 71 && delta_h <= 73, "delta was {}h", delta_h);
|
assert!((71..=73).contains(&delta_h), "delta was {}h", delta_h);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Time extraction: tomorrow ----
|
// ---- Time extraction: tomorrow ----
|
||||||
|
|
@ -717,7 +778,7 @@ mod tests {
|
||||||
let before = Utc::now();
|
let before = Utc::now();
|
||||||
let r = p("check on the server in an hour");
|
let r = p("check on the server in an hour");
|
||||||
let delta_min = (r.time.unwrap() - before).num_minutes();
|
let delta_min = (r.time.unwrap() - before).num_minutes();
|
||||||
assert!(delta_min >= 59 && delta_min <= 61, "delta was {}min", delta_min);
|
assert!((59..=61).contains(&delta_min), "delta was {}min", delta_min);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -737,7 +798,7 @@ mod tests {
|
||||||
let before = Utc::now();
|
let before = Utc::now();
|
||||||
let r = p("in a couple of hours remind me to check the oven");
|
let r = p("in a couple of hours remind me to check the oven");
|
||||||
let delta_min = (r.time.unwrap() - before).num_minutes();
|
let delta_min = (r.time.unwrap() - before).num_minutes();
|
||||||
assert!(delta_min >= 119 && delta_min <= 121, "delta was {}min", delta_min);
|
assert!((119..=121).contains(&delta_min), "delta was {}min", delta_min);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -758,7 +819,7 @@ mod tests {
|
||||||
let before = Utc::now();
|
let before = Utc::now();
|
||||||
let r = p("in half an hour submit the report");
|
let r = p("in half an hour submit the report");
|
||||||
let delta_min = (r.time.unwrap() - before).num_minutes();
|
let delta_min = (r.time.unwrap() - before).num_minutes();
|
||||||
assert!(delta_min >= 29 && delta_min <= 31, "delta was {}min", delta_min);
|
assert!((29..=31).contains(&delta_min), "delta was {}min", delta_min);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Tonight / this evening ----
|
// ---- Tonight / this evening ----
|
||||||
|
|
@ -847,64 +908,3 @@ mod tests {
|
||||||
assert!(rule.as_str().contains("BYHOUR=16"), "rule: {}", rule.as_str());
|
assert!(rule.as_str().contains("BYHOUR=16"), "rule: {}", rule.as_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn infer_type(text: &str, has_time: bool, has_rrule: bool) -> NoteType {
|
|
||||||
let lower = text.to_lowercase();
|
|
||||||
if has_rrule || has_time {
|
|
||||||
return NoteType::Reminder;
|
|
||||||
}
|
|
||||||
if lower.contains("buy ")
|
|
||||||
|| lower.contains("pick up")
|
|
||||||
|| lower.contains("clean ")
|
|
||||||
|| lower.starts_with("call ")
|
|
||||||
|| lower.starts_with("email ")
|
|
||||||
|| lower.starts_with("fix ")
|
|
||||||
|| lower.starts_with("check ")
|
|
||||||
|| lower.starts_with("finish ")
|
|
||||||
|| lower.starts_with("write ")
|
|
||||||
|| lower.starts_with("update ")
|
|
||||||
|| lower.starts_with("prepare ")
|
|
||||||
|| lower.starts_with("schedule ")
|
|
||||||
|| lower.starts_with("organize ")
|
|
||||||
|| lower.starts_with("deploy ")
|
|
||||||
|| lower.starts_with("install ")
|
|
||||||
|| lower.starts_with("send ")
|
|
||||||
|| lower.starts_with("submit ")
|
|
||||||
|| lower.starts_with("create ")
|
|
||||||
|| lower.starts_with("setup ")
|
|
||||||
|| lower.starts_with("restore ")
|
|
||||||
|| lower.starts_with("archive ")
|
|
||||||
|| lower.starts_with("export ")
|
|
||||||
|| lower.starts_with("import ")
|
|
||||||
|| lower.starts_with("approve ")
|
|
||||||
|| lower.starts_with("configure ")
|
|
||||||
|| lower.starts_with("refactor ")
|
|
||||||
|| lower.starts_with("review ")
|
|
||||||
{
|
|
||||||
return NoteType::Todo;
|
|
||||||
}
|
|
||||||
if lower.starts_with("what if ")
|
|
||||||
|| lower.starts_with("idea:")
|
|
||||||
|| lower.contains("could ")
|
|
||||||
|| lower.contains("maybe ")
|
|
||||||
|| lower.starts_with("should we ")
|
|
||||||
{
|
|
||||||
return NoteType::Idea;
|
|
||||||
}
|
|
||||||
if lower.starts_with("why ")
|
|
||||||
|| lower.starts_with("how ")
|
|
||||||
|| (lower.starts_with("what ") && !lower.starts_with("what if "))
|
|
||||||
|| lower.starts_with("when ")
|
|
||||||
|| lower.starts_with("where ")
|
|
||||||
|| lower.starts_with("who ")
|
|
||||||
|| lower.starts_with("will ")
|
|
||||||
|| lower.starts_with("is ")
|
|
||||||
|| lower.starts_with("are ")
|
|
||||||
|| lower.starts_with("did ")
|
|
||||||
|| lower.starts_with("does ")
|
|
||||||
|| lower.ends_with('?')
|
|
||||||
{
|
|
||||||
return NoteType::Question;
|
|
||||||
}
|
|
||||||
NoteType::Note
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ pub(crate) fn parse_next_from_rrule(rrule_str: &str, default_morning: &str) -> O
|
||||||
} else {
|
} else {
|
||||||
(now.date_naive() + chrono::Duration::days(1)).and_time(fire_time)
|
(now.date_naive() + chrono::Duration::days(1)).and_time(fire_time)
|
||||||
};
|
};
|
||||||
return Some(local_naive_to_utc(naive));
|
Some(local_naive_to_utc(naive))
|
||||||
}
|
}
|
||||||
"WEEKLY" => {
|
"WEEKLY" => {
|
||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
|
|
@ -204,7 +204,7 @@ pub(crate) fn parse_next_from_rrule(rrule_str: &str, default_morning: &str) -> O
|
||||||
};
|
};
|
||||||
let target_date =
|
let target_date =
|
||||||
(now.date_naive() + chrono::Duration::days(days_ahead)).and_time(fire_time);
|
(now.date_naive() + chrono::Duration::days(days_ahead)).and_time(fire_time);
|
||||||
return Some(local_naive_to_utc(target_date));
|
Some(local_naive_to_utc(target_date))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,11 +118,11 @@ impl Store {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rewrite_notes<F>(&self, mut f: F) -> Result<()>
|
fn rewrite_notes<F>(&self, f: F) -> Result<()>
|
||||||
where
|
where
|
||||||
F: FnMut(Note) -> Note,
|
F: FnMut(Note) -> Note,
|
||||||
{
|
{
|
||||||
let notes: Vec<Note> = self.load_all()?.into_iter().map(|n| f(n)).collect();
|
let notes: Vec<Note> = self.load_all()?.into_iter().map(f).collect();
|
||||||
self.write_all(&self.notes_path, ¬es)
|
self.write_all(&self.notes_path, ¬es)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +145,7 @@ impl Store {
|
||||||
let notes = self.load_all()?;
|
let notes = self.load_all()?;
|
||||||
let (to_archive, keep): (Vec<Note>, Vec<Note>) = notes
|
let (to_archive, keep): (Vec<Note>, Vec<Note>) = notes
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.partition(|n| n.done && n.completed.map_or(false, |c| c < cutoff));
|
.partition(|n| n.done && n.completed.is_some_and(|c| c < cutoff));
|
||||||
|
|
||||||
if to_archive.is_empty() {
|
if to_archive.is_empty() {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn css_defines_bg_color() {
|
fn css_defines_bg_color() {
|
||||||
let css = build_css(&Palette::default(), None);
|
let css = build_css(&Palette::default(), None);
|
||||||
assert!(css.contains("@define-color bg #1e1e2e"), "css missing bg: {}", &css[..300]);
|
assert!(css.contains("@define-color bg #0c0c0c"), "css missing bg: {}", &css[..300]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -323,9 +323,11 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn css_reflects_custom_palette_colors() {
|
fn css_reflects_custom_palette_colors() {
|
||||||
let mut p = Palette::default();
|
let p = Palette {
|
||||||
p.background = "#deadbe".into();
|
background: "#deadbe".into(),
|
||||||
p.color4 = "#cafe00".into();
|
color4: "#cafe00".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
let css = build_css(&p, None);
|
let css = build_css(&p, None);
|
||||||
assert!(css.contains("@define-color bg #deadbe"), "css: {}", &css[..300]);
|
assert!(css.contains("@define-color bg #deadbe"), "css: {}", &css[..300]);
|
||||||
assert!(css.contains("@define-color blue #cafe00"), "css: {}", &css[..300]);
|
assert!(css.contains("@define-color blue #cafe00"), "css: {}", &css[..300]);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ pub enum NoteType {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NoteType {
|
impl NoteType {
|
||||||
|
// Not std::str::FromStr — infallible, returns Self directly rather than
|
||||||
|
// Result, and used across 30+ call sites as NoteType::from_str(..).
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
pub fn from_str(s: &str) -> Self {
|
pub fn from_str(s: &str) -> Self {
|
||||||
match s.to_lowercase().as_str() {
|
match s.to_lowercase().as_str() {
|
||||||
"todo" => NoteType::Todo,
|
"todo" => NoteType::Todo,
|
||||||
|
|
|
||||||
|
|
@ -279,15 +279,19 @@ fn resolved_ort_dylib_empty_returns_none() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolved_ort_dylib_whitespace_only_returns_none() {
|
fn resolved_ort_dylib_whitespace_only_returns_none() {
|
||||||
let mut m = ModelConfig::default();
|
let m = ModelConfig {
|
||||||
m.ort_dylib_path = " ".into();
|
ort_dylib_path: " ".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(m.resolved_ort_dylib_path().is_none());
|
assert!(m.resolved_ort_dylib_path().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolved_ort_dylib_set_returns_some() {
|
fn resolved_ort_dylib_set_returns_some() {
|
||||||
let mut m = ModelConfig::default();
|
let m = ModelConfig {
|
||||||
m.ort_dylib_path = "/usr/lib/libonnxruntime.so".into();
|
ort_dylib_path: "/usr/lib/libonnxruntime.so".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
m.resolved_ort_dylib_path().unwrap().to_str().unwrap(),
|
m.resolved_ort_dylib_path().unwrap().to_str().unwrap(),
|
||||||
"/usr/lib/libonnxruntime.so"
|
"/usr/lib/libonnxruntime.so"
|
||||||
|
|
|
||||||
|
|
@ -389,7 +389,7 @@ fn cmd_show(index: usize, corpus_path: &Path, tier: &TierArg) -> Result<()> {
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
let sep = "─".repeat(62);
|
let sep = "─".repeat(62);
|
||||||
println!("{:<14} {:<26} {}", "field", "expected", "actual");
|
println!("{:<14} {:<26} actual", "field", "expected");
|
||||||
println!("{sep}");
|
println!("{sep}");
|
||||||
println!(
|
println!(
|
||||||
"{:<14} {:<26} {}",
|
"{:<14} {:<26} {}",
|
||||||
|
|
|
||||||
|
|
@ -468,7 +468,7 @@ fn build_reminder_window(
|
||||||
let local: chrono::DateTime<chrono::Local> = t.into();
|
let local: chrono::DateTime<chrono::Local> = t.into();
|
||||||
header.append(
|
header.append(
|
||||||
>k4::Label::builder()
|
>k4::Label::builder()
|
||||||
.label(&local.format("%H:%M").to_string())
|
.label(local.format("%H:%M").to_string())
|
||||||
.css_classes(["reminder-time"])
|
.css_classes(["reminder-time"])
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
22
ci/Containerfile
Normal file
22
ci/Containerfile
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Pinned CI build environment for breadpad.
|
||||||
|
#
|
||||||
|
# Arch instead of Fedora because breadpad targets BOS/Arch only, and Arch's
|
||||||
|
# repos already carry current libadwaita/gtk4/gtk4-layer-shell as prebuilt
|
||||||
|
# packages — no from-source libadwaita build needed (that from-source build
|
||||||
|
# was the repeated CI breakage: rust dep mismatches, libadwaita ABI
|
||||||
|
# mismatches, and finally a removed meson option).
|
||||||
|
#
|
||||||
|
# Base image pinned by digest, package set frozen at build time: this image
|
||||||
|
# only changes when someone deliberately rebuilds it, not on every push.
|
||||||
|
FROM archlinux@sha256:fae033b815a16f930325c2697e620362be4d2e5d739a301b10ad1fc9c8643a06
|
||||||
|
|
||||||
|
RUN pacman -Syu --noconfirm --needed \
|
||||||
|
base-devel \
|
||||||
|
git \
|
||||||
|
pkgconf \
|
||||||
|
rust \
|
||||||
|
gtk4 \
|
||||||
|
libadwaita \
|
||||||
|
gtk4-layer-shell \
|
||||||
|
graphene \
|
||||||
|
&& pacman -Scc --noconfirm
|
||||||
31
ci/build.sh
Executable file
31
ci/build.sh
Executable file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Builds (or reuses, via docker's own layer cache) the pinned Arch CI image
|
||||||
|
# from ci/Containerfile, then runs the given cargo command inside it against
|
||||||
|
# this repo checkout.
|
||||||
|
#
|
||||||
|
# Cargo's registry/git caches and CARGO_TARGET_DIR are persisted in named
|
||||||
|
# docker volumes so they survive across runs even though the repo checkout
|
||||||
|
# itself (a fresh --depth 1 clone per workflow run) does not.
|
||||||
|
#
|
||||||
|
# Usage: ci/build.sh cargo build --release --locked
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
docker build -t breadpad-ci:archlinux -f ci/Containerfile ci
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "${ROOT}:/workspace" \
|
||||||
|
-v breadpad-cargo-registry:/root/.cargo/registry \
|
||||||
|
-v breadpad-cargo-git:/root/.cargo/git \
|
||||||
|
-v breadpad-cargo-target:/cargo-target \
|
||||||
|
-w /workspace \
|
||||||
|
-e CARGO_TARGET_DIR=/cargo-target \
|
||||||
|
breadpad-ci:archlinux \
|
||||||
|
bash -c '
|
||||||
|
set -euo pipefail
|
||||||
|
"$@"
|
||||||
|
mkdir -p /workspace/target
|
||||||
|
cp -a /cargo-target/. /workspace/target/
|
||||||
|
' bash "$@"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue