use std::{ future::Future, sync::{Arc, Mutex, OnceLock}, }; use gotcha_gitea::{Client, apis}; use slint::{ModelRc, VecModel}; mod activity; mod api; mod diff; mod domain; mod markdown; mod storage; mod ui; mod view; use api::*; use domain::*; use storage::*; use ui::*; use view::*; static RUNTIME: OnceLock = OnceLock::new(); fn main() -> Result<(), Box> { let ui = AppWindow::new()?; let (preferences, startup_error) = match load_preferences() { Ok(preferences) => (preferences, None), Err(error) => (Preferences::default(), Some(error)), }; let state = Arc::new(Mutex::new(State { preferences, ..Default::default() })); refresh_servers(&ui, &state.lock().unwrap().preferences); { let state = state.lock().unwrap(); ui.set_issue_filter(state.preferences.issue_status.clone().into()); ui.set_pull_filter(state.preferences.pull_status.clone().into()); } if let Some(error) = startup_error { ui.set_error(error.into()); } wire_callbacks(&ui, state.clone()); let startup_server = { let state = state.lock().unwrap(); state .preferences .last_server .filter(|index| *index < state.preferences.servers.len()) .or_else(|| (state.preferences.servers.len() == 1).then_some(0)) }; if let Some(index) = startup_server { open_server(&ui, state.clone(), index); open_home(&ui, state); } ui.run()?; Ok(()) } fn wire_callbacks(ui: &AppWindow, state: Arc>) { ui.on_select_tab({ let weak = ui.as_weak(); let state = state.clone(); move |tab| { let Some(ui) = weak.upgrade() else { return }; let changed = ui.get_tab() != tab; if changed { state.lock().unwrap().opened_from_home = false; } ui.set_error("".into()); ui.set_tab(tab.clone()); if changed { match tab.as_str() { "home" => open_home(&ui, state.clone()), "pulls" => open_pulls(&ui, state.clone()), "issues" | "repositories" => { ui.set_page(if state.lock().unwrap().active_server.is_some() { "repositories".into() } else { "servers".into() }); } _ => {} } } } }); ui.on_refresh_home({ let weak = ui.as_weak(); let state = state.clone(); move || { if let Some(ui) = weak.upgrade() { open_home(&ui, state.clone()); } } }); ui.on_refresh_repositories({ let weak = ui.as_weak(); let state = state.clone(); move || { let server = { let state = state.lock().unwrap(); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() }; if let (Some(ui), Some(server)) = (weak.upgrade(), server) { fetch_repositories(&ui, state.clone(), server, true); } } }); ui.on_refresh_issues({ let weak = ui.as_weak(); let state = state.clone(); move || { let target = { let state = state.lock().unwrap(); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() .zip(state.active_repository.clone()) }; if let (Some(ui), Some((server, (owner, repository)))) = (weak.upgrade(), target) { fetch_issues(&ui, state.clone(), server, owner, repository, true); } } }); ui.on_refresh_issue({ let weak = ui.as_weak(); let state = state.clone(); move || { let target = { let state = state.lock().unwrap(); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() .zip(state.active_repository.clone()) .zip(state.active_issue) }; if let (Some(ui), Some(((server, (owner, repository)), number))) = (weak.upgrade(), target) { fetch_issue(&ui, server, owner, repository, number, true); } } }); ui.on_refresh_pulls({ let weak = ui.as_weak(); let state = state.clone(); move || { if let Some(ui) = weak.upgrade() { open_pulls_with_refresh(&ui, state.clone(), true); } } }); ui.on_refresh_pull({ let weak = ui.as_weak(); let state = state.clone(); move || { let target = { let state = state.lock().unwrap(); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() .zip(state.active_pull.clone()) }; if let (Some(ui), Some((server, (owner, repository, number)))) = (weak.upgrade(), target) { fetch_pull(&ui, server, owner, repository, number, true); } } }); ui.on_refresh_commits({ let weak = ui.as_weak(); let state = state.clone(); move || { let target = { let state = state.lock().unwrap(); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() .zip(state.active_repository.clone()) }; if let (Some(ui), Some((server, (owner, repository)))) = (weak.upgrade(), target) { fetch_commits(&ui, state.clone(), server, owner, repository, true); } } }); ui.on_show_add_server({ let weak = ui.as_weak(); move || { if let Some(ui) = weak.upgrade() { ui.set_error("".into()); ui.set_page("add".into()); } } }); ui.on_save_server({ let weak = ui.as_weak(); let state = state.clone(); move |name, url, token| { let server = match validate_server(name.as_str(), url.as_str(), token.as_str()) { Ok(server) => server, Err(error) => { if let Some(ui) = weak.upgrade() { ui.set_error(error.into()); } return; } }; if let Some(ui) = weak.upgrade() { ui.set_error("".into()); ui.set_loading(true); } let weak = weak.clone(); let state = state.clone(); spawn(async move { let result = match Client::new(&server.url, Some(&server.token)) { Ok(client) => client .current_user() .await .map_err(|error| error.to_string()), Err(error) => Err(error.to_string()), }; post(move || { let Some(ui) = weak.upgrade() else { return }; ui.set_loading(false); match result { Ok(_) => { if let Err(error) = save_server_token(&server) { ui.set_error(error.into()); return; } let index = { let mut state = state.lock().unwrap(); state.preferences.servers.push(server); let index = state.preferences.servers.len() - 1; state.preferences.last_server = Some(index); if let Err(error) = save_preferences(&state.preferences) { ui.set_error(error.into()); return; } refresh_servers(&ui, &state.preferences); index }; open_server(&ui, state, index); } Err(error) => ui.set_error(error.into()), } }); }); } }); ui.on_select_server({ let weak = ui.as_weak(); let state = state.clone(); move |index| { if let Some(ui) = weak.upgrade() { open_server(&ui, state.clone(), index as usize); } } }); ui.on_select_repository({ let weak = ui.as_weak(); let state = state.clone(); move |owner, name| { if let Some(ui) = weak.upgrade() { if ui.get_tab().as_str() == "repositories" { open_commit_history(&ui, state.clone(), owner.to_string(), name.to_string()); } else { open_repository(&ui, state.clone(), owner.to_string(), name.to_string()); } } } }); ui.on_toggle_favorite({ let weak = ui.as_weak(); let state = state.clone(); move |owner, name| { let mut state = state.lock().unwrap(); let Some(server) = state .active_server .and_then(|i| state.preferences.servers.get(i)) else { return; }; let key = favorite_key(&server.url, owner.as_str(), name.as_str()); if !state.preferences.favorites.remove(&key) { state.preferences.favorites.insert(key); } if let Err(error) = save_preferences(&state.preferences) { if let Some(ui) = weak.upgrade() { ui.set_error(error.into()); } return; } if let Some(ui) = weak.upgrade() { refresh_repositories(&ui, &state); } } }); ui.on_select_issue({ let weak = ui.as_weak(); let state = state.clone(); move |number| { let target = { let mut state = state.lock().unwrap(); state.opened_from_home = false; state.active_issue = Some(number as i64); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() .zip(state.active_repository.clone()) }; if let (Some(ui), Some((server, (owner, repository)))) = (weak.upgrade(), target) { fetch_issue(&ui, server, owner, repository, number as i64, false); } } }); ui.on_select_pull({ let weak = ui.as_weak(); let state = state.clone(); move |owner, repository, number| { let server = { let mut state = state.lock().unwrap(); state.opened_from_home = false; state.active_pull = Some((owner.to_string(), repository.to_string(), number as i64)); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() }; if let (Some(ui), Some(server)) = (weak.upgrade(), server) { fetch_pull( &ui, server, owner.to_string(), repository.to_string(), number as i64, false, ); } } }); ui.on_select_commit({ let weak = ui.as_weak(); let state = state.clone(); move |sha| { let target = { let mut state = state.lock().unwrap(); state.opened_from_home = false; state.active_commit = Some(sha.to_string()); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() .zip(state.active_repository.clone()) }; if let (Some(ui), Some((server, (owner, repository)))) = (weak.upgrade(), target) { fetch_commit(&ui, server, owner, repository, sha.to_string()); } } }); ui.on_select_branch({ let weak = ui.as_weak(); let state = state.clone(); move |selection| { let target = { let mut state = state.lock().unwrap(); let branch = (selection.as_str() != "All").then(|| selection.to_string()); if branch .as_ref() .is_some_and(|branch| !state.branches.contains(branch)) { return; } state.active_branch = branch; state.commits.clear(); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() .zip(state.active_repository.clone()) }; if let (Some(ui), Some((server, (owner, repository)))) = (weak.upgrade(), target) { ui.set_commits(empty_model()); fetch_commits(&ui, state.clone(), server, owner, repository, false); } } }); ui.on_select_file({ let weak = ui.as_weak(); let state = state.clone(); move |path| { let target = { let state = state.lock().unwrap(); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() .zip(state.active_repository.clone()) .zip(state.active_commit.clone()) }; if let (Some(ui), Some(((server, (owner, repository)), sha))) = (weak.upgrade(), target) { fetch_file_diff(&ui, server, owner, repository, sha, path.to_string()); } } }); ui.on_select_pull_file({ let weak = ui.as_weak(); let state = state.clone(); move |path| { let target = { let state = state.lock().unwrap(); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() .zip(state.active_pull.clone()) }; if let (Some(ui), Some((server, (owner, repository, number)))) = (weak.upgrade(), target) { fetch_pull_file_diff(&ui, server, owner, repository, number, path.to_string()); } } }); ui.on_select_activity({ let weak = ui.as_weak(); let state = state.clone(); move |target, owner, repository, number, sha| { let server = { let mut state = state.lock().unwrap(); state.opened_from_home = true; state.active_repository = Some((owner.to_string(), repository.to_string())); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() }; let (Some(ui), Some(server)) = (weak.upgrade(), server) else { return; }; match target.as_str() { "repository" => { ui.set_tab("repositories".into()); open_commit_history( &ui, state.clone(), owner.to_string(), repository.to_string(), ); } "issue" => { state.lock().unwrap().active_issue = Some(number as i64); ui.set_tab("issues".into()); fetch_issue( &ui, server, owner.to_string(), repository.to_string(), number as i64, false, ); } "pull" => { state.lock().unwrap().active_pull = Some((owner.to_string(), repository.to_string(), number as i64)); ui.set_tab("pulls".into()); fetch_pull( &ui, server, owner.to_string(), repository.to_string(), number as i64, false, ); } "commit" => { state.lock().unwrap().active_commit = Some(sha.to_string()); ui.set_tab("repositories".into()); ui.set_repository_title(repository.clone()); fetch_commit( &ui, server, owner.to_string(), repository.to_string(), sha.to_string(), ); } _ => {} } } }); ui.on_choose_filter({ let weak = ui.as_weak(); let state = state.clone(); move |status| { if !matches!(status.as_str(), "open" | "closed") { return; } let Some(ui) = weak.upgrade() else { return }; { let mut state = state.lock().unwrap(); if ui.get_tab().as_str() == "pulls" { state.preferences.pull_status = status.to_string(); ui.set_pull_filter(status.clone()); } else { state.preferences.issue_status = status.to_string(); ui.set_issue_filter(status.clone()); } if let Err(error) = save_preferences(&state.preferences) { ui.set_error(error.into()); return; } } if ui.get_tab().as_str() == "pulls" { open_pulls(&ui, state.clone()); } else { let target = { let state = state.lock().unwrap(); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() .zip(state.active_repository.clone()) }; if let Some((server, (owner, repository))) = target { fetch_issues(&ui, state.clone(), server, owner, repository, false); } } } }); ui.on_back({ let weak = ui.as_weak(); let state = state.clone(); move || { let Some(ui) = weak.upgrade() else { return }; ui.set_error("".into()); if state.lock().unwrap().opened_from_home && matches!(ui.get_page().as_str(), "issue" | "pull" | "files") { state.lock().unwrap().opened_from_home = false; ui.set_tab("home".into()); open_home(&ui, state.clone()); return; } ui.set_page(match (ui.get_tab().as_str(), ui.get_page().as_str()) { ("pulls", "pull") => "pulls".into(), ("pulls", "diff") => "pull".into(), ("repositories", "diff") => "files".into(), ("repositories", "files") => "commits".into(), ("repositories", "commits") => "repositories".into(), (_, "issue") => "issues".into(), (_, "issues") => "repositories".into(), (_, "repositories" | "add") => "servers".into(), _ => "servers".into(), }); } }); } fn open_server(ui: &AppWindow, state: Arc>, index: usize) { let server = { let mut state = state.lock().unwrap(); let Some(server) = state.preferences.servers.get(index).cloned() else { return; }; state.active_server = Some(index); state.preferences.last_server = Some(index); state.repositories.clear(); if let Err(error) = save_preferences(&state.preferences) { ui.set_error(error.into()); } server }; ui.set_has_active_server(true); ui.set_home_server(server.name.clone().into()); ui.set_server_title(server.name.clone().into()); ui.set_repositories(empty_model()); ui.set_page("repositories".into()); ui.set_error("".into()); fetch_repositories(ui, state, server, false); } fn fetch_repositories(ui: &AppWindow, state: Arc>, server: Server, refreshing: bool) { if refreshing { ui.set_refreshing(true); } else { ui.set_loading(true); } let weak = ui.as_weak(); spawn(async move { let result = load_repositories(&server).await; post(move || { let Some(ui) = weak.upgrade() else { return }; if refreshing { ui.set_refreshing(false); } else { ui.set_loading(false); } match result { Ok(repositories) => { let mut state = state.lock().unwrap(); state.repositories = repositories; refresh_repositories(&ui, &state); } Err(error) => ui.set_error(error.into()), } }); }); } fn open_home(ui: &AppWindow, state: Arc>) { let server = { let state = state.lock().unwrap(); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() }; let Some(server) = server else { ui.set_has_active_server(false); ui.set_activities(empty_model()); ui.set_heat_cells(empty_model()); ui.set_contribution_count(0); return; }; ui.set_has_active_server(true); ui.set_home_server(server.name.clone().into()); ui.set_home_loading(true); let server_url = server.url.clone(); let weak = ui.as_weak(); spawn(async move { let result = load_home(&server).await; post(move || { let Some(ui) = weak.upgrade() else { return }; let is_current = { let state = state.lock().unwrap(); state .active_server .and_then(|index| state.preferences.servers.get(index)) .is_some_and(|server| server.url == server_url) }; if !is_current { return; } ui.set_home_loading(false); match result { Ok(home) => refresh_home(&ui, home), Err(error) => ui.set_error(error.into()), } }); }); } fn open_repository(ui: &AppWindow, state: Arc>, owner: String, name: String) { let server = { let mut state = state.lock().unwrap(); state.active_repository = Some((owner.clone(), name.clone())); state.issues.clear(); state .active_server .and_then(|i| state.preferences.servers.get(i)) .cloned() }; let Some(server) = server else { return }; ui.set_repository_title(name.clone().into()); ui.set_issues(empty_model()); ui.set_page("issues".into()); ui.set_error("".into()); fetch_issues(ui, state, server, owner, name, false); } fn fetch_issues( ui: &AppWindow, state: Arc>, server: Server, owner: String, repository: String, refreshing: bool, ) { if refreshing { ui.set_refreshing(true); } else { ui.set_loading(true); } let weak = ui.as_weak(); let status = state.lock().unwrap().preferences.issue_status.clone(); spawn(async move { let result = load_issues(&server, &owner, &repository, &status).await; post(move || { let Some(ui) = weak.upgrade() else { return }; if refreshing { ui.set_refreshing(false); } else { ui.set_loading(false); } match result { Ok(issues) => { let mut state = state.lock().unwrap(); state.issues = issues; refresh_issues(&ui, &state.issues); } Err(error) => ui.set_error(error.into()), } }); }); } fn fetch_issue( ui: &AppWindow, server: Server, owner: String, repository: String, number: i64, refreshing: bool, ) { if refreshing { ui.set_refreshing(true); } else { ui.set_loading(true); } let weak = ui.as_weak(); spawn(async move { let result = async { let client = Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?; let configuration = client.configuration(); let (issue, comments) = tokio::join!( apis::issue_api::issue_get_issue(&configuration, &owner, &repository, number), apis::issue_api::issue_get_comments( &configuration, &owner, &repository, number, None, None, ), ); Ok::<_, String>(IssueDetails { issue: issue.map_err(|error| error.to_string())?, comments: comments.map_err(|error| error.to_string())?, }) } .await; post(move || { let Some(ui) = weak.upgrade() else { return }; if refreshing { ui.set_refreshing(false); } else { ui.set_loading(false); } match result { Ok(details) => show_issue(&ui, &details), Err(error) => ui.set_error(error.into()), } }); }); } fn open_pulls(ui: &AppWindow, state: Arc>) { open_pulls_with_refresh(ui, state, false); } fn open_pulls_with_refresh(ui: &AppWindow, state: Arc>, refreshing: bool) { let target = { let state = state.lock().unwrap(); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() .map(|server| (server, state.preferences.pull_status.clone())) }; let Some((server, status)) = target else { ui.set_has_active_server(false); ui.set_pulls(empty_model()); return; }; ui.set_has_active_server(true); ui.set_page("pulls".into()); if refreshing { ui.set_refreshing(true); } else { ui.set_loading(true); } let weak = ui.as_weak(); spawn(async move { let result = load_pulls(&server, &status).await; post(move || { let Some(ui) = weak.upgrade() else { return }; if refreshing { ui.set_refreshing(false); } else { ui.set_loading(false); } match result { Ok(pulls) => { state.lock().unwrap().pulls = pulls; refresh_pulls(&ui, &state.lock().unwrap().pulls); } Err(error) => ui.set_error(error.into()), } }); }); } fn fetch_pull( ui: &AppWindow, server: Server, owner: String, repository: String, number: i64, refreshing: bool, ) { if refreshing { ui.set_refreshing(true); } else { ui.set_loading(true); } let weak = ui.as_weak(); spawn(async move { let result = async { let client = Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?; let configuration = client.configuration(); let (pull, comments, files) = tokio::join!( apis::repository_api::repo_get_pull_request( &configuration, &owner, &repository, number, ), apis::issue_api::issue_get_comments( &configuration, &owner, &repository, number, None, None, ), load_pull_files(&configuration, &owner, &repository, number,), ); Ok::<_, String>(PullDetails { pull: pull.map_err(|error| error.to_string())?, comments: comments.map_err(|error| error.to_string())?, files: files.map_err(|error| error.to_string())?, }) } .await; post(move || { let Some(ui) = weak.upgrade() else { return }; if refreshing { ui.set_refreshing(false); } else { ui.set_loading(false); } match result { Ok(details) => show_pull(&ui, &details), Err(error) => ui.set_error(error.into()), } }); }); } fn open_commit_history( ui: &AppWindow, state: Arc>, owner: String, repository: String, ) { let server = { let mut state = state.lock().unwrap(); let default_branch = state .repositories .iter() .find(|candidate| candidate.owner == owner && candidate.name == repository) .map(|candidate| candidate.default_branch.clone()) .unwrap_or_else(|| "main".into()); state.active_repository = Some((owner.clone(), repository.clone())); state.active_branch = Some(default_branch.clone()); state.branches.clear(); state.commits.clear(); ui.set_branch_choices(model(vec!["All".into(), default_branch.into()])); ui.set_branch_index(1); state .active_server .and_then(|index| state.preferences.servers.get(index)) .cloned() }; let Some(server) = server else { return }; ui.set_repository_title(repository.clone().into()); ui.set_commits(empty_model()); ui.set_page("commits".into()); ui.set_error("".into()); fetch_commits(ui, state, server, owner, repository, false); } fn fetch_commits( ui: &AppWindow, state: Arc>, server: Server, owner: String, repository: String, refreshing: bool, ) { if refreshing { ui.set_refreshing(true); } else { ui.set_loading(true); } let weak = ui.as_weak(); let (active_branch, branches) = { let state = state.lock().unwrap(); (state.active_branch.clone(), state.branches.clone()) }; spawn(async move { let requested_branch = active_branch.clone(); let result = async { let default_branch = active_branch .as_deref() .or(branches.first().map(String::as_str)) .unwrap_or("main"); let branches = if branches.is_empty() { load_branches(&server, &owner, &repository, default_branch).await? } else { branches }; let commits = match active_branch.as_deref() { Some(branch) => load_branch_commits(&server, &owner, &repository, branch).await?, None => load_all_commits(&server, &owner, &repository, &branches).await?, }; Ok::<_, String>((requested_branch, branches, commits)) } .await; post(move || { let Some(ui) = weak.upgrade() else { return }; if refreshing { ui.set_refreshing(false); } else { ui.set_loading(false); } match result { Ok((requested_branch, branches, commits)) => { let mut state = state.lock().unwrap(); if state.active_branch != requested_branch { return; } state.branches = branches; state.commits = commits; let mut choices: Vec = vec!["All".into()]; choices.extend(state.branches.iter().map(Into::into)); ui.set_branch_choices(model(choices)); ui.set_branch_index( state .active_branch .as_ref() .and_then(|active| { state.branches.iter().position(|branch| branch == active) }) .map_or(0, |index| index as i32 + 1), ); let lane_count = if state.active_branch.is_none() { state .commits .iter() .flat_map(|commit| { commit .top_lanes .iter() .chain(commit.bottom_lanes.iter()) .chain(commit.connections.iter()) .chain(commit.node_lane.iter()) }) .max() .map_or(0, |lane| lane + 1) } else { 0 }; refresh_commits(&ui, &state.commits, lane_count); } Err(error) => ui.set_error(error.into()), } }); }); } fn fetch_commit(ui: &AppWindow, server: Server, owner: String, repository: String, sha: String) { ui.set_loading(true); let weak = ui.as_weak(); spawn(async move { let result = async { let client = Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?; apis::repository_api::repo_get_single_commit( &client.configuration(), &owner, &repository, &sha, Some(true), None, Some(true), ) .await .map_err(|error| error.to_string()) } .await; post(move || { let Some(ui) = weak.upgrade() else { return }; ui.set_loading(false); match result { Ok(commit) => { ui.set_files(model( commit .files .unwrap_or_default() .into_iter() .filter_map(|file| { Some(FileRow { path: file.filename?.into(), status: file.status.unwrap_or_else(|| "modified".into()).into(), }) }) .collect(), )); ui.set_page("files".into()); } Err(error) => ui.set_error(error.into()), } }); }); } fn fetch_file_diff( ui: &AppWindow, server: Server, owner: String, repository: String, sha: String, path: String, ) { ui.set_loading(true); let weak = ui.as_weak(); spawn(async move { let result = async { let client = Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?; apis::repository_api::repo_download_commit_diff_or_patch( &client.configuration(), &owner, &repository, &sha, "diff", ) .await .map(|text| diff::parse_file(&text, &path)) .map_err(|error| error.to_string()) } .await; post(move || { let Some(ui) = weak.upgrade() else { return }; ui.set_loading(false); match result { Ok(diff) => show_file_diff(&ui, &path, diff), Err(error) => ui.set_error(error.into()), } }); }); } fn fetch_pull_file_diff( ui: &AppWindow, server: Server, owner: String, repository: String, number: i64, path: String, ) { ui.set_loading(true); let weak = ui.as_weak(); spawn(async move { let result = async { let client = Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?; apis::repository_api::repo_download_pull_diff_or_patch( &client.configuration(), &owner, &repository, number, "diff", Some(false), ) .await .map(|text| diff::parse_file(&text, &path)) .map_err(|error| error.to_string()) } .await; post(move || { let Some(ui) = weak.upgrade() else { return }; ui.set_loading(false); match result { Ok(diff) => show_file_diff(&ui, &path, diff), Err(error) => ui.set_error(error.into()), } }); }); } fn show_file_diff(ui: &AppWindow, path: &str, diff: diff::Parsed) { ui.set_detail_title(path.rsplit('/').next().unwrap_or(path).into()); ui.set_diff_columns(diff.columns.min(i32::MAX as usize) as i32); ui.set_diff_lines(model( diff.lines .into_iter() .map(|line| DiffLine { old_number: line.old_number.into(), new_number: line.new_number.into(), text: line.text.into(), kind: line.kind.into(), }) .collect(), )); ui.set_page("diff".into()); } fn runtime() -> &'static tokio::runtime::Runtime { RUNTIME.get_or_init(|| tokio::runtime::Runtime::new().expect("Tokio runtime")) } fn spawn(future: impl Future + Send + 'static) { runtime().spawn(future); } fn post(callback: impl FnOnce() + Send + 'static) { let _ = slint::invoke_from_event_loop(callback); } fn model(rows: Vec) -> ModelRc { ModelRc::new(VecModel::from(rows)) } fn empty_model() -> ModelRc { model(Vec::new()) }