Enforce Rust and Swift ownership boundary

This commit is contained in:
Georg Bauer
2026-07-31 14:10:00 +02:00
parent 6eb27537e8
commit a2615d5d83
10 changed files with 576 additions and 190 deletions

View File

@@ -27,6 +27,34 @@ cargo test --workspace
Do not weaken or skip these gates to make a commit pass. Fix the underlying Do not weaken or skip these gates to make a commit pass. Fix the underlying
warning, lint, formatting issue, or test failure. warning, lint, formatting issue, or test failure.
## Rust and Swift ownership boundary
Rust owns all application behavior. Put networking, authentication, input
validation, persistence, preference changes, filtering, sorting, aggregation,
parsing, content classification, display-data formatting, and navigation target
derivation in `crates/app`. Cover non-trivial behavior there with Rust tests and
expose view-ready records and operations through UniFFI.
Swift is the native iOS presentation layer. Limit `ios/Sources` to UIKit and
SwiftUI lifecycle, view-controller navigation, native controls, layout, drawing,
fonts, colors, symbols, accessibility, task cancellation tied to view lifetime,
and Apple presentation integrations such as Quick Look. Swift may map
Rust-provided states to visual treatments and maintain transient control state;
it must not infer domain state from display strings, duplicate Rust
transformations, classify content, or implement persistence and API behavior.
When changing iOS functionality:
- Trace the complete flow before editing and implement behavior in Rust first.
- Prefer view-ready Rust records over exposing raw Gitea models or rebuilding
titles, summaries, selections, progress, and categories in Swift.
- Treat a pure Swift helper that does not require an Apple UI framework as a
boundary warning; move it to Rust unless it only calculates view geometry.
- Regenerate and commit the UniFFI Swift and C bindings whenever the exported
Rust interface changes.
- During review, inspect both sides of the bridge and reject new business logic
added to Swift merely because its caller is a view controller.
## UI verification conventions ## UI verification conventions
`TESTING.md` is the source of truth for iOS visual verification and the release `TESTING.md` is the source of truth for iOS visual verification and the release

View File

@@ -80,6 +80,12 @@ pub struct IssueFilter {
pub labels: BTreeSet<String>, pub labels: BTreeSet<String>,
} }
impl IssueFilter {
pub fn is_active(&self, status: &str) -> bool {
status != "open" || self != &Self::default()
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct RepositoryData { pub struct RepositoryData {
pub owner: String, pub owner: String,
@@ -132,3 +138,21 @@ pub struct PullDetails {
pub fn open_status() -> String { pub fn open_status() -> String {
"open".into() "open".into()
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn issue_filter_default_state_is_owned_by_rust() {
assert!(!IssueFilter::default().is_active("open"));
assert!(IssueFilter::default().is_active("closed"));
assert!(
IssueFilter {
milestone: "v1".into(),
..Default::default()
}
.is_active("open")
);
}
}

View File

@@ -259,6 +259,26 @@ impl GotchaCore {
Ok(issue_filter_options(&labels, &milestones, &filter)) Ok(issue_filter_options(&labels, &milestones, &filter))
} }
pub fn issue_filters_active(
&self,
owner: String,
repository: String,
) -> Result<bool, GotchaError> {
let server = self.server()?;
let state = self.state.lock().unwrap();
let filter = state
.preferences
.issue_filters
.get(&repository_key(
&server.url,
owner.trim(),
repository.trim(),
))
.cloned()
.unwrap_or_default();
Ok(filter.is_active(&state.preferences.issue_status))
}
pub fn set_issue_filters( pub fn set_issue_filters(
&self, &self,
owner: String, owner: String,
@@ -381,8 +401,9 @@ impl GotchaCore {
owner: String, owner: String,
repository: String, repository: String,
path: String, path: String,
) -> Result<Vec<u8>, GotchaError> { ) -> Result<RepositoryFilePage, GotchaError> {
Ok(load_repository_file(&self.server()?, &owner, &repository, &path).await?) let data = load_repository_file(&self.server()?, &owner, &repository, &path).await?;
Ok(repository_file_page(&path, data))
} }
pub async fn commit_files( pub async fn commit_files(

View File

@@ -71,6 +71,7 @@ pub struct IssuePage {
pub struct IssueFilterOptions { pub struct IssueFilterOptions {
pub milestones: Vec<String>, pub milestones: Vec<String>,
pub labels: Vec<String>, pub labels: Vec<String>,
pub unavailable_labels: Vec<String>,
pub selected_milestone: String, pub selected_milestone: String,
pub selected_labels: Vec<String>, pub selected_labels: Vec<String>,
} }
@@ -81,8 +82,9 @@ pub struct MilestoneRow {
pub title: String, pub title: String,
pub description: String, pub description: String,
pub meta: String, pub meta: String,
pub open_issues: i64, pub has_issues: bool,
pub closed_issues: i64, pub progress: f64,
pub progress_accessibility: String,
} }
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
@@ -105,8 +107,7 @@ pub struct PullPage {
pub struct CommitRow { pub struct CommitRow {
pub sha: String, pub sha: String,
pub title: String, pub title: String,
pub meta: String, pub detail: String,
pub refs: String,
pub top_lanes: Vec<u32>, pub top_lanes: Vec<u32>,
pub bottom_lanes: Vec<u32>, pub bottom_lanes: Vec<u32>,
pub node_lane: Option<u32>, pub node_lane: Option<u32>,
@@ -134,6 +135,15 @@ pub struct RepositoryContentRow {
pub size: i64, pub size: i64,
} }
#[derive(Clone, uniffi::Record)]
pub struct RepositoryFilePage {
pub name: String,
pub kind: String,
pub language: String,
pub text: String,
pub data: Vec<u8>,
}
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
pub struct DiffLine { pub struct DiffLine {
pub old_number: String, pub old_number: String,
@@ -164,17 +174,16 @@ pub struct ActivityRow {
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
pub struct HeatCell { pub struct HeatCell {
pub week: u32,
pub day: u32,
pub level: u32, pub level: u32,
pub timestamp: i64, pub timestamp: i64,
pub contributions: i64,
} }
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
pub struct HomePage { pub struct HomePage {
pub server_name: String, pub server_name: String,
pub activities: Vec<ActivityRow>, pub activities: Vec<ActivityRow>,
pub issue_activities: Vec<ActivityRow>,
pub pull_activities: Vec<ActivityRow>,
pub heat_cells: Vec<HeatCell>, pub heat_cells: Vec<HeatCell>,
pub contribution_count: i64, pub contribution_count: i64,
} }
@@ -237,17 +246,28 @@ pub fn issue_filter_options(
.iter() .iter()
.filter_map(|label| label.name.clone()) .filter_map(|label| label.name.clone())
.collect(); .collect();
let unavailable_labels = filter
.labels
.iter()
.filter(|label| !labels.contains(label))
.cloned()
.collect();
labels.extend(filter.labels.iter().cloned());
labels.sort_by_key(|label| label.to_lowercase()); labels.sort_by_key(|label| label.to_lowercase());
labels.dedup(); labels.dedup();
let mut milestones: Vec<_> = milestones let mut milestones: Vec<_> = milestones
.iter() .iter()
.filter_map(|milestone| milestone.title.clone()) .filter_map(|milestone| milestone.title.clone())
.collect(); .collect();
if !filter.milestone.is_empty() {
milestones.push(filter.milestone.clone());
}
milestones.sort_by_key(|milestone| milestone.to_lowercase()); milestones.sort_by_key(|milestone| milestone.to_lowercase());
milestones.dedup(); milestones.dedup();
IssueFilterOptions { IssueFilterOptions {
milestones, milestones,
labels, labels,
unavailable_labels,
selected_milestone: filter.milestone.clone(), selected_milestone: filter.milestone.clone(),
selected_labels: filter.labels.iter().cloned().collect(), selected_labels: filter.labels.iter().cloned().collect(),
} }
@@ -299,10 +319,12 @@ pub fn pull_rows(pulls: &[models::Issue]) -> Vec<PullRow> {
number: pull.number.unwrap_or_default(), number: pull.number.unwrap_or_default(),
owner: repository.owner.clone()?, owner: repository.owner.clone()?,
repository: repository.name.clone()?, repository: repository.name.clone()?,
title: pull title: format!(
.title "{} #{}\n{}",
.clone() repository.name.as_deref()?,
.unwrap_or_else(|| "Untitled pull request".into()), pull.number.unwrap_or_default(),
pull.title.as_deref().unwrap_or("Untitled pull request")
),
summary: pull summary: pull
.body .body
.as_deref() .as_deref()
@@ -367,8 +389,16 @@ pub fn commit_page(
.map(|message| message.lines().next().unwrap_or(message)) .map(|message| message.lines().next().unwrap_or(message))
.unwrap_or("Commit") .unwrap_or("Commit")
.into(), .into(),
meta: format!("{author} · {}", compact_date(date)), detail: if row.refs.is_empty() {
refs: row.refs.join(" · "), format!("{author} · {}\n{}", compact_date(date), short_sha(sha))
} else {
format!(
"{}\n{author} · {} · {}",
row.refs.join(" · "),
compact_date(date),
short_sha(sha)
)
},
top_lanes: indices(&row.top_lanes), top_lanes: indices(&row.top_lanes),
bottom_lanes: indices(&row.bottom_lanes), bottom_lanes: indices(&row.bottom_lanes),
node_lane: row.node_lane.map(|lane| lane as u32), node_lane: row.node_lane.map(|lane| lane as u32),
@@ -488,6 +518,66 @@ pub fn repository_content_rows(
rows rows
} }
pub fn repository_file_page(path: &str, data: Vec<u8>) -> RepositoryFilePage {
let name = path.rsplit('/').next().unwrap_or("Preview").to_string();
let extension = path.rsplit('.').next().unwrap_or_default().to_lowercase();
let language = source_language(path).unwrap_or_default().to_string();
let markdown = matches!(extension.as_str(), "md" | "markdown" | "mdown" | "mkd");
let media = matches!(
extension.as_str(),
"apng"
| "avi"
| "bmp"
| "flac"
| "gif"
| "heic"
| "heif"
| "jpeg"
| "jpg"
| "m4a"
| "m4v"
| "mkv"
| "mov"
| "mp3"
| "mp4"
| "mpeg"
| "mpg"
| "ogg"
| "pdf"
| "png"
| "tif"
| "tiff"
| "wav"
| "webm"
| "webp"
);
if media {
return RepositoryFilePage {
name,
kind: "preview".into(),
language,
text: String::new(),
data,
};
}
match String::from_utf8(data) {
Ok(text) => RepositoryFilePage {
name,
kind: if markdown { "markdown" } else { "source" }.into(),
language,
text,
data: Vec::new(),
},
Err(error) => RepositoryFilePage {
name,
kind: "preview".into(),
language,
text: String::new(),
data: error.into_bytes(),
},
}
}
pub fn diff_page(path: &str, diff: crate::diff::Parsed) -> DiffPage { pub fn diff_page(path: &str, diff: crate::diff::Parsed) -> DiffPage {
DiffPage { DiffPage {
title: path.rsplit('/').next().unwrap_or(path).into(), title: path.rsplit('/').next().unwrap_or(path).into(),
@@ -509,12 +599,51 @@ pub fn home_page(server_name: String, home: HomeData) -> HomePage {
let (heat_cells, contribution_count) = heat_cells(&home.heatmap); let (heat_cells, contribution_count) = heat_cells(&home.heatmap);
HomePage { HomePage {
server_name, server_name,
issue_activities: home
.activities
.iter()
.filter(|activity| is_issue_activity(activity))
.map(activity_row)
.collect(),
pull_activities: home
.activities
.iter()
.filter(|activity| is_pull_activity(activity))
.map(activity_row)
.collect(),
activities: home.activities.iter().map(activity_row).collect(), activities: home.activities.iter().map(activity_row).collect(),
heat_cells, heat_cells,
contribution_count, contribution_count,
} }
} }
fn is_issue_activity(activity: &models::Activity) -> bool {
use models::activity::OpType;
matches!(
activity.op_type,
Some(OpType::CreateIssue | OpType::CloseIssue | OpType::ReopenIssue | OpType::CommentIssue)
)
}
fn is_pull_activity(activity: &models::Activity) -> bool {
use models::activity::OpType;
matches!(
activity.op_type,
Some(
OpType::CreatePullRequest
| OpType::MergePullRequest
| OpType::AutoMergePullRequest
| OpType::ClosePullRequest
| OpType::ReopenPullRequest
| OpType::CommentPull
| OpType::ApprovePullRequest
| OpType::RejectPullRequest
)
)
}
fn activity_row(activity: &models::Activity) -> ActivityRow { fn activity_row(activity: &models::Activity) -> ActivityRow {
use models::activity::OpType; use models::activity::OpType;
@@ -653,15 +782,20 @@ fn activity_detail(activity: &models::Activity) -> String {
} }
fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) { fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
const DAYS: i64 = 9 * 31;
let latest = data let latest = data
.iter() .iter()
.filter_map(|entry| entry.timestamp) .filter_map(|entry| entry.timestamp)
.max() .max()
.unwrap_or(0) .unwrap_or(0)
/ 86_400; / 86_400;
let start = latest - DAYS + 1; let (year, month, _) = civil_from_days(latest);
let mut counts = vec![0_i64; DAYS as usize]; let first_month = year * 12 + month - 1 - 8;
let start = days_from_civil(
first_month.div_euclid(12),
first_month.rem_euclid(12) + 1,
1,
);
let mut counts = vec![0_i64; (latest - start + 1) as usize];
for entry in data { for entry in data {
let day = entry.timestamp.unwrap_or_default() / 86_400; let day = entry.timestamp.unwrap_or_default() / 86_400;
if (start..=latest).contains(&day) { if (start..=latest).contains(&day) {
@@ -674,20 +808,42 @@ fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(index, count)| HeatCell { .map(|(index, count)| HeatCell {
week: (index / 7) as u32,
day: (index % 7) as u32,
level: if count == 0 || maximum == 0 { level: if count == 0 || maximum == 0 {
0 0
} else { } else {
((count * 4 + maximum - 1) / maximum).clamp(1, 4) as u32 ((count * 4 + maximum - 1) / maximum).clamp(1, 4) as u32
}, },
timestamp: (start + index as i64) * 86_400, timestamp: (start + index as i64) * 86_400,
contributions: count,
}) })
.collect(); .collect();
(cells, contribution_count) (cells, contribution_count)
} }
fn civil_from_days(days: i64) -> (i64, i64, i64) {
let days = days + 719_468;
let era = days.div_euclid(146_097);
let day_of_era = days - era * 146_097;
let year_of_era =
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let mut year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let month_prime = (5 * day_of_year + 2) / 153;
let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
let month = month_prime + if month_prime < 10 { 3 } else { -9 };
year += i64::from(month <= 2);
(year, month, day)
}
fn days_from_civil(mut year: i64, month: i64, day: i64) -> i64 {
year -= i64::from(month <= 2);
let era = year.div_euclid(400);
let year_of_era = year - era * 400;
let month_prime = month + if month > 2 { -3 } else { 9 };
let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
era * 146_097 + day_of_era - 719_468
}
fn label_row(label: &models::Label) -> Option<LabelRow> { fn label_row(label: &models::Label) -> Option<LabelRow> {
let name = label.name.as_deref()?.trim(); let name = label.name.as_deref()?.trim();
let color = label.color.as_deref()?.trim().trim_start_matches('#'); let color = label.color.as_deref()?.trim().trim_start_matches('#');
@@ -777,6 +933,7 @@ fn issue_milestone(issue: &models::Issue) -> String {
fn milestone_row(milestone: &models::Milestone) -> MilestoneRow { fn milestone_row(milestone: &models::Milestone) -> MilestoneRow {
let open = milestone.open_issues.unwrap_or_default(); let open = milestone.open_issues.unwrap_or_default();
let closed = milestone.closed_issues.unwrap_or_default(); let closed = milestone.closed_issues.unwrap_or_default();
let total = open + closed;
let due = milestone let due = milestone
.due_on .due_on
.as_deref() .as_deref()
@@ -798,8 +955,68 @@ fn milestone_row(milestone: &models::Milestone) -> MilestoneRow {
milestone.state.as_deref().unwrap_or("unknown"), milestone.state.as_deref().unwrap_or("unknown"),
open + closed open + closed
), ),
open_issues: open, has_issues: total > 0,
closed_issues: closed, progress: if total == 0 {
0.0
} else {
closed as f64 / total as f64
},
progress_accessibility: format!("{closed} closed, {open} open"),
}
}
fn short_sha(sha: &str) -> &str {
&sha[..sha.len().min(8)]
}
fn source_language(path: &str) -> Option<&'static str> {
let filename = path.rsplit('/').next()?.to_lowercase();
match filename.as_str() {
"cmakelists.txt" => return Some("cmake"),
"dockerfile" => return Some("dockerfile"),
"gemfile" | "podfile" => return Some("ruby"),
"makefile" => return Some("makefile"),
_ => {}
}
match filename.rsplit('.').next()? {
"asm" => Some("x86asm"),
"c" => Some("c"),
"cc" | "cpp" | "h" | "hpp" => Some("cpp"),
"clj" => Some("clojure"),
"cs" => Some("csharp"),
"css" => Some("css"),
"dart" => Some("dart"),
"ex" | "exs" => Some("elixir"),
"fs" => Some("fsharp"),
"go" => Some("go"),
"groovy" => Some("groovy"),
"hs" => Some("haskell"),
"html" | "plist" | "xml" => Some("xml"),
"java" => Some("java"),
"jl" => Some("julia"),
"js" | "jsx" => Some("javascript"),
"json" => Some("json"),
"kt" | "kts" => Some("kotlin"),
"lua" => Some("lua"),
"m" | "mm" => Some("objectivec"),
"md" | "markdown" | "mdown" | "mkd" => Some("markdown"),
"php" => Some("php"),
"pl" => Some("perl"),
"ps1" => Some("powershell"),
"py" => Some("python"),
"r" => Some("r"),
"rb" => Some("ruby"),
"rs" => Some("rust"),
"scala" => Some("scala"),
"scss" => Some("scss"),
"sh" => Some("bash"),
"sql" => Some("sql"),
"swift" => Some("swift"),
"toml" => Some("ini"),
"ts" | "tsx" => Some("typescript"),
"txt" => Some("plaintext"),
"yaml" | "yml" => Some("yaml"),
_ => None,
} }
} }
@@ -853,24 +1070,19 @@ mod tests {
fn maps_heat_levels_and_labels() { fn maps_heat_levels_and_labels() {
let heatmap = vec![ let heatmap = vec![
models::UserHeatmapData { models::UserHeatmapData {
timestamp: Some(100 * 86_400), timestamp: Some(20_393 * 86_400),
contributions: Some(1), contributions: Some(1),
}, },
models::UserHeatmapData { models::UserHeatmapData {
timestamp: Some(101 * 86_400), timestamp: Some(20_665 * 86_400),
contributions: Some(4), contributions: Some(4),
}, },
]; ];
let (cells, total) = heat_cells(&heatmap); let (cells, total) = heat_cells(&heatmap);
assert_eq!((cells.len(), total), (279, 5)); assert_eq!((cells.len(), total), (273, 5));
assert_eq!( assert_eq!(
( (cells[0].level, cells[0].timestamp, cells[272].level,),
cells[277].level, (1, 20_393 * 86_400, 4)
cells[277].timestamp,
cells[277].contributions,
cells[278].level,
),
(1, 100 * 86_400, 1, 4)
); );
let label = models::Label { let label = models::Label {
@@ -890,10 +1102,8 @@ mod tests {
..Default::default() ..Default::default()
}; };
let milestone_row = milestone_rows(std::slice::from_ref(&milestone)).remove(0); let milestone_row = milestone_rows(std::slice::from_ref(&milestone)).remove(0);
assert_eq!( assert_eq!(milestone_row.progress, 0.4);
(milestone_row.open_issues, milestone_row.closed_issues), assert_eq!(milestone_row.progress_accessibility, "2 closed, 3 open");
(3, 2)
);
let issue = models::Issue { let issue = models::Issue {
milestone: Some(Box::new(milestone)), milestone: Some(Box::new(milestone)),
..Default::default() ..Default::default()
@@ -901,6 +1111,33 @@ mod tests {
assert_eq!(issue_rows(&[issue])[0].milestone, "Version 1"); assert_eq!(issue_rows(&[issue])[0].milestone, "Version 1");
} }
#[test]
fn categorizes_home_activity_before_crossing_the_bridge() {
use models::activity::OpType;
let page = home_page(
"Gitea".into(),
HomeData {
activities: [
OpType::CreateIssue,
OpType::CreatePullRequest,
OpType::CreateRepo,
]
.into_iter()
.map(|op_type| models::Activity {
op_type: Some(op_type),
..Default::default()
})
.collect(),
heatmap: Vec::new(),
},
);
assert_eq!(page.activities.len(), 3);
assert_eq!(page.issue_activities.len(), 1);
assert_eq!(page.pull_activities.len(), 1);
}
#[test] #[test]
fn issue_page_exposes_state() { fn issue_page_exposes_state() {
let page = issue_page(IssueDetails { let page = issue_page(IssueDetails {
@@ -917,8 +1154,8 @@ mod tests {
#[test] #[test]
fn builds_sorted_issue_filter_options() { fn builds_sorted_issue_filter_options() {
let filter = IssueFilter { let filter = IssueFilter {
milestone: "v2".into(), milestone: "v3".into(),
labels: ["bug".into(), "critical".into()].into(), labels: ["bug".into(), "retired".into()].into(),
}; };
let options = issue_filter_options( let options = issue_filter_options(
&[ &[
@@ -944,9 +1181,38 @@ mod tests {
&filter, &filter,
); );
assert_eq!(options.labels, ["bug", "critical"]); assert_eq!(options.labels, ["bug", "critical", "retired"]);
assert_eq!(options.milestones, ["v1", "v2"]); assert_eq!(options.unavailable_labels, ["retired"]);
assert_eq!(options.selected_labels, ["bug", "critical"]); assert_eq!(options.milestones, ["v1", "v2", "v3"]);
assert_eq!(options.selected_milestone, "v2"); assert_eq!(options.selected_labels, ["bug", "retired"]);
assert_eq!(options.selected_milestone, "v3");
}
#[test]
fn classifies_repository_files_in_rust() {
let markdown = repository_file_page("docs/README.md", b"# Hello".to_vec());
assert_eq!(
(
markdown.name.as_str(),
markdown.kind.as_str(),
markdown.language.as_str()
),
("README.md", "markdown", "markdown")
);
assert_eq!(markdown.text, "# Hello");
let source = repository_file_page("Sources/App.swift", b"import UIKit".to_vec());
assert_eq!(
(source.kind.as_str(), source.language.as_str()),
("source", "swift")
);
let preview = repository_file_page("image.png", vec![0, 159]);
assert_eq!(preview.kind, "preview");
assert_eq!(preview.data, [0, 159]);
for path in ["manual.pdf", "sound.mp3", "movie.mp4"] {
assert_eq!(repository_file_page(path, Vec::new()).kind, "preview");
}
} }
} }

View File

@@ -494,6 +494,22 @@ fileprivate struct FfiConverterInt64: FfiConverterPrimitive {
} }
} }
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
fileprivate struct FfiConverterDouble: FfiConverterPrimitive {
typealias FfiType = Double
typealias SwiftType = Double
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Double {
return try lift(readDouble(&buf))
}
public static func write(_ value: Double, into buf: inout [UInt8]) {
writeDouble(&buf, lower(value))
}
}
#if swift(>=5.8) #if swift(>=5.8)
@_documentation(visibility: private) @_documentation(visibility: private)
#endif #endif
@@ -605,6 +621,8 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func issueFilters(owner: String, repository: String) async throws -> IssueFilterOptions func issueFilters(owner: String, repository: String) async throws -> IssueFilterOptions
func issueFiltersActive(owner: String, repository: String) throws -> Bool
func issues(owner: String, repository: String) async throws -> [IssueRow] func issues(owner: String, repository: String) async throws -> [IssueRow]
func milestone(owner: String, repository: String, id: Int64) async throws -> MilestonePage func milestone(owner: String, repository: String, id: Int64) async throws -> MilestonePage
@@ -621,7 +639,7 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func repositoryContents(owner: String, repository: String, path: String) async throws -> [RepositoryContentRow] func repositoryContents(owner: String, repository: String, path: String) async throws -> [RepositoryContentRow]
func repositoryFile(owner: String, repository: String, path: String) async throws -> Data func repositoryFile(owner: String, repository: String, path: String) async throws -> RepositoryFilePage
func selectServer(index: UInt32) throws func selectServer(index: UInt32) throws
@@ -819,10 +837,10 @@ open func issue(owner: String, repository: String, number: Int64)async throws -
open func issueFilters(owner: String, repository: String)async throws -> IssueFilterOptions { open func issueFilters(owner: String, repository: String)async throws -> IssueFilterOptions {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_issue_filters( uniffi_gotcha_core_fn_method_gotchacore_issue_filters(
self.uniffiCloneHandle(), FfiConverterString.lower(owner), FfiConverterString.lower(repository) self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository)
) )
}, },
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer, pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
@@ -833,6 +851,17 @@ open func issueFilters(owner: String, repository: String)async throws -> IssueF
) )
} }
open func issueFiltersActive(owner: String, repository: String)throws -> Bool {
return try FfiConverterBool.lift(try rustCallWithError(FfiConverterTypeGotchaError_lift) {
uniffiCallStatus in
uniffi_gotcha_core_fn_method_gotchacore_issue_filters_active(
self.uniffiCloneHandle(),
FfiConverterString.lower(owner),
FfiConverterString.lower(repository),uniffiCallStatus
)
})
}
open func issues(owner: String, repository: String)async throws -> [IssueRow] { open func issues(owner: String, repository: String)async throws -> [IssueRow] {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
@@ -961,7 +990,7 @@ open func repositoryContents(owner: String, repository: String, path: String)asy
) )
} }
open func repositoryFile(owner: String, repository: String, path: String)async throws -> Data { open func repositoryFile(owner: String, repository: String, path: String)async throws -> RepositoryFilePage {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
@@ -972,7 +1001,7 @@ open func repositoryFile(owner: String, repository: String, path: String)async t
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer, pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
completeFunc: ffi_gotcha_core_rust_future_complete_rust_buffer, completeFunc: ffi_gotcha_core_rust_future_complete_rust_buffer,
freeFunc: ffi_gotcha_core_rust_future_free_rust_buffer, freeFunc: ffi_gotcha_core_rust_future_free_rust_buffer,
liftFunc: FfiConverterData.lift, liftFunc: FfiConverterTypeRepositoryFilePage_lift,
errorHandler: FfiConverterTypeGotchaError_lift errorHandler: FfiConverterTypeGotchaError_lift
) )
} }
@@ -1007,11 +1036,11 @@ open func setAppearance(index: UInt32)throws {try rustCallWithError(FfiConvert
open func setIssueFilters(owner: String, repository: String, milestone: String, labels: [String])throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) { open func setIssueFilters(owner: String, repository: String, milestone: String, labels: [String])throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
uniffiCallStatus in uniffiCallStatus in
uniffi_gotcha_core_fn_method_gotchacore_set_issue_filters( uniffi_gotcha_core_fn_method_gotchacore_set_issue_filters(
self.uniffiCloneHandle(), self.uniffiCloneHandle(),
FfiConverterString.lower(owner), FfiConverterString.lower(owner),
FfiConverterString.lower(repository), FfiConverterString.lower(repository),
FfiConverterString.lower(milestone), FfiConverterString.lower(milestone),
FfiConverterSequenceString.lower(labels), uniffiCallStatus FfiConverterSequenceString.lower(labels),uniffiCallStatus
) )
} }
} }
@@ -1312,8 +1341,7 @@ public func FfiConverterTypeCommitPage_lower(_ value: CommitPage) -> RustBuffer
public struct CommitRow: Equatable, Hashable { public struct CommitRow: Equatable, Hashable {
public var sha: String public var sha: String
public var title: String public var title: String
public var meta: String public var detail: String
public var refs: String
public var topLanes: [UInt32] public var topLanes: [UInt32]
public var bottomLanes: [UInt32] public var bottomLanes: [UInt32]
public var nodeLane: UInt32? public var nodeLane: UInt32?
@@ -1321,11 +1349,10 @@ public struct CommitRow: Equatable, Hashable {
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(sha: String, title: String, meta: String, refs: String, topLanes: [UInt32], bottomLanes: [UInt32], nodeLane: UInt32?, connections: [UInt32]) { public init(sha: String, title: String, detail: String, topLanes: [UInt32], bottomLanes: [UInt32], nodeLane: UInt32?, connections: [UInt32]) {
self.sha = sha self.sha = sha
self.title = title self.title = title
self.meta = meta self.detail = detail
self.refs = refs
self.topLanes = topLanes self.topLanes = topLanes
self.bottomLanes = bottomLanes self.bottomLanes = bottomLanes
self.nodeLane = nodeLane self.nodeLane = nodeLane
@@ -1350,8 +1377,7 @@ public struct FfiConverterTypeCommitRow: FfiConverterRustBuffer {
try CommitRow( try CommitRow(
sha: FfiConverterString.read(from: &buf), sha: FfiConverterString.read(from: &buf),
title: FfiConverterString.read(from: &buf), title: FfiConverterString.read(from: &buf),
meta: FfiConverterString.read(from: &buf), detail: FfiConverterString.read(from: &buf),
refs: FfiConverterString.read(from: &buf),
topLanes: FfiConverterSequenceUInt32.read(from: &buf), topLanes: FfiConverterSequenceUInt32.read(from: &buf),
bottomLanes: FfiConverterSequenceUInt32.read(from: &buf), bottomLanes: FfiConverterSequenceUInt32.read(from: &buf),
nodeLane: FfiConverterOptionUInt32.read(from: &buf), nodeLane: FfiConverterOptionUInt32.read(from: &buf),
@@ -1362,8 +1388,7 @@ public struct FfiConverterTypeCommitRow: FfiConverterRustBuffer {
public static func write(_ value: CommitRow, into buf: inout [UInt8]) { public static func write(_ value: CommitRow, into buf: inout [UInt8]) {
FfiConverterString.write(value.sha, into: &buf) FfiConverterString.write(value.sha, into: &buf)
FfiConverterString.write(value.title, into: &buf) FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.meta, into: &buf) FfiConverterString.write(value.detail, into: &buf)
FfiConverterString.write(value.refs, into: &buf)
FfiConverterSequenceUInt32.write(value.topLanes, into: &buf) FfiConverterSequenceUInt32.write(value.topLanes, into: &buf)
FfiConverterSequenceUInt32.write(value.bottomLanes, into: &buf) FfiConverterSequenceUInt32.write(value.bottomLanes, into: &buf)
FfiConverterOptionUInt32.write(value.nodeLane, into: &buf) FfiConverterOptionUInt32.write(value.nodeLane, into: &buf)
@@ -1562,20 +1587,14 @@ public func FfiConverterTypeFileRow_lower(_ value: FileRow) -> RustBuffer {
public struct HeatCell: Equatable, Hashable { public struct HeatCell: Equatable, Hashable {
public var week: UInt32
public var day: UInt32
public var level: UInt32 public var level: UInt32
public var timestamp: Int64 public var timestamp: Int64
public var contributions: Int64
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(week: UInt32, day: UInt32, level: UInt32, timestamp: Int64, contributions: Int64) { public init(level: UInt32, timestamp: Int64) {
self.week = week
self.day = day
self.level = level self.level = level
self.timestamp = timestamp self.timestamp = timestamp
self.contributions = contributions
} }
@@ -1594,20 +1613,14 @@ public struct FfiConverterTypeHeatCell: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> HeatCell { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> HeatCell {
return return
try HeatCell( try HeatCell(
week: FfiConverterUInt32.read(from: &buf),
day: FfiConverterUInt32.read(from: &buf),
level: FfiConverterUInt32.read(from: &buf), level: FfiConverterUInt32.read(from: &buf),
timestamp: FfiConverterInt64.read(from: &buf), timestamp: FfiConverterInt64.read(from: &buf)
contributions: FfiConverterInt64.read(from: &buf)
) )
} }
public static func write(_ value: HeatCell, into buf: inout [UInt8]) { public static func write(_ value: HeatCell, into buf: inout [UInt8]) {
FfiConverterUInt32.write(value.week, into: &buf)
FfiConverterUInt32.write(value.day, into: &buf)
FfiConverterUInt32.write(value.level, into: &buf) FfiConverterUInt32.write(value.level, into: &buf)
FfiConverterInt64.write(value.timestamp, into: &buf) FfiConverterInt64.write(value.timestamp, into: &buf)
FfiConverterInt64.write(value.contributions, into: &buf)
} }
} }
@@ -1630,14 +1643,18 @@ public func FfiConverterTypeHeatCell_lower(_ value: HeatCell) -> RustBuffer {
public struct HomePage: Equatable, Hashable { public struct HomePage: Equatable, Hashable {
public var serverName: String public var serverName: String
public var activities: [ActivityRow] public var activities: [ActivityRow]
public var issueActivities: [ActivityRow]
public var pullActivities: [ActivityRow]
public var heatCells: [HeatCell] public var heatCells: [HeatCell]
public var contributionCount: Int64 public var contributionCount: Int64
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(serverName: String, activities: [ActivityRow], heatCells: [HeatCell], contributionCount: Int64) { public init(serverName: String, activities: [ActivityRow], issueActivities: [ActivityRow], pullActivities: [ActivityRow], heatCells: [HeatCell], contributionCount: Int64) {
self.serverName = serverName self.serverName = serverName
self.activities = activities self.activities = activities
self.issueActivities = issueActivities
self.pullActivities = pullActivities
self.heatCells = heatCells self.heatCells = heatCells
self.contributionCount = contributionCount self.contributionCount = contributionCount
} }
@@ -1660,6 +1677,8 @@ public struct FfiConverterTypeHomePage: FfiConverterRustBuffer {
try HomePage( try HomePage(
serverName: FfiConverterString.read(from: &buf), serverName: FfiConverterString.read(from: &buf),
activities: FfiConverterSequenceTypeActivityRow.read(from: &buf), activities: FfiConverterSequenceTypeActivityRow.read(from: &buf),
issueActivities: FfiConverterSequenceTypeActivityRow.read(from: &buf),
pullActivities: FfiConverterSequenceTypeActivityRow.read(from: &buf),
heatCells: FfiConverterSequenceTypeHeatCell.read(from: &buf), heatCells: FfiConverterSequenceTypeHeatCell.read(from: &buf),
contributionCount: FfiConverterInt64.read(from: &buf) contributionCount: FfiConverterInt64.read(from: &buf)
) )
@@ -1668,6 +1687,8 @@ public struct FfiConverterTypeHomePage: FfiConverterRustBuffer {
public static func write(_ value: HomePage, into buf: inout [UInt8]) { public static func write(_ value: HomePage, into buf: inout [UInt8]) {
FfiConverterString.write(value.serverName, into: &buf) FfiConverterString.write(value.serverName, into: &buf)
FfiConverterSequenceTypeActivityRow.write(value.activities, into: &buf) FfiConverterSequenceTypeActivityRow.write(value.activities, into: &buf)
FfiConverterSequenceTypeActivityRow.write(value.issueActivities, into: &buf)
FfiConverterSequenceTypeActivityRow.write(value.pullActivities, into: &buf)
FfiConverterSequenceTypeHeatCell.write(value.heatCells, into: &buf) FfiConverterSequenceTypeHeatCell.write(value.heatCells, into: &buf)
FfiConverterInt64.write(value.contributionCount, into: &buf) FfiConverterInt64.write(value.contributionCount, into: &buf)
} }
@@ -1692,17 +1713,23 @@ public func FfiConverterTypeHomePage_lower(_ value: HomePage) -> RustBuffer {
public struct IssueFilterOptions: Equatable, Hashable { public struct IssueFilterOptions: Equatable, Hashable {
public var milestones: [String] public var milestones: [String]
public var labels: [String] public var labels: [String]
public var unavailableLabels: [String]
public var selectedMilestone: String public var selectedMilestone: String
public var selectedLabels: [String] public var selectedLabels: [String]
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(milestones: [String], labels: [String], selectedMilestone: String, selectedLabels: [String]) { public init(milestones: [String], labels: [String], unavailableLabels: [String], selectedMilestone: String, selectedLabels: [String]) {
self.milestones = milestones self.milestones = milestones
self.labels = labels self.labels = labels
self.unavailableLabels = unavailableLabels
self.selectedMilestone = selectedMilestone self.selectedMilestone = selectedMilestone
self.selectedLabels = selectedLabels self.selectedLabels = selectedLabels
} }
} }
#if compiler(>=6) #if compiler(>=6)
@@ -1718,6 +1745,7 @@ public struct FfiConverterTypeIssueFilterOptions: FfiConverterRustBuffer {
try IssueFilterOptions( try IssueFilterOptions(
milestones: FfiConverterSequenceString.read(from: &buf), milestones: FfiConverterSequenceString.read(from: &buf),
labels: FfiConverterSequenceString.read(from: &buf), labels: FfiConverterSequenceString.read(from: &buf),
unavailableLabels: FfiConverterSequenceString.read(from: &buf),
selectedMilestone: FfiConverterString.read(from: &buf), selectedMilestone: FfiConverterString.read(from: &buf),
selectedLabels: FfiConverterSequenceString.read(from: &buf) selectedLabels: FfiConverterSequenceString.read(from: &buf)
) )
@@ -1726,11 +1754,13 @@ public struct FfiConverterTypeIssueFilterOptions: FfiConverterRustBuffer {
public static func write(_ value: IssueFilterOptions, into buf: inout [UInt8]) { public static func write(_ value: IssueFilterOptions, into buf: inout [UInt8]) {
FfiConverterSequenceString.write(value.milestones, into: &buf) FfiConverterSequenceString.write(value.milestones, into: &buf)
FfiConverterSequenceString.write(value.labels, into: &buf) FfiConverterSequenceString.write(value.labels, into: &buf)
FfiConverterSequenceString.write(value.unavailableLabels, into: &buf)
FfiConverterString.write(value.selectedMilestone, into: &buf) FfiConverterString.write(value.selectedMilestone, into: &buf)
FfiConverterSequenceString.write(value.selectedLabels, into: &buf) FfiConverterSequenceString.write(value.selectedLabels, into: &buf)
} }
} }
#if swift(>=5.8) #if swift(>=5.8)
@_documentation(visibility: private) @_documentation(visibility: private)
#endif #endif
@@ -2003,18 +2033,20 @@ public struct MilestoneRow: Equatable, Hashable {
public var title: String public var title: String
public var description: String public var description: String
public var meta: String public var meta: String
public var openIssues: Int64 public var hasIssues: Bool
public var closedIssues: Int64 public var progress: Double
public var progressAccessibility: String
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(id: Int64, title: String, description: String, meta: String, openIssues: Int64, closedIssues: Int64) { public init(id: Int64, title: String, description: String, meta: String, hasIssues: Bool, progress: Double, progressAccessibility: String) {
self.id = id self.id = id
self.title = title self.title = title
self.description = description self.description = description
self.meta = meta self.meta = meta
self.openIssues = openIssues self.hasIssues = hasIssues
self.closedIssues = closedIssues self.progress = progress
self.progressAccessibility = progressAccessibility
} }
@@ -2037,8 +2069,9 @@ public struct FfiConverterTypeMilestoneRow: FfiConverterRustBuffer {
title: FfiConverterString.read(from: &buf), title: FfiConverterString.read(from: &buf),
description: FfiConverterString.read(from: &buf), description: FfiConverterString.read(from: &buf),
meta: FfiConverterString.read(from: &buf), meta: FfiConverterString.read(from: &buf),
openIssues: FfiConverterInt64.read(from: &buf), hasIssues: FfiConverterBool.read(from: &buf),
closedIssues: FfiConverterInt64.read(from: &buf) progress: FfiConverterDouble.read(from: &buf),
progressAccessibility: FfiConverterString.read(from: &buf)
) )
} }
@@ -2047,8 +2080,9 @@ public struct FfiConverterTypeMilestoneRow: FfiConverterRustBuffer {
FfiConverterString.write(value.title, into: &buf) FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.description, into: &buf) FfiConverterString.write(value.description, into: &buf)
FfiConverterString.write(value.meta, into: &buf) FfiConverterString.write(value.meta, into: &buf)
FfiConverterInt64.write(value.openIssues, into: &buf) FfiConverterBool.write(value.hasIssues, into: &buf)
FfiConverterInt64.write(value.closedIssues, into: &buf) FfiConverterDouble.write(value.progress, into: &buf)
FfiConverterString.write(value.progressAccessibility, into: &buf)
} }
} }
@@ -2270,6 +2304,72 @@ public func FfiConverterTypeRepositoryContentRow_lower(_ value: RepositoryConten
} }
public struct RepositoryFilePage: Equatable, Hashable {
public var name: String
public var kind: String
public var language: String
public var text: String
public var data: Data
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(name: String, kind: String, language: String, text: String, data: Data) {
self.name = name
self.kind = kind
self.language = language
self.text = text
self.data = data
}
}
#if compiler(>=6)
extension RepositoryFilePage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeRepositoryFilePage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> RepositoryFilePage {
return
try RepositoryFilePage(
name: FfiConverterString.read(from: &buf),
kind: FfiConverterString.read(from: &buf),
language: FfiConverterString.read(from: &buf),
text: FfiConverterString.read(from: &buf),
data: FfiConverterData.read(from: &buf)
)
}
public static func write(_ value: RepositoryFilePage, into buf: inout [UInt8]) {
FfiConverterString.write(value.name, into: &buf)
FfiConverterString.write(value.kind, into: &buf)
FfiConverterString.write(value.language, into: &buf)
FfiConverterString.write(value.text, into: &buf)
FfiConverterData.write(value.data, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeRepositoryFilePage_lift(_ buf: RustBuffer) throws -> RepositoryFilePage {
return try FfiConverterTypeRepositoryFilePage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeRepositoryFilePage_lower(_ value: RepositoryFilePage) -> RustBuffer {
return FfiConverterTypeRepositoryFilePage.lower(value)
}
public struct RepositoryRow: Equatable, Hashable { public struct RepositoryRow: Equatable, Hashable {
public var name: String public var name: String
public var owner: String public var owner: String
@@ -3035,6 +3135,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_issue_filters() != 24054) { if (uniffi_gotcha_core_checksum_method_gotchacore_issue_filters() != 24054) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_issue_filters_active() != 51470) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_issues() != 1316) { if (uniffi_gotcha_core_checksum_method_gotchacore_issues() != 1316) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
@@ -3059,7 +3162,7 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_repository_contents() != 8454) { if (uniffi_gotcha_core_checksum_method_gotchacore_repository_contents() != 8454) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_repository_file() != 27399) { if (uniffi_gotcha_core_checksum_method_gotchacore_repository_file() != 44948) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_select_server() != 22721) { if (uniffi_gotcha_core_checksum_method_gotchacore_select_server() != 22721) {
@@ -3109,4 +3212,4 @@ public func uniffiEnsureGotchaCoreInitialized() {
} }
} }
// swiftlint:enable all // swiftlint:enable all

View File

@@ -304,6 +304,11 @@ uint64_t uniffi_gotcha_core_fn_method_gotchacore_issue(uint64_t ptr, RustBuffer
uint64_t uniffi_gotcha_core_fn_method_gotchacore_issue_filters(uint64_t ptr, RustBuffer owner, RustBuffer repository uint64_t uniffi_gotcha_core_fn_method_gotchacore_issue_filters(uint64_t ptr, RustBuffer owner, RustBuffer repository
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE_FILTERS_ACTIVE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE_FILTERS_ACTIVE
int8_t uniffi_gotcha_core_fn_method_gotchacore_issue_filters_active(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUES #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUES #define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUES
uint64_t uniffi_gotcha_core_fn_method_gotchacore_issues(uint64_t ptr, RustBuffer owner, RustBuffer repository uint64_t uniffi_gotcha_core_fn_method_gotchacore_issues(uint64_t ptr, RustBuffer owner, RustBuffer repository
@@ -706,6 +711,12 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_issue(void
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUE_FILTERS #define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUE_FILTERS
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_issue_filters(void uint16_t uniffi_gotcha_core_checksum_method_gotchacore_issue_filters(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUE_FILTERS_ACTIVE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUE_FILTERS_ACTIVE
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_issue_filters_active(void
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUES #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUES

View File

@@ -135,13 +135,10 @@ final class IssuesViewController: RefreshingTableViewController {
private func milestoneMenu(_ options: IssueFilterOptions) -> UIMenu { private func milestoneMenu(_ options: IssueFilterOptions) -> UIMenu {
let selected = options.selectedMilestone let selected = options.selectedMilestone
var milestones = options.milestones
if !selected.isEmpty, !milestones.contains(selected) { milestones.append(selected) }
let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) { let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) {
[weak self] _ in self?.selectMilestone("") [weak self] _ in self?.selectMilestone("")
} }
let actions = milestones.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending } let actions = options.milestones.map { milestone in
.map { milestone in
UIAction( UIAction(
title: milestone, title: milestone,
state: selected == milestone ? .on : .off state: selected == milestone ? .on : .off
@@ -156,13 +153,10 @@ final class IssuesViewController: RefreshingTableViewController {
} }
private func labelMenu(_ options: IssueFilterOptions) -> UIMenu { private func labelMenu(_ options: IssueFilterOptions) -> UIMenu {
let available = Set(options.labels)
let selected = Set(options.selectedLabels) let selected = Set(options.selectedLabels)
let labels = available.union(selected) let actions = options.labels.map { label in
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
let actions = labels.map { label in
UIAction( UIAction(
title: available.contains(label) ? label : "\(label) (Unavailable)", title: options.unavailableLabels.contains(label) ? "\(label) (Unavailable)" : label,
attributes: .keepsMenuPresented, attributes: .keepsMenuPresented,
state: selected.contains(label) ? .on : .off state: selected.contains(label) ? .on : .off
) { [weak self] action in ) { [weak self] action in
@@ -172,7 +166,7 @@ final class IssuesViewController: RefreshingTableViewController {
if labels.remove(label) == nil { labels.insert(label) } if labels.remove(label) == nil { labels.insert(label) }
do { do {
try self.saveFilters(milestone: options.selectedMilestone, labels: labels) try self.saveFilters(milestone: options.selectedMilestone, labels: labels)
self.filterOptions?.selectedLabels = labels.sorted() self.filterOptions?.selectedLabels = Array(labels)
action.state = labels.contains(label) ? .on : .off action.state = labels.contains(label) ? .on : .off
self.updateFilterMenu() self.updateFilterMenu()
self.loadIssues(refreshing: false) self.loadIssues(refreshing: false)
@@ -208,14 +202,12 @@ final class IssuesViewController: RefreshingTableViewController {
owner: owner, owner: owner,
repository: repository, repository: repository,
milestone: milestone, milestone: milestone,
labels: labels.sorted() labels: Array(labels)
) )
} }
private var filtersActive: Bool { private var filtersActive: Bool {
context.core.settings().issueStatus != "open" (try? context.core.issueFiltersActive(owner: owner, repository: repository)) ?? false
|| filterOptions?.selectedMilestone.isEmpty == false
|| filterOptions?.selectedLabels.isEmpty == false
} }
private func updateFilterTint() { private func updateFilterTint() {
@@ -519,11 +511,10 @@ final class MilestoneCell: UITableViewCell {
titleLabel.text = row.title titleLabel.text = row.title
descriptionLabel.text = row.description descriptionLabel.text = row.description
metaLabel.text = row.meta metaLabel.text = row.meta
let total = row.openIssues + row.closedIssues progress.progress = Float(row.progress)
progress.progress = total == 0 ? 0 : Float(row.closedIssues) / Float(total) progress.trackTintColor = row.hasIssues ? .systemOrange : .systemGray5
progress.trackTintColor = total == 0 ? .systemGray5 : .systemOrange
progress.accessibilityLabel = "Milestone progress" progress.accessibilityLabel = "Milestone progress"
progress.accessibilityValue = "\(row.closedIssues) closed, \(row.openIssues) open" progress.accessibilityValue = row.progressAccessibility
} }
} }
@@ -599,7 +590,7 @@ final class PullsViewController: RefreshingTableViewController {
let row = rows[indexPath.row] let row = rows[indexPath.row]
configureTextCell( configureTextCell(
cell, cell,
title: "\(row.repository) #\(row.number)\n\(row.title)", title: row.title,
detail: "\(row.summary)\n\(row.meta)" detail: "\(row.summary)\n\(row.meta)"
) )
cell.accessoryType = .disclosureIndicator cell.accessoryType = .disclosureIndicator
@@ -819,9 +810,7 @@ final class CommitCell: UITableViewCell {
var content = defaultContentConfiguration() var content = defaultContentConfiguration()
content.directionalLayoutMargins.leading = laneCount == 0 ? 0 : min(72, CGFloat(laneCount) * 10 + 8) content.directionalLayoutMargins.leading = laneCount == 0 ? 0 : min(72, CGFloat(laneCount) * 10 + 8)
content.text = row.title content.text = row.title
content.secondaryText = row.refs.isEmpty content.secondaryText = row.detail
? "\(row.meta)\n\(row.sha.prefix(8))"
: "\(row.refs)\n\(row.meta) · \(row.sha.prefix(8))"
content.secondaryTextProperties.numberOfLines = 2 content.secondaryTextProperties.numberOfLines = 2
contentConfiguration = content contentConfiguration = content
setNeedsDisplay() setNeedsDisplay()

View File

@@ -3,7 +3,6 @@ import MarkdownUI
import QuickLook import QuickLook
import SwiftUI import SwiftUI
import UIKit import UIKit
import UniformTypeIdentifiers
@MainActor @MainActor
class MarkdownPageViewController: UIViewController { class MarkdownPageViewController: UIViewController {
@@ -273,13 +272,13 @@ final class RepositoryDirectoryViewController: RefreshingTableViewController {
private let path: String private let path: String
private var rows: [RepositoryContentRow] = [] private var rows: [RepositoryContentRow] = []
init(context: AppContext, owner: String, repository: String, path: String) { init(context: AppContext, owner: String, repository: String, path: String, name: String) {
self.context = context self.context = context
self.owner = owner self.owner = owner
self.repository = repository self.repository = repository
self.path = path self.path = path
super.init() super.init()
title = path.split(separator: "/").last.map(String.init) ?? repository title = name
} }
@available(*, unavailable) @available(*, unavailable)
@@ -349,18 +348,19 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
private var loadingTask: Task<Void, Never>? private var loadingTask: Task<Void, Never>?
private var previewURL: URL? private var previewURL: URL?
private var markdownSource: String? private var markdownSource: String?
private var fileLanguage: String?
private var displayedController: UIViewController? private var displayedController: UIViewController?
private var displayedView: UIView? private var displayedView: UIView?
private weak var sourceScrollView: UIScrollView? private weak var sourceScrollView: UIScrollView?
private weak var sourceTextView: UITextView? private weak var sourceTextView: UITextView?
init(context: AppContext, owner: String, repository: String, path: String) { init(context: AppContext, owner: String, repository: String, path: String, name: String) {
self.context = context self.context = context
self.owner = owner self.owner = owner
self.repository = repository self.repository = repository
self.path = path self.path = path
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
title = path.split(separator: "/").last.map(String.init) title = name
} }
@available(*, unavailable) @available(*, unavailable)
@@ -372,19 +372,17 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
beginNavigationLoading(spinner) beginNavigationLoading(spinner)
loadingTask = Task { loadingTask = Task {
do { do {
let data = try await context.core.repositoryFile( let page = try await context.core.repositoryFile(
owner: owner, owner: owner,
repository: repository, repository: repository,
path: path path: path
) )
if !isMediaFile(path), let source = String(data: data, encoding: .utf8) { title = page.name
if isMarkdownFile { fileLanguage = page.language.isEmpty ? nil : page.language
showMarkdown(source) switch page.kind {
} else { case "markdown": showMarkdown(page.text)
showSource(source) case "source": showSource(page.text)
} default: try showQuickLook(page.data, name: page.name)
} else {
try showQuickLook(data)
} }
} catch { } catch {
if !Task.isCancelled { show(error: error) } if !Task.isCancelled { show(error: error) }
@@ -398,20 +396,6 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
updateSourceLayout() updateSourceLayout()
} }
private func isMediaFile(_ path: String) -> Bool {
guard let type = UTType(filenameExtension: (path as NSString).pathExtension) else {
return false
}
return type.conforms(to: .image)
|| type.conforms(to: .audio)
|| type.conforms(to: .audiovisualContent)
|| type.conforms(to: .pdf)
}
private var isMarkdownFile: Bool {
["md", "markdown", "mdown", "mkd"].contains((path as NSString).pathExtension.lowercased())
}
deinit { deinit {
loadingTask?.cancel() loadingTask?.cancel()
if let previewURL { try? FileManager.default.removeItem(at: previewURL) } if let previewURL { try? FileManager.default.removeItem(at: previewURL) }
@@ -464,7 +448,7 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
traitCollection.userInterfaceStyle == .dark ? "atom-one-dark" : "atom-one-light" traitCollection.userInterfaceStyle == .dark ? "atom-one-dark" : "atom-one-light"
) )
highlighter?.theme.setCodeFont(.monospacedSystemFont(ofSize: 13, weight: .regular)) highlighter?.theme.setCodeFont(.monospacedSystemFont(ofSize: 13, weight: .regular))
let highlighted = highlighter?.highlight(source, as: sourceLanguage(path)) let highlighted = highlighter?.highlight(source, as: fileLanguage)
?? NSAttributedString(string: source, attributes: [ ?? NSAttributedString(string: source, attributes: [
.font: UIFont.monospacedSystemFont(ofSize: 13, weight: .regular), .font: UIFont.monospacedSystemFont(ofSize: 13, weight: .regular),
.foregroundColor: UIColor.label, .foregroundColor: UIColor.label,
@@ -532,8 +516,7 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
scrollView.contentSize = contentSize scrollView.contentSize = contentSize
} }
private func showQuickLook(_ data: Data) throws { private func showQuickLook(_ data: Data, name: String) throws {
let name = path.split(separator: "/").last.map(String.init) ?? "Preview"
let url = FileManager.default.temporaryDirectory let url = FileManager.default.temporaryDirectory
.appendingPathComponent("\(UUID().uuidString)-\(name)") .appendingPathComponent("\(UUID().uuidString)-\(name)")
try data.write(to: url, options: .atomic) try data.write(to: url, options: .atomic)

View File

@@ -303,17 +303,6 @@ final class HomeViewController: RefreshingTableViewController {
case all case all
case issues case issues
case pulls case pulls
func includes(_ row: ActivityRow) -> Bool {
switch self {
case .all:
return true
case .issues:
return row.icon.contains("issue")
case .pulls:
return row.icon.contains("pull")
}
}
} }
private let context: AppContext private let context: AppContext
@@ -321,7 +310,12 @@ final class HomeViewController: RefreshingTableViewController {
private var filter = ActivityFilter.all private var filter = ActivityFilter.all
private var activities: [ActivityRow] { private var activities: [ActivityRow] {
page?.activities.filter(filter.includes) ?? [] guard let page else { return [] }
switch filter {
case .all: return page.activities
case .issues: return page.issueActivities
case .pulls: return page.pullActivities
}
} }
init(context: AppContext) { init(context: AppContext) {
@@ -443,20 +437,7 @@ final class HeatmapView: UIView {
private let calendar = Calendar(identifier: .gregorian) private let calendar = Calendar(identifier: .gregorian)
init(page: HomePage, selectedFilter: Int, onFilter: @escaping (Int) -> Void) { init(page: HomePage, selectedFilter: Int, onFilter: @escaping (Int) -> Void) {
let latest = page.heatCells.last.map { cells = page.heatCells
Date(timeIntervalSince1970: TimeInterval($0.timestamp))
} ?? Date()
let latestMonth = Calendar(identifier: .gregorian).date(
from: Calendar(identifier: .gregorian).dateComponents([.year, .month], from: latest)
) ?? latest
let firstMonth = Calendar(identifier: .gregorian).date(
byAdding: .month,
value: -8,
to: latestMonth
) ?? latestMonth
cells = page.heatCells.filter {
Date(timeIntervalSince1970: TimeInterval($0.timestamp)) >= firstMonth
}
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 180)) super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 180))
backgroundColor = .systemBackground backgroundColor = .systemBackground
let title = UILabel(frame: CGRect(x: 16, y: 12, width: 300, height: 22)) let title = UILabel(frame: CGRect(x: 16, y: 12, width: 300, height: 22))
@@ -465,7 +446,7 @@ final class HeatmapView: UIView {
title.textColor = .secondaryLabel title.textColor = .secondaryLabel
addSubview(title) addSubview(title)
let total = UILabel(frame: CGRect(x: 16, y: 108, width: 300, height: 18)) let total = UILabel(frame: CGRect(x: 16, y: 108, width: 300, height: 18))
total.text = "\(cells.reduce(0) { $0 + $1.contributions }) contributions" total.text = "\(page.contributionCount) contributions"
total.font = .preferredFont(forTextStyle: .caption1) total.font = .preferredFont(forTextStyle: .caption1)
total.textColor = .tertiaryLabel total.textColor = .tertiaryLabel
addSubview(total) addSubview(total)

View File

@@ -151,39 +151,19 @@ func showRepositoryContent(
context: context, context: context,
owner: owner, owner: owner,
repository: repository, repository: repository,
path: row.path path: row.path,
name: row.name
) )
: RepositoryFileViewController( : RepositoryFileViewController(
context: context, context: context,
owner: owner, owner: owner,
repository: repository, repository: repository,
path: row.path path: row.path,
name: row.name
) )
navigationController?.pushViewController(destination, animated: true) navigationController?.pushViewController(destination, animated: true)
} }
func sourceLanguage(_ path: String) -> String? {
let filename = (path as NSString).lastPathComponent.lowercased()
let filenames = [
"cmakelists.txt": "cmake", "dockerfile": "dockerfile", "gemfile": "ruby",
"makefile": "makefile", "podfile": "ruby",
]
if let language = filenames[filename] { return language }
let languages = [
"asm": "x86asm", "c": "c", "cc": "cpp", "clj": "clojure", "cpp": "cpp",
"cs": "csharp", "css": "css", "dart": "dart", "ex": "elixir", "exs": "elixir",
"fs": "fsharp", "go": "go", "groovy": "groovy", "h": "cpp", "hpp": "cpp",
"hs": "haskell", "html": "xml", "java": "java", "jl": "julia", "js": "javascript",
"json": "json", "jsx": "javascript", "kt": "kotlin", "kts": "kotlin", "lua": "lua",
"m": "objectivec", "md": "markdown", "mm": "objectivec", "php": "php", "pl": "perl",
"plist": "xml", "ps1": "powershell", "py": "python", "r": "r", "rb": "ruby",
"rs": "rust", "scala": "scala", "scss": "scss", "sh": "bash", "sql": "sql",
"swift": "swift", "toml": "ini", "ts": "typescript", "tsx": "typescript",
"txt": "plaintext", "xml": "xml", "yaml": "yaml", "yml": "yaml",
]
return languages[(path as NSString).pathExtension.lowercased()]
}
func symbolText(_ symbol: String, text: String, font: UIFont, color: UIColor) -> NSAttributedString { func symbolText(_ symbol: String, text: String, font: UIFont, color: UIColor) -> NSAttributedString {
let output = NSMutableAttributedString() let output = NSMutableAttributedString()
if let image = UIImage(systemName: symbol)?.withTintColor(color, renderingMode: .alwaysOriginal) { if let image = UIImage(systemName: symbol)?.withTintColor(color, renderingMode: .alwaysOriginal) {