Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ jobs:

- run: cargo llvm-cov --no-report --workspace
- run: cargo llvm-cov --no-report run -p cp-mac --example mac_probe
- run: cargo llvm-cov report --lcov --output-path lcov-rust.info --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure|reaching)\.rs|_test\.rs$|src-tauri[/\]src[/\]'
- run: cargo llvm-cov report --lcov --output-path lcov-rust.info --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure|reaching|showing)\.rs|_test\.rs$|src-tauri[/\]src[/\]'

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
Expand Down Expand Up @@ -193,7 +193,7 @@ jobs:
retention-days: 1
- name: The threshold is asked after the measuring, so a dip still ships the report
if: always()
run: cargo llvm-cov report --fail-under-lines 85 --summary-only --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure|reaching)\.rs|_test\.rs$|src-tauri[/\]src[/\]'
run: cargo llvm-cov report --fail-under-lines 85 --summary-only --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure|reaching|showing)\.rs|_test\.rs$|src-tauri[/\]src[/\]'

coverage-windows:
name: coverage / the Windows crates
Expand All @@ -220,7 +220,7 @@ jobs:
shell: pwsh
run: Set-Clipboard -Value "cp-probe"
- run: cargo llvm-cov --no-report run -p cp-win --example win_probe
- run: cargo llvm-cov report --lcov --output-path lcov-win.info --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure|reaching)\.rs|_test\.rs$|src-tauri[/\]src[/\]'
- run: cargo llvm-cov report --lcov --output-path lcov-win.info --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure|reaching|showing)\.rs|_test\.rs$|src-tauri[/\]src[/\]'
- name: Every path is relative to the repository, which is what Sonar reads
if: always()
shell: bash
Expand All @@ -234,7 +234,7 @@ jobs:
retention-days: 1
- name: The threshold is asked after the measuring, so a dip still ships the report
if: always()
run: cargo llvm-cov report --fail-under-lines 85 --summary-only --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure|reaching)\.rs|_test\.rs$|src-tauri[/\]src[/\]'
run: cargo llvm-cov report --fail-under-lines 85 --summary-only --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure|reaching|showing)\.rs|_test\.rs$|src-tauri[/\]src[/\]'

sonar:
name: sonarcloud
Expand Down Expand Up @@ -285,6 +285,6 @@ jobs:
-Dsonar.javascript.lcov.reportPaths=app/coverage/lcov.info
-Dsonar.rust.clippy.enabled=false
-Dsonar.cpd.exclusions=app/src/locales.ts
-Dsonar.coverage.exclusions=app/src/main.tsx,app/src-tauri/src/**,crates/cp-panel/src/main.rs,crates/cp-panel/src/app.rs,crates/cp-panel/src/measure.rs,crates/cp-panel/src/reaching.rs
-Dsonar.coverage.exclusions=app/src/main.tsx,app/src-tauri/src/**,crates/cp-panel/src/main.rs,crates/cp-panel/src/app.rs,crates/cp-panel/src/showing.rs,crates/cp-panel/src/measure.rs,crates/cp-panel/src/reaching.rs
-Dsonar.qualitygate.wait=${{ github.event_name == 'pull_request' }}
-Dsonar.qualitygate.timeout=300
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ windows = { version = "0.62", features = [
"Win32_System_Threading",
"Win32_UI_WindowsAndMessaging",
"Win32_Graphics_Gdi",
"Win32_UI_HiDpi",
"Win32_Graphics_Dwm",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_Security",
Expand Down
2 changes: 1 addition & 1 deletion PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ CopyPaste makes **one type of network request** for update checking:
- Only when you press «Update» does the app also read `latest.json` (or `candidate.json` for test versions) from the same branch, which says where the installer lives and carries its signature
- The installer is **cryptographically signed**, and the signature is verified against a public key built into the app before anything is installed. The download address is also checked against our own release hosts before a single byte is fetched
- **Microsoft Store version:** nothing is checked and nothing is offered. The Store delivers its own updates
- **Homebrew** (`copypaste`, or `copypaste-beta` for test versions): you are told the `brew upgrade` command for your cask. Nothing is downloaded or installed behind Homebrew's back
- **Homebrew** (`copypaste`, or `copypaste-beta` for test versions): the same as a standalone build, «Update» downloads and installs it. The cask tells Homebrew that CopyPaste updates itself, so `brew upgrade --cask` keeps working too
- **Standalone builds (Windows / macOS):** if you press «Update», and only then, the installer is downloaded and run, and CopyPaste restarts itself. Nothing is downloaded or installed without you asking for it

### User-Initiated Browser Navigation
Expand Down
7 changes: 4 additions & 3 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ copy that only knew that channel is not left behind.

What it does with an offer depends on where the copy came from, which it reads
off its own path: a copy under `WindowsApps` is the Store's to update and is
never offered anything; one under `Caskroom/copypaste-beta` or
`Caskroom/copypaste` is told its own `brew` command rather than handed an
installer; anything else installs its own update.
never offered anything; anything else, a Homebrew copy included, installs its
own update with one click, as Tisty does. The casks say `auto_updates true`, so
Homebrew expects that; its own `brew` command is shown only when the copy cannot
replace itself, such as when it runs from the mounted `.dmg`.

Four things guard the install, all of them borrowed from Tisty: the download
address must be this repository's releases on `github.com` (or
Expand Down
2 changes: 1 addition & 1 deletion app/src-tauri/src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ struct Kept {
}

pub const fn self_installs(route: Route) -> bool {
matches!(route, Route::Download)
matches!(route, Route::Brew | Route::BrewBeta | Route::Download)
}

pub fn ours(url: &str) -> bool {
Expand Down
7 changes: 5 additions & 2 deletions app/src-tauri/src/update_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ fn the_cask_moves_the_bundle_out_of_the_caskroom_and_the_receipt_still_tells() {
Route::Download,
"no receipt, no brew"
);
assert!(!self_installs(Route::Brew));
assert!(
self_installs(Route::Brew),
"the cask says auto_updates, so a brewed copy replaces itself like Tisty does"
);
}

#[test]
Expand Down Expand Up @@ -112,7 +115,7 @@ fn a_copy_from_the_beta_cask_is_told_its_own_command() {
if cfg!(target_os = "macos") {
assert_eq!(chosen(Some(at), beta), Route::BrewBeta);
}
assert!(!self_installs(Route::BrewBeta));
assert!(self_installs(Route::BrewBeta));
}

#[test]
Expand Down
8 changes: 4 additions & 4 deletions app/src/ui/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ function Newer() {
: t("updateLook");

const why = ready
? brew
? t(seen?.route === "brewBeta" ? "updateBrewBeta" : "updateBrew")
: ready.installs
? t("updateTake")
? ready.installs
? t("updateTake")
: brew
? t(seen?.route === "brewBeta" ? "updateBrewBeta" : "updateBrew")
: t("updateMove")
: t("updateWhen");

Expand Down
1 change: 1 addition & 0 deletions crates/cp-mac-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod ocr;
pub mod pasteboard;
pub mod paths;
pub mod permissions;
pub mod pointer;
pub mod processes;
pub mod runloop;
pub mod theme;
Expand Down
46 changes: 46 additions & 0 deletions crates/cp-mac-sys/src/pointer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use objc2_app_kit::{NSEvent, NSScreen};
use objc2_foundation::{MainThreadMarker, NSPoint, NSRect};

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Spot {
pub x: f64,
pub y: f64,
pub left: f64,
pub top: f64,
pub right: f64,
pub bottom: f64,
}

pub fn spot() -> Option<Spot> {
let mtm = MainThreadMarker::new()?;
let at = NSEvent::mouseLocation();
let screens = NSScreen::screens(mtm);
let primary = screens.firstObject()?.frame();
let under = screens
.iter()
.find(|screen| holds(screen.frame(), at))
.or_else(|| NSScreen::mainScreen(mtm))?;
Some(flipped(at, under.visibleFrame(), primary.size.height))
}

fn holds(frame: NSRect, at: NSPoint) -> bool {
at.x >= frame.origin.x
&& at.x <= frame.origin.x + frame.size.width
&& at.y >= frame.origin.y
&& at.y <= frame.origin.y + frame.size.height
}

pub fn flipped(at: NSPoint, visible: NSRect, primary_height: f64) -> Spot {
Spot {
x: at.x,
y: primary_height - at.y,
left: visible.origin.x,
top: primary_height - (visible.origin.y + visible.size.height),
right: visible.origin.x + visible.size.width,
bottom: primary_height - visible.origin.y,
}
}

#[cfg(test)]
#[path = "pointer_test.rs"]
mod tests;
35 changes: 35 additions & 0 deletions crates/cp-mac-sys/src/pointer_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use super::*;
use objc2_foundation::NSSize;

#[test]
fn appkit_counts_from_the_bottom_and_the_panel_from_the_top() {
let visible = NSRect::new(NSPoint::new(0.0, 80.0), NSSize::new(1440.0, 795.0));
let spot = flipped(NSPoint::new(700.0, 600.0), visible, 900.0);
assert_eq!(
spot,
Spot {
x: 700.0,
y: 300.0,
left: 0.0,
top: 25.0,
right: 1440.0,
bottom: 820.0,
}
);
}

#[test]
fn a_screen_above_the_main_one_has_negative_tops() {
let visible = NSRect::new(NSPoint::new(0.0, 900.0), NSSize::new(1920.0, 1055.0));
let spot = flipped(NSPoint::new(100.0, 1500.0), visible, 900.0);
assert_eq!(spot.y, -600.0);
assert_eq!(spot.top, -1055.0);
assert_eq!(spot.bottom, 0.0);
}

#[test]
fn off_the_main_thread_there_is_no_spot() {
if MainThreadMarker::new().is_none() {
assert_eq!(spot(), None);
}
}
12 changes: 12 additions & 0 deletions crates/cp-mac/examples/probe/panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,16 @@ fn the_panel_and_its_windows(b: &mut Battery) {
Ok(())
},
);

b.case(
"S6",
"the pointer sits on a screen whose visible part holds it",
|| {
let spot = cp_mac_sys::pointer::spot().ok_or("this needs the main thread and a screen")?;
if spot.right <= spot.left || spot.bottom <= spot.top {
return Err(format!("the visible part of the screen is empty: {spot:?}"));
}
Ok(())
},
);
}
46 changes: 6 additions & 40 deletions crates/cp-panel/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::model::{Metrics, Rows, reveal};
use crate::note::note;
use crate::opening::Reached;
use crate::reaching::reach_for;
use crate::showing::{NEXT_FRAME, appear, place, vanish};
use crate::view::{AS_IS, as_is_label, label_of_form, shorthand_of};
use crate::view::{chips_of, compact, count_text, empty_of, form_of, harvest, label_of, sweeten};
use crate::{Chip, FormRow, Options, Panel};
Expand All @@ -18,8 +19,6 @@ use std::time::{Duration, Instant};

const CHIP_STEP: f32 = 78.0;
const KEPT_IN_VIEW: usize = 2;
const OUT: Duration = Duration::from_millis(130);
const NEXT_FRAME: Duration = Duration::from_millis(16);
const SETTLES: Duration = Duration::from_millis(70);
const RESTS: Duration = Duration::from_millis(110);
const LOOKS: Duration = Duration::from_millis(250);
Expand Down Expand Up @@ -245,12 +244,9 @@ impl App {
});
let ui = self.ui.clone();
panel.on_nudge(move |dx, dy| {
let Some(ui) = ui.upgrade() else {
return;
};
let window = ui.window();
let at = window.position().to_logical(window.scale_factor());
window.set_position(slint::LogicalPosition::new(at.x + dx, at.y + dy));
if let Some(ui) = ui.upgrade() {
crate::showing::nudge(&ui, dx, dy);
}
});
}

Expand Down Expand Up @@ -1116,38 +1112,6 @@ fn arm_sheet(state: &Rc<RefCell<State>>, ui: &Panel) {
});
}

thread_local! {
static CURTAIN: slint::Timer = slint::Timer::default();
}

fn appear(ui: &Panel) {
crate::note::tell("shown");
ui.set_shown(0.0);
let weak = ui.as_weak();
CURTAIN.with(|timer| {
timer.stop();
timer.start(slint::TimerMode::SingleShot, NEXT_FRAME, move || {
if let Some(ui) = weak.upgrade() {
ui.set_shown(1.0);
}
});
});
}

fn vanish(ui: &Panel) {
ui.set_sheet_open(false);
ui.set_shown(0.0);
let weak = ui.as_weak();
CURTAIN.with(|timer| {
timer.stop();
timer.start(slint::TimerMode::SingleShot, OUT, move || {
if let Some(ui) = weak.upgrade() {
let _ = ui.hide();
}
});
});
}

fn busy() -> &'static str {
if crate::here::read_stuck() {
return crate::say::pick(
Expand Down Expand Up @@ -1440,9 +1404,11 @@ fn listen(ui: slint::Weak<Panel>, ahead: Arc<AtomicIsize>, backdrop: String) {
crate::say::adopt_what_was_kept();
let dressed = backdrop.clone();
let _ = ui.upgrade_in_event_loop(move |panel| {
place(&panel);
if panel.show().is_err() {
return;
}
place(&panel);
crate::view::dress_words(&panel);
dress(&panel, &dressed);
forward(&panel);
Expand Down
40 changes: 39 additions & 1 deletion crates/cp-panel/src/here.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@
pub use platform::{
Dragged, THUMBNAILS_FILES, Watching, ahead_now, capture_insisting, content_of, data_dir,
drag_out, dress, forward, ground, in_front, media_of, ocr_available, open_link, open_path,
ours_up_front, paste_into, read_stuck, stay_out_of_the_dock, system_is_light, text_in,
ours_up_front, paste_into, pointer, read_stuck, stay_out_of_the_dock, system_is_light, text_in,
thumb_of_file, thumbs_dir, to_clipboard, towards, utc_offset_at, watch_start,
};

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Pointer {
pub at: (f64, f64),
pub area: crate::placing::Area,
pub physical: bool,
pub scale: f64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Landed {
Nothing,
Expand Down Expand Up @@ -195,6 +203,21 @@
}

pub fn ground(_handle: RawWindowHandle) {}

pub fn pointer() -> Option<super::Pointer> {
let spot = cp_win_sys::pointer::spot()?;
Some(super::Pointer {
at: (f64::from(spot.x), f64::from(spot.y)),
area: crate::placing::Area {
left: f64::from(spot.left),
top: f64::from(spot.top),
right: f64::from(spot.right),
bottom: f64::from(spot.bottom),
},
physical: true,
scale: spot.scale,
})

Check warning on line 219 in crates/cp-panel/src/here.rs

View workflow job for this annotation

GitHub Actions / windows 1 of 2 / the lines this branch changed

Missed mutant

replace platform::pointer -> Option<super::Pointer> with None
}
}

#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -387,6 +410,21 @@
}
}

pub fn pointer() -> Option<super::Pointer> {
let spot = cp_mac_sys::pointer::spot()?;
Some(super::Pointer {
at: (spot.x, spot.y),
area: crate::placing::Area {
left: spot.left,
top: spot.top,
right: spot.right,
bottom: spot.bottom,
},
physical: false,
scale: 1.0,
})

Check warning on line 425 in crates/cp-panel/src/here.rs

View workflow job for this annotation

GitHub Actions / windows 1 of 2 / the lines this branch changed

Missed mutant

replace platform::pointer -> Option<super::Pointer> with Some(Default::default())

Check warning on line 425 in crates/cp-panel/src/here.rs

View workflow job for this annotation

GitHub Actions / windows 1 of 2 / the lines this branch changed

Missed mutant

replace platform::pointer -> Option<super::Pointer> with None
}

pub fn ground(handle: RawWindowHandle) {
if let RawWindowHandle::AppKit(appkit) = handle {
cp_mac_sys::floating::grounded(appkit.ns_view);
Expand Down
13 changes: 13 additions & 0 deletions crates/cp-panel/src/here_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,16 @@ fn a_paste_that_fails_tells_the_panel_why_exactly_once() {
);
assert_eq!(asked.get(), 3, "once it is done nothing is asked again");
}

#[test]
fn the_pointer_lands_on_a_work_area_that_holds_it() {
let Some(pointer) = pointer() else {
return;
};
let area = pointer.area;
assert!(
area.right > area.left && area.bottom > area.top,
"{pointer:?}"
);
assert!(pointer.scale >= 1.0, "{pointer:?}");
}
Loading
Loading