1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
use std::path::PathBuf;
use std::time::Duration;
use crossterm::event::{self, Event, KeyEvent, MouseEvent};
use tokio::sync::mpsc;
use crate::hydrate::{AmbiguousMatch, HydrationProgress};
use crate::store::Store;
/// Payload of a successful gist-import fetch: the file's markdown content,
/// the gist's filename (prefills the save-as prompt), and the ready-made
/// store entry (import means local == remote, i.e. born synced).
#[derive(Debug)]
pub struct GistImportData {
pub content: String,
pub filename: String,
pub entry: crate::store::FileEntry,
}
#[derive(Debug)]
pub struct HydrationDoneData {
pub matched: usize,
pub ambiguous: Vec<AmbiguousMatch>,
pub store: Box<Store>,
/// The root this walk covered - the per-root hydration cursor is keyed by it.
pub root: PathBuf,
/// The timestamp to record as `root`'s new hydration cursor - captured
/// when the walk began, so gists created mid-walk are caught on the next
/// pass rather than skipped.
pub new_cursor: chrono::DateTime<chrono::Utc>,
}
/// Messages from async tasks back to the main UI loop.
#[derive(Debug)]
pub enum AsyncEvent {
/// Gist push completed for a file
PushDone {
root: PathBuf,
rel_path: String,
result: std::result::Result<crate::store::FileEntry, String>,
},
/// Gist pull completed
PullDone {
root: PathBuf,
rel_path: String,
/// SHA-256 of the local file when the pull was initiated. If the
/// on-disk content no longer matches by the time this event lands
/// (user edited mid-pull), the write is refused.
expected_local_sha256: String,
result: std::result::Result<(String, crate::store::FileEntry), String>,
},
/// Push refused because the remote changed since the last sync.
/// Carries the freshly observed remote state so the store can record
/// the divergence and the UI can offer a force-push.
PushBlocked {
root: PathBuf,
rel_path: String,
remote_sha256: String,
remote_updated_at: chrono::DateTime<chrono::Utc>,
},
/// Progress for a bulk remote check (`f`).
/// Bulk remote check finished. `started` timestamps the check so stale
/// results don't clobber entries that synced while it ran.
RemoteCheckDone {
root: PathBuf,
started: chrono::DateTime<chrono::Utc>,
result: std::result::Result<crate::remote::RemoteCheckOutcome, String>,
},
/// Hydration progress update
HydrationUpdate(HydrationProgress),
/// Hydration finished. On success, carries the (partial) updated store
/// so the main thread can merge in the discovered mappings without
/// clobbering concurrent writes, plus any ambiguous matches that need
/// user resolution.
HydrationDone(std::result::Result<HydrationDoneData, String>),
/// Remote status check result (from the diff view's fetch). Carries the
/// observed remote state so it can be written back to the store - a
/// diff is also a remote check.
StatusCheck {
root: PathBuf,
rel_path: String,
/// When the fetch began; write-back is skipped if the entry synced
/// after this (the sync result is the newer truth).
started: chrono::DateTime<chrono::Utc>,
result: std::result::Result<crate::sync::FullStatus, String>,
},
/// Google Doc fetch result
GdocFetched(std::result::Result<String, String>),
/// Gist import fetch result: content plus the mapping to record once the
/// file is saved locally.
GistImportFetched(std::result::Result<GistImportData, String>),
/// Remote gist deletion completed
DeleteDone {
root: PathBuf,
rel_path: String,
result: std::result::Result<(), String>,
},
/// Manual link (`L`) completed: a gist was fetched and reconciled against
/// the selected local file. On success carries the store entry to record.
LinkDone {
root: PathBuf,
rel_path: String,
result: std::result::Result<crate::store::FileEntry, String>,
},
/// Remote gist filename update (companion to a local rename) completed.
/// `rel_path` here is the new rel_path (for the status message);
/// the local store + filesystem are already updated by the time this
/// event fires, so we don't need the root.
RenameRemoteDone {
rel_path: String,
result: std::result::Result<(), String>,
},
}
/// User-input events from the terminal that the app cares about.
#[derive(Debug)]
pub enum UiEvent {
Key(KeyEvent),
Mouse(MouseEvent),
}
/// Poll for a crossterm UI event (key or mouse) with the given timeout.
pub fn poll_event(timeout: Duration) -> Option<UiEvent> {
if !event::poll(timeout).ok()? {
return None;
}
match event::read().ok()? {
Event::Key(k) => Some(UiEvent::Key(k)),
Event::Mouse(m) => Some(UiEvent::Mouse(m)),
_ => None,
}
}
pub type AsyncSender = mpsc::UnboundedSender<AsyncEvent>;
pub type AsyncReceiver = mpsc::UnboundedReceiver<AsyncEvent>;
pub fn async_channel() -> (AsyncSender, AsyncReceiver) {
mpsc::unbounded_channel()
}