LPSUBGUBMG2QHJJSAWQ35SZIMUR62R6ODPWBS7TSNNXJ5UJCNABQC 6YZAVBWU6E5FYOI5JGEIPXGZLIKAW6LS2AOFIQWEE5DMOPPCD5PQC KLR5FRIBS6UOH3S3XAOE22TJACVSVOY7TOLW22DIWNGY27S6WZRAC IQDCHWCP47LL46EXQLQGHQPGFYIHQLMQBHA57RWJCIOX5UEUIQAQC SWWE2R6MVBX5CNM6X3WLXZTSRTU53PBJL7WJSFVF77XBPXDX4COAC YBJRDOTCX3ZRDB5EVXJBR55FX3CADCSIGMYWNYVC2PD5W3GXR3DQC A5YBC77VWH2LXCZJOPZORQJI5ZYABSCHJWVX5HVNWPM5RABXESLQC OQ6HSAWHIRTAIIWMDGCTIOK47JDY7QVVAHLRDA2R5TTJKNSBFCWQC WI2BVQ6JOJBM4OC5KSZBMTDPBWESIR7GD72B5TLO7H2SY7QBDHJAC NWJD6VM6POMYKQTTPP3X6LVCWU3FHLDRIHMCSC2PPUT7JWNY42LAC CALXOZXANFZ64NBZBTR2KYTZ6ZLLCJXNFAEALBB2EYAVDVJJ6X6AC 23SFYK4Q5NKBPJG53PQNPWQH6UOUU2YKJEL7RLXYBRLJOJYV7AWQC XSZZB47UXR6KGYFZZQFQR63X2LDKOH6TPNNBRRGHUCI5JJ4JIWVAC WGID4LS4EISIOXB5Y5SOFGEF5PLBJSCPFCETH2CGRTFN3NC4WGJQC VCNKFNUF7OWVSWC6I5D25KUZ3XZZICZ3LHWVPF2N5ZSP7LQ2JOUQC ACDXXAX26ZJJFKJDGRC2GOSJY5JHQWCSTP55SYI6D6LH5UIRYUBAC UF5NJKASGMZSZMBUKSUI67B2GIMQFX5SNNQEHHGUBNDBQ2QZZWSAC I2AG42PAVOII4V4TWDJV5ZVNDIHKBRDT254BFQLFUIY723TW6CCQC DXAYDIMQ7BYEI3ASOHKADQWSMVJOA2ZVNEL2TDENJCJKX2U4GMWAC SASAN2XCWDQ2VEHZ7TAQEN2R3Y7AG7JUGEFVRL4DZAGHXDFEZFRQC KWTBNTO3QUUE2YADF6SYW6G6ZOKYEWRJQKIWDGZXR33S3YNDVIZQC OJPGHVC3RFBQ7TTSCZH6URSSATII3TESD74EISDNOTNXXSX7PQMAC FJSVMFB4FRZV6VXQTQ3FWY3GRHSM5RLYNCZ67JIDN254CVY7QFOQC WAOGSCOJ5A372BZKHEYD2BCDBCENNVLFYW3INKUOOAZMDADDIFIQC EJPSD5XO43DWUBBZGNQMY4TMCAXL5EWCGX3OEHUERQ5GRASGWQLQC YK3MOJJLRYEKZ4FUCNJ3YKMTKOINWIYOJKR3ER7IRSGTC7O6FJZQC 7WCB5YQJJZIPUAFHTCQBWNI6ZM5XMIQJAKTLYTR7NOR5NKESRMDQC IFQPVMBD552DZ3B5HCM6W6MI2SB6576ZYJNU5KVA3O4YPZAUEFHAC LFEMJYYDO45ASMQSOJ3TNID7B5UZXDHB3NWFZJXWOAWNBS6GMDEAC UPWS6J3BIHQKXSSWHD7CFLJOXWT3MRABFRVQ4T4NRYFALBAKJOOQC YGZ3VCW4OAJYPI2CYK3MTABNFY7Y2ENSSTFE5ZZ4K6HK57FCU3XQC ODCT4QJNJLQTDNFPIF7HX4XCFTEXZBESG3PTD276O7TWB7MSGWMQC UTDTZCTXAAP6AHENYQP7MOQ5QNIKKXN34NV2ONWEGM4HA4FU637AC XQTT6NDF6KRXHUABU45HB2TDSQQXYUF7S66PGMM7TVKE3OFBTVAQC F6O6FGOJ762C5CFA4R5K4BTUHTERHT2AOWGQS4S3QYUI7QU6N4IQC TEDT26JQBWGATVTY6HZTIOGFR6BXW2BHSUKUTXTA7HOXARRQ5D6AC YTOYRQ2MPUW3FJK64HPML5KQIB66G6CJJFQ4BGOAHT4MSAQAKFIAC YRGDFHABL6BRX55ZWIBGXX3ZX2R4WUV4BELP7JMW5AZX54P5BBIQC //! App directories shared between all Flowers apps.use directories::ProjectDirs;use std::path::PathBuf;const QUALIFIER: &str = "com";const ORGANIZATION: &str = "Holonyte";const APPLICATION: &str = "Flowers";/// App config dirpub fn config() -> PathBuf {projects_dir().config_dir().to_path_buf()}/// App config dirpub fn data() -> PathBuf {projects_dir().config_dir().to_path_buf()}fn projects_dir() -> ProjectDirs {ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION).unwrap()}
use crate::prelude::*;use super::dir;use async_fd_lock::{LockError, LockRead, LockWrite};use std::collections::BTreeMap;use std::path::{Path, PathBuf};use tokio::fs::File;use tokio::io::{AsyncReadExt, AsyncWriteExt};pub type Repos = BTreeMap<String, Repo>;#[derive(Debug, Serialize, Deserialize)]pub struct Repo {path: PathBuf,}#[derive(Debug, Error)]#[error("Another file-lock is blocking")]pub struct BlockingLockError;/// Acquire a read-lock to try to read [`Repos`]. If there's an active/// write-lock the call will fail with a [`BlockingLockError`].pub async fn read_repos() -> Result<Repos, ReadErr> {let file = match File::options().create_new(false).open(repos_path()).await{Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => {// Create a default `Repos` if file not foundreturn Ok(Repos::default());}result => result.map_err(OneOf::new)?,};let mut read_guard = match file.lock_read().await {Err(err)if matches!(err.error.kind(), std::io::ErrorKind::WouldBlock) =>{return Err(OneOf::new(BlockingLockError));}result => result.map_err(OneOf::new)?,};let mut buffer = Vec::new();read_guard.read_to_end(&mut buffer).await.map_err(OneOf::new)?;toml_edit::de::from_slice(&buffer).map_err(OneOf::new)}/// Acquire a write-lock to try to read [`Repos`]. If there's another active/// write-lock the call will fail with a [`BlockingLockError`].pub async fn write_repos(repos: &Repos) -> Result<(), WriteErr> {let file = File::options().create_new(true).open(repos_path()).await.map_err(OneOf::new)?;// Optimistically encode first to reduce the scope of the locklet repos_bytes =toml_edit::ser::to_string_pretty(repos).map_err(OneOf::new)?;let mut write_guard = match file.lock_write().await {Err(err)if matches!(err.error.kind(), std::io::ErrorKind::WouldBlock) =>{return Err(OneOf::new(BlockingLockError));}result => result.map_err(OneOf::new)?,};write_guard.write_all(repos_bytes.as_bytes()).await.map_err(OneOf::new)}pub type ReadErr = OneOf<(BlockingLockError,std::io::Error,LockError<tokio::fs::File>,toml_edit::de::Error,)>;pub type WriteErr = OneOf<(BlockingLockError,std::io::Error,LockError<tokio::fs::File>,toml_edit::ser::Error,)>;fn repos_path() -> PathBuf {PathBuf::from_iter([dir::data().as_path(), Path::new(REPOS_FILE)])}const REPOS_FILE: &str = "repos.toml";/// All the known local repositories// TODO: order by most recently opened
#[cfg(test)]mod test;use crate::prelude::*;use super::dir;use anyhow::{format_err, Context};use async_fd_lock::{LockRead, LockWrite};use tokio::fs::{self, File};use tokio::io::{AsyncReadExt, AsyncWriteExt};use std::cmp;use std::collections::{BTreeMap, BTreeSet};use std::path::{Path, PathBuf};/// All the known local repositoriespub type Projects = BTreeSet<Project>;#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,)]pub struct Project {// NOTE: This field must be first to order by most recently closed time// descendingpub last_closed_time: cmp::Reverse<Option<Timestamp>>,pub path: PathBuf,}#[derive(Debug, Error)]#[error("Another file-lock is blocking")]pub struct BlockingLockError;/// Acquire a read-lock to try to read [`Projects`]. If there's an active/// write-lock the call will fail.pub async fn read_projects() -> Result<Projects, ProjectsFileErr> {let path = projects_path();let path_str = path.to_string_lossy();let file = match File::options().create(false).read(true).open(&path).await{Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => {// Create a default if file not foundreturn Ok(Projects::default());}result => result.with_context(|| format!("Opening projects file ({path_str})")).map_err(OneOf::new)?,};let mut read_guard = match file.try_lock_read().await {Err(err)if matches!(err.error.kind(), std::io::ErrorKind::WouldBlock) =>{return Err(OneOf::new(BlockingLockError));}result => result.map_err(|e| {format_err!("Obtaining projects file read-lock failed with {e:?} ({path_str})")}).map_err(OneOf::new)?,};let mut buffer = Vec::new();read_guard.read_to_end(&mut buffer).await.with_context(|| format!("Reading projects file ({path_str})")).map_err(OneOf::new)?;let stored: ProjectsStored = toml_edit::de::from_slice(&buffer).with_context(|| format!("Decoding projects file ({path_str})")).map_err(OneOf::new)?;Ok(from_stored(stored))}pub async fn upsert_project(project: Project) -> Result<(), ProjectsFileErr> {with_projects(move |mut projects| {let Project {last_closed_time,path,} = project;let last_closed_time = if let Some(ProjectStored {last_closed_time: current_last_time,..}) = projects.remove(&path){cmp::min(last_closed_time, current_last_time)} else {last_closed_time};let project = ProjectStored { last_closed_time };projects.insert(path, project);projects}).await}pub fn updated_closed_time_blocking(path: PathBuf,) -> Result<(), ProjectsFileErr> {with_projects_blocking(move |mut projects| {let project = ProjectStored {last_closed_time: cmp::Reverse(Some(Timestamp::now())),};projects.insert(path, project);projects})}pub async fn rm_project(path: PathBuf) -> Result<(), ProjectsFileErr> {with_projects(move |mut projects| {projects.remove(&path);projects}).await}type ProjectsStored = BTreeMap<PathBuf, ProjectStored>;#[derive(Debug, Clone, Serialize, Deserialize)]struct ProjectStored {last_closed_time: cmp::Reverse<Option<Timestamp>>,}fn from_stored(state: ProjectsStored) -> Projects {state.into_iter().map(|(path, ProjectStored { last_closed_time })| Project {last_closed_time,path,}).collect()}/// Acquire a lock to try to read, update and write [`Projects`].async fn with_projects<F: FnOnce(ProjectsStored) -> ProjectsStored>(f: F,) -> Result<(), ProjectsFileErr> {let path = projects_path();let path_str = path.to_string_lossy();fs::create_dir_all(path.parent().unwrap()).await.with_context(|| {format!("Creating projects file store directories ({path_str})")}).map_err(OneOf::new)?;let read_state = async || {let file = match File::options().create(false).read(true).open(&path).await{Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => {// Create a default if file not foundreturn Ok(ProjectsStored::default());}result => result.with_context(|| format!("Opening projects file ({path_str})")).map_err(OneOf::new)?,};let mut read_guard = match file.lock_read().await {Err(err)if matches!(err.error.kind(), std::io::ErrorKind::WouldBlock) =>{return Err(OneOf::new(BlockingLockError));}result => result.map_err(|e| {format_err!("Obtaining projects file read-lock failed with {e:?} ({path_str})")}).map_err(OneOf::new)?,};let mut buffer = Vec::new();read_guard.read_to_end(&mut buffer).await.with_context(|| format!("Reading projects file ({path_str})")).map_err(OneOf::new)?;let stored: ProjectsStored = toml_edit::de::from_slice(&buffer).with_context(|| format!("Decoding projects file ({path_str})")).map_err(OneOf::new)?;Ok(stored)};let state = read_state().await?;let file = File::options().create(true).write(true).truncate(true).open(&path).await.with_context(|| format!("Opening projects file ({path_str})")).map_err(OneOf::new)?;let mut write_guard = match file.lock_write().await {Err(err)if matches!(err.error.kind(), std::io::ErrorKind::WouldBlock) =>{return Err(OneOf::new(BlockingLockError));}result => result.map_err(|e| {format_err!("Obtaining projects file write-lock failed with {e:?} ({path_str})")}).map_err(OneOf::new)?,};let state = f(state);let state_bytes = toml_edit::ser::to_string_pretty(&state).with_context(|| format!("Encoding projects file ({path_str})")).map_err(OneOf::new)?;write_guard.write_all(state_bytes.as_bytes()).await.with_context(|| format!("Writing projects file ({path_str})")).map_err(OneOf::new)}/// Acquire a write-lock to try to read, update and write [`Projects`]. If/// there's an active write-lock the call will fail with a/// [`BlockingLockError`].fn with_projects_blocking<F: FnOnce(ProjectsStored) -> ProjectsStored>(f: F,) -> Result<(), ProjectsFileErr> {use async_fd_lock::blocking::{LockRead, LockWrite};use std::fs;use std::io::{Read, Write};let path = projects_path();let path_str = path.to_string_lossy();fs::create_dir_all(path.parent().unwrap()).with_context(|| {format!("Creating projects file store directories ({path_str})")}).map_err(OneOf::new)?;let read_state = || {let file = match fs::File::options().create(false).read(true).open(&path){Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => {// Create a default if file not foundreturn Ok(ProjectsStored::default());}result => result.with_context(|| format!("Opening projects file ({path_str})")).map_err(OneOf::new)?,};let mut read_guard = match file.lock_read() {Err(err)if matches!(err.error.kind(), std::io::ErrorKind::WouldBlock) =>{return Err(OneOf::new(BlockingLockError));}result => result.map_err(|e| {format_err!("Obtaining projects file read-lock failed with {e:?} ({path_str})")}).map_err(OneOf::new)?,};let mut buffer = Vec::new();read_guard.read_to_end(&mut buffer).with_context(|| format!("Reading projects file ({path_str})")).map_err(OneOf::new)?;let stored: ProjectsStored = toml_edit::de::from_slice(&buffer).with_context(|| format!("Decoding projects file ({path_str})")).map_err(OneOf::new)?;Ok(stored)};let state = read_state()?;let file = fs::File::options().create(true).write(true).truncate(true).open(&path).with_context(|| format!("Opening projects file ({path_str})")).map_err(OneOf::new)?;let mut write_guard = match file.lock_write() {Err(err)if matches!(err.error.kind(), std::io::ErrorKind::WouldBlock) =>{return Err(OneOf::new(BlockingLockError));}result => result.map_err(|e| {format_err!("Obtaining projects file write-lock failed with {e:?} ({path_str})")}).map_err(OneOf::new)?,};let state = f(state);let state_bytes = toml_edit::ser::to_string_pretty(&state).with_context(|| format!("Encoding projects file ({path_str})")).map_err(OneOf::new)?;write_guard.write_all(state_bytes.as_bytes()).with_context(|| format!("Writing projects file ({path_str})")).map_err(OneOf::new)}pub type ProjectsFileErr = OneOf<(BlockingLockError, anyhow::Error)>;fn projects_path() -> PathBuf {PathBuf::from_iter([dir::data().as_path(), Path::new(PROJECTS_FILE)])}const PROJECTS_FILE: &str = "projects.toml";
use super::*;#[test]fn repo_order() {let mk_repo = |last_closed_time: Option<Timestamp>, path: &str| Project {last_closed_time: cmp::Reverse(last_closed_time),path: PathBuf::from(path),};// Repos in expected orderconst FST_SECOND: i64 = 10000000;// Most recently closedlet repo_0 =mk_repo(Some(Timestamp::from_second(FST_SECOND).unwrap()), "0");let repo_1 =mk_repo(Some(Timestamp::from_second(FST_SECOND - 1).unwrap()), "0");let time_2 = Timestamp::from_second(FST_SECOND - 2).unwrap();let repo_2 = mk_repo(Some(time_2), "0");let repo_3 = mk_repo(Some(time_2), "1");let repo_4 =mk_repo(Some(Timestamp::from_second(FST_SECOND - 3).unwrap()), "0");let repo_5 = mk_repo(None, "3");let repo_6 = mk_repo(None, "4");// Construct in random orderlet repos = Projects::from_iter([repo_3.clone(),repo_1.clone(),repo_6.clone(),repo_4.clone(),repo_2.clone(),repo_0.clone(),repo_5.clone(),]);itertools::assert_equal(repos,[repo_0, repo_1, repo_2, repo_3, repo_4, repo_5, repo_6],);}
pub mod dir;mod state;pub use state::{read_projects, rm_project, updated_closed_time_blocking, upsert_project,BlockingLockError, Project, Projects, ProjectsFileErr,};
//! App directories shared between all Inflorescence apps.use directories::ProjectDirs;use std::path::PathBuf;const QUALIFIER: &str = "com";const ORGANIZATION: &str = "Holonyte";const APPLICATION: &str = "Inflorescence";/// App config dirpub fn config() -> PathBuf {projects_dir().config_dir().to_path_buf()}/// App config dirpub fn data() -> PathBuf {projects_dir().config_dir().to_path_buf()}fn projects_dir() -> ProjectDirs {ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION).unwrap()}
fn picking_repo<'a>(
fn picking_project<'a>(state: &'a model::PickingProject,window_size: iced::Size,allowed_actions: &'a [action::Binding],report: &'a report::Container,) -> Element<'a, Msg, Theme> {let model::PickingProject {projects,is_blocking,} = state;let main = if let Some(projects) = projects {el(column([el(column([el(row([el(button(text("Find or create a new project")).on_press(Msg::PickNewProject)),el(text("or pick a known project below")),]).spacing(SPACING).align_y(alignment::Vertical::Center))])),el(column(projects.iter().map(|store::Project {last_closed_time: _,path,}| {el(button(text(format!("{}", path.to_string_lossy()))).on_press_with(|| Msg::OpenProject(path.clone())))},))),]).spacing(SPACING))} else {el(text(if *is_blocking {"Waiting for a lock release on projects file..."} else {"Loading projects..."}))};let main = el(container(main).width(Length::Fill).height(Length::Fill));add_actions_and_report(None, main, window_size, allowed_actions, report)}fn picking_repo_dir<'a>(
// TODO: `include_bytes!`?let set_icon_task =task::window_set_icon(window_id, PathBuf::from("assets/icon.png"));
let set_icon_task = task::window_set_icon(window_id,include_bytes!("../../assets/icon.png").to_vec(),);
let start_path = env::home_dir().unwrap_or_else(|| PathBuf::from("/"));let (picker, picker_task) = dir_picker::init(start_path);let tasks = Task::batch([open_window_task.map(|_id| Msg::NoOp),set_icon_task,picker_task.map(view::Msg::PickingRepo).map(Msg::View),]);let state = model::PickingRepoDir { picker };let sub = model::SubState::PickingRepoDir(state);
let (sub, projects_task) = init_picking_project();
}fn init_picking_project() -> (model::SubState, Task<Msg>) {let read_projects_task = Task::perform(async { store::read_projects().await },Msg::LoadedProjects,);let state = model::PickingProject {is_blocking: false,projects: None,};let sub = model::SubState::PickingProject(state);(sub, read_projects_task)
model::SubState::PickingProject(model) => {let (task, new_sub_state, managing_repo) =update_picking_project_from_view(model, msg);if let Some(new_sub) = new_sub_state {if let model::SubState::ManagingRepo(_) = new_sub {state.managing_repo = Some(managing_repo.expect("managing_repo is required"),);}state.model.sub = new_sub;}task}
update_managing_repo(sub, model, &mut state.model.report, msg).map(Msg::ManagingRepo)
let (task, new_state) = update_managing_repo(sub,model,&mut state.model.report,msg,);if let Some(new_state) = new_state {state.model.sub = new_state;}task
Msg::LoadedProjects(result) => match result {Ok(projects) => {if projects.is_empty() {// Move on to dir pickerlet (model, task) = init_picking_repo_dir();let sub = model::SubState::PickingRepoDir(model);state.model.sub = sub;task} else {if let model::SubState::PickingProject(sub) =&mut state.model.sub{sub.projects = Some(projects);}Task::none()}}Err(err) => {if let model::SubState::PickingProject(sub) =&mut state.model.sub{match err.as_enum() {terrors::E2::A(store::BlockingLockError) => {sub.is_blocking = true;// retryTask::perform(async { store::read_projects().await },Msg::LoadedProjects,)}_ => {let msg = format!("Failed to read projects from store: {err:?}");report::show_err(&mut state.model.report, msg);Task::none()}}} else {Task::none()}}},Msg::StoredProjects(result) => match result {Ok(()) => Task::none(),Err(err) => {let msg = format!("Failed to write projects to store: {err:?}");report::show_err(&mut state.model.report, msg);Task::none()}},
fn update_picking_project_from_view(_model: &mut model::PickingProject,msg: view::Msg,) -> (Task<Msg>, Option<model::SubState>, Option<ManagingRepo>) {let mut new_state = None;let mut new_managing_repo = None;let task = match msg {view::Msg::Action(_msg) => todo!(),view::Msg::OpenProject(dir) => {let (sub, managing_repo, managing_repo_task) =init_managing_repo(dir);new_state = Some(model::SubState::ManagingRepo(sub));new_managing_repo = Some(managing_repo);managing_repo_task}view::Msg::PickNewProject => {let (model, task) = init_picking_repo_dir();new_state = Some(model::SubState::PickingRepoDir(model));task}view::Msg::PickingRepoDir(_)| view::Msg::EditRecordMsg(_)| view::Msg::EditRecordDesc(_)| view::Msg::EditForkChannelName(_)| view::Msg::UnfilteredSelection(_)| view::Msg::ToRecord(_) => Task::none(),};(task, new_state, new_managing_repo)}
) -> Task<ManagingRepoMsg> {let report_info = |report: &mut report::Container, err: String| {report::show_info(report, err);Task::none()};let report_err = |report: &mut report::Container, err: String| {report::show_err(report, err);Task::none()};match msg {repo::MsgOut::Init(repo) => repo_init(state, model, report, repo),repo::MsgOut::InitFailed(err) => report_err(report, err.to_string()),
) -> (Task<Msg>, Option<model::SubState>) {let mut new_state = None;let task = match msg {repo::MsgOut::Init(repo) => {repo_init(state, model, report, repo).map(Msg::ManagingRepo)}repo::MsgOut::InitFailed {err,switch_to_project_picker,} => {report::show_err(report, err);if switch_to_project_picker {let (sub, projects_task) = init_picking_project();new_state = Some(sub);projects_task} else {Task::none()}}
report_err(report, "Task managing repo has crashed. This shouldn't happen, please report what happened!".to_string())
report::show_err(report, "Task managing repo has crashed. This shouldn't happen, please report what happened!".to_string());Task::none()
Ok(diffs) => repo_got_change_diffs(model, hash, diffs),Err(err) => report_err(report, err.to_string()),
Ok(diffs) => {repo_got_change_diffs(model, hash, diffs).map(Msg::ManagingRepo)}Err(err) => {report::show_err(report, err.to_string());Task::none()}
Ok(log) => loaded_other_channel_log(model, channel, log),Err(err) => report_err(report, err.to_string()),
Ok(log) => loaded_other_channel_log(model, channel, log).map(Msg::ManagingRepo),Err(err) => {report::show_err(report, err.to_string());Task::none()}
let task = match result {Ok(()) => report_info(report, format!("Pushed to {channel}")),Err(repo::PushError::Empty) => report_info(report, format!("Nothing to push to {channel}")),Err(err) => report_err(report, err.to_string()),
match result {Ok(()) => {report::show_info(report, format!("Pushed to {channel}"))}Err(repo::PushError::Empty) => report::show_info(report,format!("Nothing to push to {channel}"),),Err(err) => report::show_err(report, err.to_string()),
if let Some(ReadyState { jobs, .. }) =model::is_ready_mut(model){jobs.swap_remove(&Job::Push{ channel });
if let Some(ReadyState { jobs, .. }) = model::is_ready_mut(model) {jobs.swap_remove(&Job::Push { channel });
task},repo::MsgOut::Pulled { channel, result } =>{let task = match result {Ok(()) => report_info(report, format!("Pulled from {channel}")),Err(repo::PullError::Empty) => report_info(report, format!("Nothing to pull from {channel}")),Err(err) => report_err(report, err.to_string()),
Task::none()}repo::MsgOut::Pulled { channel, result } => {match result {Ok(()) => {report::show_info(report, format!("Pulled from {channel}"))}Err(repo::PullError::Empty) => report::show_info(report,format!("Nothing to pull from {channel}"),),Err(err) => report::show_err(report, err.to_string()),
if let Some(ReadyState { jobs, .. }) =model::is_ready_mut(model){jobs.swap_remove(&Job::Pull{ channel });
if let Some(ReadyState { jobs, .. }) = model::is_ready_mut(model) {jobs.swap_remove(&Job::Pull { channel });
task},}
Task::none()}};(task, new_state)