Prepare repo for GitHub publication
- Add MIT LICENSE file - Expand .gitignore with standard Rust/Linux entries - Remove dangling symlinks (breadmancli, breadpadcli) and dev scratchpad (svgs.txt) from git tracking - Replace unsafe unwrap() calls with expect() in breadman CLI (guarded by prior filter) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
feefdb81b9
commit
347508828f
34 changed files with 2825 additions and 771 deletions
|
|
@ -20,3 +20,4 @@ tokio.workspace = true
|
|||
chrono.workspace = true
|
||||
gtk4.workspace = true
|
||||
dirs.workspace = true
|
||||
futures-channel = "0.3"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use breadpad_shared::{
|
||||
parser::parse_rule_based,
|
||||
scheduler::Scheduler,
|
||||
store::Store,
|
||||
types::{Note, NoteType, RecurrenceRule},
|
||||
};
|
||||
use chrono::{Local, TimeZone, Utc};
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{glib, prelude::*};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -13,8 +14,9 @@ pub fn build_editor_popover(
|
|||
note: &Note,
|
||||
store: Arc<Store>,
|
||||
morning: String,
|
||||
on_save: impl Fn(Note) + 'static,
|
||||
on_delete: impl Fn() + 'static,
|
||||
on_save: Rc<dyn Fn(Note)>,
|
||||
on_delete: Rc<dyn Fn()>,
|
||||
on_error: Rc<dyn Fn(String)>,
|
||||
) -> gtk4::Popover {
|
||||
let popover = gtk4::Popover::new();
|
||||
popover.set_has_arrow(false);
|
||||
|
|
@ -86,7 +88,7 @@ pub fn build_editor_popover(
|
|||
btn_row.append(&save_btn);
|
||||
vbox.append(&btn_row);
|
||||
|
||||
// Delete: two-click confirm using a single handler and shared state
|
||||
// Delete: two-click confirm
|
||||
let confirming = Rc::new(RefCell::new(false));
|
||||
{
|
||||
let confirming = confirming.clone();
|
||||
|
|
@ -94,16 +96,32 @@ pub fn build_editor_popover(
|
|||
let note_id = note.id.clone();
|
||||
let store_del = store.clone();
|
||||
let popover_del = popover.clone();
|
||||
let on_delete = Rc::clone(&on_delete);
|
||||
let on_error = Rc::clone(&on_error);
|
||||
|
||||
delete_btn.connect_clicked(move |_| {
|
||||
let currently = *confirming.borrow();
|
||||
if currently {
|
||||
if let Err(e) = store_del.delete_note(¬e_id) {
|
||||
tracing::error!("failed to delete note: {}", e);
|
||||
} else {
|
||||
on_delete();
|
||||
}
|
||||
popover_del.popdown();
|
||||
if *confirming.borrow() {
|
||||
let store = store_del.clone();
|
||||
let id = note_id.clone();
|
||||
let on_delete = Rc::clone(&on_delete);
|
||||
let on_error = Rc::clone(&on_error);
|
||||
let popover = popover_del.clone();
|
||||
spawn_bg(
|
||||
move || -> anyhow::Result<()> {
|
||||
store.delete_note(&id)?;
|
||||
if let Err(e) = Scheduler::cancel(&id) {
|
||||
tracing::warn!("failed to cancel timer for {}: {}", id, e);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
move |result| {
|
||||
match result {
|
||||
Ok(()) => on_delete(),
|
||||
Err(e) => on_error(format!("delete failed: {}", e)),
|
||||
}
|
||||
popover.popdown();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
*confirming.borrow_mut() = true;
|
||||
delete_btn_label.set_label("Sure?");
|
||||
|
|
@ -112,46 +130,77 @@ pub fn build_editor_popover(
|
|||
}
|
||||
|
||||
// Save
|
||||
let note_clone = note.clone();
|
||||
let popover_save = popover.clone();
|
||||
{
|
||||
let note_clone = note.clone();
|
||||
let popover_save = popover.clone();
|
||||
let on_error = Rc::clone(&on_error);
|
||||
|
||||
save_btn.connect_clicked(move |_| {
|
||||
let mut updated = note_clone.clone();
|
||||
updated.body = body_entry.text().to_string();
|
||||
save_btn.connect_clicked(move |_| {
|
||||
// Read all field values on the main thread before handing off.
|
||||
let mut updated = note_clone.clone();
|
||||
updated.body = body_entry.text().to_string();
|
||||
updated.note_type = NoteType::from_str(
|
||||
NoteType::all_builtin()
|
||||
.get(type_combo.selected() as usize)
|
||||
.copied()
|
||||
.unwrap_or("note"),
|
||||
);
|
||||
let time_str = time_entry.text().to_string();
|
||||
updated.time = if time_str.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
parse_time_field(&time_str, &morning)
|
||||
};
|
||||
let rrule_text = rrule_entry.text().to_string();
|
||||
updated.rrule = if rrule_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(RecurrenceRule::new(rrule_text))
|
||||
};
|
||||
|
||||
updated.note_type = NoteType::from_str(
|
||||
NoteType::all_builtin()
|
||||
.get(type_combo.selected() as usize)
|
||||
.copied()
|
||||
.unwrap_or("note"),
|
||||
);
|
||||
popover_save.popdown();
|
||||
|
||||
let time_str = time_entry.text().to_string();
|
||||
updated.time = if time_str.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
parse_time_field(&time_str, &morning)
|
||||
};
|
||||
|
||||
let rrule_text = rrule_entry.text().to_string();
|
||||
updated.rrule = if rrule_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(RecurrenceRule::new(rrule_text))
|
||||
};
|
||||
|
||||
if let Err(e) = store.update_note(&updated) {
|
||||
tracing::error!("failed to update note: {}", e);
|
||||
} else {
|
||||
on_save(updated);
|
||||
}
|
||||
popover_save.popdown();
|
||||
});
|
||||
let store_bg = store.clone();
|
||||
let on_save = Rc::clone(&on_save);
|
||||
let on_error = Rc::clone(&on_error);
|
||||
spawn_bg(
|
||||
move || -> anyhow::Result<Note> {
|
||||
store_bg.update_note(&updated)?;
|
||||
if let Err(e) = Scheduler::cancel(&updated.id) {
|
||||
tracing::warn!("cancel before reschedule: {}", e);
|
||||
}
|
||||
if updated.time.is_some() || updated.rrule.is_some() {
|
||||
Scheduler::schedule(&updated)?;
|
||||
}
|
||||
Ok(updated)
|
||||
},
|
||||
move |result| match result {
|
||||
Ok(note) => on_save(note),
|
||||
Err(e) => on_error(format!("update failed: {}", e)),
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
popover.set_child(Some(&vbox));
|
||||
popover
|
||||
}
|
||||
|
||||
fn spawn_bg<F, T, C>(work: F, then: C)
|
||||
where
|
||||
F: FnOnce() -> T + Send + 'static,
|
||||
T: Send + 'static,
|
||||
C: FnOnce(T) + 'static,
|
||||
{
|
||||
let (tx, rx) = futures_channel::oneshot::channel::<T>();
|
||||
std::thread::spawn(move || { let _ = tx.send(work()); });
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
if let Ok(result) = rx.await {
|
||||
then(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_time_field(s: &str, morning: &str) -> Option<chrono::DateTime<Utc>> {
|
||||
if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(s.trim(), "%Y-%m-%d %H:%M") {
|
||||
if let chrono::LocalResult::Single(local) = Local.from_local_datetime(&naive) {
|
||||
|
|
|
|||
|
|
@ -107,6 +107,30 @@ impl AppState {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Background I/O helper ─────────────────────────────────────────────────────
|
||||
|
||||
/// Run `work` on a background thread, then call `then` on the GTK main thread.
|
||||
///
|
||||
/// `work` must be `Send + 'static` (moves into the thread).
|
||||
/// `then` only needs `'static` — it can capture GTK widgets and `Rc<RefCell<...>>`.
|
||||
///
|
||||
/// Uses `glib::MainContext::spawn_local` (called from the main thread) with a
|
||||
/// `futures_channel::oneshot` to bridge the blocking result back to the async future.
|
||||
fn spawn_bg<F, T, C>(work: F, then: C)
|
||||
where
|
||||
F: FnOnce() -> T + Send + 'static,
|
||||
T: Send + 'static,
|
||||
C: FnOnce(T) + 'static,
|
||||
{
|
||||
let (tx, rx) = futures_channel::oneshot::channel::<T>();
|
||||
std::thread::spawn(move || { let _ = tx.send(work()); });
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
if let Ok(result) = rx.await {
|
||||
then(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Refresh ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn refresh(state: &AppState) {
|
||||
|
|
@ -116,6 +140,16 @@ fn refresh(state: &AppState) {
|
|||
state.stack.set_visible_child_name(&active);
|
||||
}
|
||||
|
||||
/// Replace only the "all" stack page with a new list built from `notes`.
|
||||
/// All other pages are left untouched, preserving scroll position etc.
|
||||
fn rebuild_all_view(notes: &[Note], state: &AppState) {
|
||||
if let Some(child) = state.stack.child_by_name("all") {
|
||||
state.stack.remove(&child);
|
||||
}
|
||||
let scroll = build_note_list(notes, state.clone());
|
||||
state.stack.add_named(&scroll, Some("all"));
|
||||
}
|
||||
|
||||
fn rebuild_stack(state: &AppState) {
|
||||
while let Some(child) = state.stack.first_child() {
|
||||
state.stack.remove(&child);
|
||||
|
|
@ -208,9 +242,9 @@ fn cmd_upcoming_plain() -> Result<()> {
|
|||
&& n.effective_time().is_some()
|
||||
})
|
||||
.collect();
|
||||
notes.sort_by_key(|n| n.effective_time().unwrap());
|
||||
notes.sort_by_key(|n| n.effective_time().expect("filtered by is_some above"));
|
||||
for note in ¬es {
|
||||
let t = note.effective_time().unwrap();
|
||||
let t = note.effective_time().expect("filtered by is_some above");
|
||||
let local: chrono::DateTime<Local> = t.into();
|
||||
println!("[{}] {} — {}", note.id, local.format("%a %b %d %H:%M"), note.body);
|
||||
}
|
||||
|
|
@ -272,10 +306,10 @@ fn build_app_window(
|
|||
let new_note_btn = gtk4::Button::builder()
|
||||
.label("✚ New Note")
|
||||
.css_classes(["confirm-button"])
|
||||
.margin_start(10)
|
||||
.margin_end(10)
|
||||
.margin_top(12)
|
||||
.margin_bottom(6)
|
||||
.margin_start(12)
|
||||
.margin_end(12)
|
||||
.margin_top(16)
|
||||
.margin_bottom(12)
|
||||
.build();
|
||||
sidebar_vbox.append(&new_note_btn);
|
||||
|
||||
|
|
@ -349,10 +383,10 @@ fn build_app_window(
|
|||
let search_entry = gtk4::SearchEntry::builder()
|
||||
.placeholder_text("Search notes…")
|
||||
.css_classes(["search-entry"])
|
||||
.margin_start(8)
|
||||
.margin_end(8)
|
||||
.margin_top(8)
|
||||
.margin_bottom(4)
|
||||
.margin_start(12)
|
||||
.margin_end(12)
|
||||
.margin_top(12)
|
||||
.margin_bottom(8)
|
||||
.build();
|
||||
|
||||
let stack = gtk4::Stack::builder().hexpand(true).vexpand(true).build();
|
||||
|
|
@ -401,36 +435,8 @@ fn build_app_window(
|
|||
.filter(|n| n.body.to_lowercase().contains(&q))
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Replace the "all" page with the filtered list while preserving others
|
||||
while let Some(child) = state_c.stack.first_child() {
|
||||
state_c.stack.remove(&child);
|
||||
}
|
||||
let all_scroll = build_note_list(&filtered, state_c.clone());
|
||||
state_c.stack.add_named(&all_scroll, Some("all"));
|
||||
|
||||
let notes_snap = state_c.notes.borrow().clone();
|
||||
let cfg_snap = state_c.cfg.borrow().clone();
|
||||
let errors_snap = state_c.errors.borrow().clone();
|
||||
|
||||
let upcoming = views::upcoming::build(¬es_snap);
|
||||
state_c.stack.add_named(&upcoming, Some("upcoming"));
|
||||
for type_name in NoteType::all_builtin() {
|
||||
let nt = NoteType::from_str(type_name);
|
||||
let typed: Vec<Note> = notes_snap
|
||||
.iter()
|
||||
.filter(|n| n.note_type == nt && !n.done)
|
||||
.cloned()
|
||||
.collect();
|
||||
state_c.stack.add_named(&build_note_list(&typed, state_c.clone()), Some(type_name));
|
||||
}
|
||||
state_c.stack.add_named(&views::archive::build(¬es_snap, state_c.clone()), Some("archive"));
|
||||
let state_s = state_c.clone();
|
||||
state_c.stack.add_named(
|
||||
&views::settings::build(&cfg_snap, move |nc| { *state_s.cfg.borrow_mut() = nc; }),
|
||||
Some("settings"),
|
||||
);
|
||||
state_c.stack.add_named(&views::errors::build(&errors_snap), Some("errors"));
|
||||
// Only replace the "all" page — other views keep their scroll position.
|
||||
rebuild_all_view(&filtered, &state_c);
|
||||
state_c.stack.set_visible_child_name("all");
|
||||
});
|
||||
}
|
||||
|
|
@ -475,9 +481,11 @@ fn build_note_list(notes: &[Note], state: AppState) -> gtk4::ScrolledWindow {
|
|||
|
||||
let list = gtk4::Box::builder()
|
||||
.orientation(gtk4::Orientation::Vertical)
|
||||
.spacing(4)
|
||||
.margin_top(8)
|
||||
.margin_bottom(8)
|
||||
.spacing(8)
|
||||
.margin_top(12)
|
||||
.margin_bottom(12)
|
||||
.margin_start(12)
|
||||
.margin_end(12)
|
||||
.build();
|
||||
|
||||
let mut sorted: Vec<Note> = notes.iter().filter(|n| !n.done).cloned().collect();
|
||||
|
|
@ -503,11 +511,11 @@ fn build_note_list(notes: &[Note], state: AppState) -> gtk4::ScrolledWindow {
|
|||
fn build_note_card(note: &Note, state: AppState) -> gtk4::Box {
|
||||
let card = gtk4::Box::builder()
|
||||
.orientation(gtk4::Orientation::Vertical)
|
||||
.spacing(4)
|
||||
.margin_start(8)
|
||||
.margin_end(8)
|
||||
.margin_top(4)
|
||||
.margin_bottom(4)
|
||||
.spacing(8)
|
||||
.margin_start(0)
|
||||
.margin_end(0)
|
||||
.margin_top(0)
|
||||
.margin_bottom(0)
|
||||
.css_classes(["note-card"])
|
||||
.build();
|
||||
card.add_css_class(&format!("note-card-{}", note.note_type.as_str()));
|
||||
|
|
@ -590,14 +598,30 @@ fn build_note_card(note: &Note, state: AppState) -> gtk4::Box {
|
|||
let card_c = card.clone();
|
||||
let state_c = state.clone();
|
||||
done_btn.connect_clicked(move |_| {
|
||||
if let Ok(Some(mut n)) = state_c.store.get_by_id(¬e_id) {
|
||||
n.mark_done();
|
||||
if let Err(e) = state_c.store.update_note(&n) {
|
||||
state_c.log_error(format!("mark done failed: {}", e));
|
||||
}
|
||||
}
|
||||
card_c.set_visible(false);
|
||||
state_c.reload_notes();
|
||||
card_c.set_visible(false); // optimistic hide
|
||||
let store = state_c.write_store();
|
||||
let id = note_id.clone();
|
||||
let state = state_c.clone();
|
||||
spawn_bg(
|
||||
move || -> anyhow::Result<Vec<Note>> {
|
||||
if let Some(mut n) = store.get_by_id(&id)? {
|
||||
n.mark_done();
|
||||
store.update_note(&n)?;
|
||||
}
|
||||
store.load_all()
|
||||
},
|
||||
move |result| {
|
||||
match result {
|
||||
Ok(fresh) => {
|
||||
*state.notes.borrow_mut() = fresh;
|
||||
rebuild_stack(&state);
|
||||
let active = state.active_view.borrow().clone();
|
||||
state.stack.set_visible_child_name(&active);
|
||||
}
|
||||
Err(e) => state.log_error(format!("mark done failed: {}", e)),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
bottom_row.append(&done_btn);
|
||||
|
|
@ -622,19 +646,29 @@ fn build_note_card(note: &Note, state: AppState) -> gtk4::Box {
|
|||
let body_label_save = body_label_c.clone();
|
||||
let state_del = state_c.clone();
|
||||
let card_del = card_c.clone();
|
||||
let state_err = state_c.clone();
|
||||
|
||||
let popover = editor::build_editor_popover(
|
||||
¬e_c,
|
||||
store,
|
||||
morning,
|
||||
move |updated: Note| {
|
||||
Rc::new(move |updated: Note| {
|
||||
body_label_save.set_label(&updated.body);
|
||||
state_save.reload_notes();
|
||||
},
|
||||
move || {
|
||||
rebuild_stack(&state_save);
|
||||
let active = state_save.active_view.borrow().clone();
|
||||
state_save.stack.set_visible_child_name(&active);
|
||||
}),
|
||||
Rc::new(move || {
|
||||
card_del.set_visible(false);
|
||||
state_del.reload_notes();
|
||||
},
|
||||
rebuild_stack(&state_del);
|
||||
let active = state_del.active_view.borrow().clone();
|
||||
state_del.stack.set_visible_child_name(&active);
|
||||
}),
|
||||
Rc::new(move |e: String| {
|
||||
state_err.log_error(e);
|
||||
}),
|
||||
);
|
||||
popover.set_parent(btn);
|
||||
popover.popup();
|
||||
|
|
@ -659,12 +693,30 @@ fn build_note_card(note: &Note, state: AppState) -> gtk4::Box {
|
|||
|
||||
delete_btn.connect_clicked(move |_| {
|
||||
if *confirming.borrow() {
|
||||
card_c.set_visible(false); // optimistic hide
|
||||
let store = state_c.write_store();
|
||||
if let Err(e) = store.delete_note(¬e_id) {
|
||||
state_c.log_error(format!("delete failed: {}", e));
|
||||
}
|
||||
card_c.set_visible(false);
|
||||
state_c.reload_notes();
|
||||
let id = note_id.clone();
|
||||
let state = state_c.clone();
|
||||
spawn_bg(
|
||||
move || -> anyhow::Result<Vec<Note>> {
|
||||
store.delete_note(&id)?;
|
||||
if let Err(e) = Scheduler::cancel(&id) {
|
||||
tracing::warn!("failed to cancel timer for {}: {}", id, e);
|
||||
}
|
||||
store.load_all()
|
||||
},
|
||||
move |result| {
|
||||
match result {
|
||||
Ok(fresh) => {
|
||||
*state.notes.borrow_mut() = fresh;
|
||||
rebuild_stack(&state);
|
||||
let active = state.active_view.borrow().clone();
|
||||
state.stack.set_visible_child_name(&active);
|
||||
}
|
||||
Err(e) => state.log_error(format!("delete failed: {}", e)),
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
*confirming.borrow_mut() = true;
|
||||
btn_c.set_label("Sure?");
|
||||
|
|
@ -779,108 +831,88 @@ fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState) {
|
|||
cancel_btn.connect_clicked(move |_| win_c.close());
|
||||
}
|
||||
|
||||
// Add Note
|
||||
{
|
||||
let win_c = win.clone();
|
||||
let state_c = state.clone();
|
||||
let body_c = body_entry.clone();
|
||||
let time_c = time_entry.clone();
|
||||
let rrule_c = rrule_entry.clone();
|
||||
let sel_c = selected_type.clone();
|
||||
let status_c = status_label.clone();
|
||||
// Shared add-note logic — called by both the button and the Enter key.
|
||||
let do_add: Rc<dyn Fn()> = Rc::new({
|
||||
let win = win.clone();
|
||||
let state = state.clone();
|
||||
let body_entry = body_entry.clone();
|
||||
let time_entry = time_entry.clone();
|
||||
let rrule_entry = rrule_entry.clone();
|
||||
let selected_type = selected_type.clone();
|
||||
let status_label = status_label.clone();
|
||||
|
||||
let do_add = move || {
|
||||
let body_text = body_c.text().to_string();
|
||||
move || {
|
||||
let body_text = body_entry.text().to_string();
|
||||
if body_text.trim().is_empty() {
|
||||
status_c.set_label("Body is required.");
|
||||
status_label.set_label("Body is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
let morning = state_c.cfg.borrow().reminders.default_morning.clone();
|
||||
|
||||
// Tier 1 classification on body
|
||||
let morning = state.cfg.borrow().reminders.default_morning.clone();
|
||||
let parsed = parse_rule_based(&body_text, &morning);
|
||||
|
||||
let user_type = sel_c.borrow().clone();
|
||||
let default_type = NoteType::from_str(&state_c.cfg.borrow().settings.default_type);
|
||||
let user_type = selected_type.borrow().clone();
|
||||
let default_type = NoteType::from_str(&state.cfg.borrow().settings.default_type);
|
||||
|
||||
let mut note = Note::new(parsed.body.clone(), user_type.clone(), None);
|
||||
// Use parsed type if user left it at the default
|
||||
if user_type == default_type {
|
||||
note.note_type = parsed.note_type;
|
||||
}
|
||||
note.time = parsed.time;
|
||||
note.rrule = parsed.rrule;
|
||||
|
||||
// Time field overrides
|
||||
let time_str = time_c.text().to_string();
|
||||
let time_str = time_entry.text().to_string();
|
||||
if !time_str.trim().is_empty() {
|
||||
let tp = parse_rule_based(&time_str, &morning);
|
||||
if tp.time.is_some() { note.time = tp.time; }
|
||||
if tp.rrule.is_some() { note.rrule = tp.rrule; }
|
||||
}
|
||||
|
||||
// RRULE field overrides
|
||||
let rrule_str = rrule_c.text().to_string();
|
||||
let rrule_str = rrule_entry.text().to_string();
|
||||
if !rrule_str.trim().is_empty() {
|
||||
note.rrule = Some(RecurrenceRule::new(rrule_str));
|
||||
}
|
||||
|
||||
let store = state_c.write_store();
|
||||
if let Err(e) = store.save_note(¬e) {
|
||||
state_c.log_error(format!("save failed: {}", e));
|
||||
return;
|
||||
}
|
||||
if note.time.is_some() {
|
||||
if let Err(e) = Scheduler::schedule(¬e) {
|
||||
state_c.log_error(format!("schedule failed: {}", e));
|
||||
}
|
||||
}
|
||||
let store = state.write_store();
|
||||
win.close();
|
||||
|
||||
win_c.close();
|
||||
// Defer refresh so the window close event is processed first
|
||||
let state_refresh = state_c.clone();
|
||||
glib::idle_add_local_once(move || refresh(&state_refresh));
|
||||
};
|
||||
let state_bg = state.clone();
|
||||
spawn_bg(
|
||||
move || -> anyhow::Result<Vec<Note>> {
|
||||
store.save_note(¬e)?;
|
||||
if note.time.is_some() || note.rrule.is_some() {
|
||||
if let Err(e) = Scheduler::cancel(¬e.id) {
|
||||
tracing::warn!("cancel before schedule: {}", e);
|
||||
}
|
||||
Scheduler::schedule(¬e)?;
|
||||
}
|
||||
store.load_all()
|
||||
},
|
||||
move |result| {
|
||||
match result {
|
||||
Ok(fresh) => {
|
||||
*state_bg.notes.borrow_mut() = fresh;
|
||||
rebuild_stack(&state_bg);
|
||||
let active = state_bg.active_view.borrow().clone();
|
||||
state_bg.stack.set_visible_child_name(&active);
|
||||
}
|
||||
Err(e) => state_bg.log_error(format!("save failed: {}", e)),
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
let do_add = Rc::clone(&do_add);
|
||||
add_btn.connect_clicked(move |_| do_add());
|
||||
}
|
||||
|
||||
// Also trigger add on Enter in body field
|
||||
{
|
||||
let win_c2 = win.clone();
|
||||
let state_c2 = state.clone();
|
||||
let body_c2 = body_entry.clone();
|
||||
let time_c2 = time_entry.clone();
|
||||
let rrule_c2 = rrule_entry.clone();
|
||||
let sel_c2 = selected_type.clone();
|
||||
|
||||
let do_add = Rc::clone(&do_add);
|
||||
let time_entry = time_entry.clone();
|
||||
let rrule_entry = rrule_entry.clone();
|
||||
body_entry.connect_activate(move |_| {
|
||||
// If time/rrule fields are empty, submit immediately
|
||||
if time_c2.text().is_empty() && rrule_c2.text().is_empty() {
|
||||
let body_text = body_c2.text().to_string();
|
||||
if body_text.trim().is_empty() { return; }
|
||||
let morning = state_c2.cfg.borrow().reminders.default_morning.clone();
|
||||
let parsed = parse_rule_based(&body_text, &morning);
|
||||
let user_type = sel_c2.borrow().clone();
|
||||
let default_type = NoteType::from_str(&state_c2.cfg.borrow().settings.default_type);
|
||||
let mut note = Note::new(parsed.body.clone(), user_type.clone(), None);
|
||||
if user_type == default_type { note.note_type = parsed.note_type; }
|
||||
note.time = parsed.time;
|
||||
note.rrule = parsed.rrule;
|
||||
let store = state_c2.write_store();
|
||||
if let Err(e) = store.save_note(¬e) {
|
||||
state_c2.log_error(format!("save failed: {}", e));
|
||||
return;
|
||||
}
|
||||
if note.time.is_some() {
|
||||
if let Err(e) = Scheduler::schedule(¬e) {
|
||||
state_c2.log_error(format!("schedule failed: {}", e));
|
||||
}
|
||||
}
|
||||
win_c2.close();
|
||||
let sr = state_c2.clone();
|
||||
glib::idle_add_local_once(move || refresh(&sr));
|
||||
if time_entry.text().is_empty() && rrule_entry.text().is_empty() {
|
||||
do_add();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -898,8 +930,12 @@ fn apply_css(_cfg: &Config) {
|
|||
|
||||
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(
|
||||
>k4::gdk::Display::default().unwrap(),
|
||||
&display,
|
||||
&provider,
|
||||
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -79,14 +79,11 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled
|
|||
.build();
|
||||
attach_row(&model_grid, 1, "Tokenizer path", &tokenizer_entry);
|
||||
|
||||
let ep_options = ["auto", "npu", "vulkan", "cpu"];
|
||||
let ep_combo = gtk4::DropDown::from_strings(&ep_options);
|
||||
let ep_idx = ep_options
|
||||
.iter()
|
||||
.position(|&s| s == cfg.model.execution_provider.as_str())
|
||||
.unwrap_or(0) as u32;
|
||||
ep_combo.set_selected(ep_idx);
|
||||
attach_row(&model_grid, 2, "Execution provider", &ep_combo);
|
||||
let ort_dylib_entry = gtk4::Entry::builder()
|
||||
.text(&cfg.model.ort_dylib_path)
|
||||
.hexpand(true)
|
||||
.build();
|
||||
attach_row(&model_grid, 2, "ORT dylib path", &ort_dylib_entry);
|
||||
|
||||
outer.append(&model_frame);
|
||||
|
||||
|
|
@ -168,7 +165,7 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled
|
|||
let grs = grace_spin.clone();
|
||||
let mpe = model_path_entry.clone();
|
||||
let tke = tokenizer_entry.clone();
|
||||
let epc = ep_combo.clone();
|
||||
let ode = ort_dylib_entry.clone();
|
||||
let oec = ollama_enabled.clone();
|
||||
let oee = ollama_endpoint.clone();
|
||||
let ome = ollama_model.clone();
|
||||
|
|
@ -203,11 +200,7 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled
|
|||
model: ModelConfig {
|
||||
path: mpe.text().to_string(),
|
||||
tokenizer: tke.text().to_string(),
|
||||
execution_provider: ep_options
|
||||
.get(epc.selected() as usize)
|
||||
.copied()
|
||||
.unwrap_or("auto")
|
||||
.to_string(),
|
||||
ort_dylib_path: ode.text().to_string(),
|
||||
ollama: OllamaConfig {
|
||||
enabled: oec.is_active(),
|
||||
endpoint: oee.text().to_string(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue