Files
Gotcha/crates/app/src/main.rs
2026-07-30 17:40:34 +02:00

1181 lines
39 KiB
Rust

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<tokio::runtime::Runtime> = OnceLock::new();
fn main() -> Result<(), Box<dyn std::error::Error>> {
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<Mutex<State>>) {
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<Mutex<State>>, 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<Mutex<State>>, 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<Mutex<State>>) {
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<Mutex<State>>, 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<Mutex<State>>,
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<Mutex<State>>) {
open_pulls_with_refresh(ui, state, false);
}
fn open_pulls_with_refresh(ui: &AppWindow, state: Arc<Mutex<State>>, 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<Mutex<State>>,
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<Mutex<State>>,
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<slint::SharedString> = 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<Output = ()> + Send + 'static) {
runtime().spawn(future);
}
fn post(callback: impl FnOnce() + Send + 'static) {
let _ = slint::invoke_from_event_loop(callback);
}
fn model<T: Clone + 'static>(rows: Vec<T>) -> ModelRc<T> {
ModelRc::new(VecModel::from(rows))
}
fn empty_model<T: Clone + 'static>() -> ModelRc<T> {
model(Vec::new())
}