mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-03 00:01:55 +00:00
feat(mobile): files, changes, a custom key bar, and a steadier connection (#1029)
The phone can now hand a pane a photo or file, read what an agent changed, answer its prompts with a tap, and keep its link across leaving a pane. Gateway and protocol: - `Open::Upload` puts a file (up to 20 MB) in the directory the desktop keeps pasted images in, under a name a shell takes unquoted, and answers with its path. Panes on this machine only for now. - `Open::Diff` answers with the working tree's changes against HEAD and its untracked files, for the directory a pane is in. - Both are one-shot streams; an older gateway drops them unanswered, which the client reports as "update tty7". App: - Attach button in the message box: the uploaded path goes into the draft. - Changes sheet from a pane's menu: a block per file, lines coloured. - Settings → Key bar: remove, reorder and add keys, from a catalog or written out (`/compact\r`, `^C`), across pages; reset to the default. - An agent's numbered choices show as buttons while it waits. - Pinch to zoom, tap a link to open it, find in the scrollback, a Copy button for text a program copies (OSC 52), and an optional Face ID lock. - Swipe from the left edge to go back; the iOS WebView has no page stack. - One workspace at a time on a machine, picked from a row of chips. - Agent avatars in their own colours. - Each screen's watch is its own: a second watch no longer ends the first, which showed as "Can't reach" and a reconnect after leaving a pane. - Dialing no longer holds the session lock, so Forget works while a machine is still connecting; its confirmation is an in-app sheet, since `window.confirm` shows nothing in the iOS WebView. - iOS 15 minimum, as App Store Connect will require from April 2027. - `scripts/testflight.sh` archives, signs for the App Store at export (the team has no devices for a development profile) and uploads. Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -20,8 +20,9 @@ use tty7_core::core::machine::Machine;
|
||||
use tty7_core::daemon::control::PaneAgentState;
|
||||
use tty7_core::daemon::protocol::{DaemonMsg, LeaseRequest, WinSize};
|
||||
use tty7_mobile_proto::{
|
||||
ControlEvent, ControlRequest, Frame, GridSize, Open, OpenReply, PROTOCOL_VERSION, PaneEvent,
|
||||
PaneRequest, RemoteView, TabCreated, Tree, read_frame, write_bytes, write_msg,
|
||||
ControlEvent, ControlRequest, Diff, Frame, GridSize, MAX_DIFF, MAX_UPLOAD, Open, OpenReply,
|
||||
PROTOCOL_VERSION, PaneEvent, PaneRequest, RemoteView, TabCreated, Tree, Uploaded, read_frame,
|
||||
write_bytes, write_msg,
|
||||
};
|
||||
|
||||
use crate::state::State;
|
||||
@@ -202,6 +203,56 @@ async fn serve_stream(
|
||||
finish(send).await;
|
||||
Ok(())
|
||||
}
|
||||
Open::Diff { cwd, machine } => {
|
||||
let read = if machine.is_some() {
|
||||
Err(io::Error::other(
|
||||
"changes can only be read on this computer for now",
|
||||
))
|
||||
} else {
|
||||
tokio::task::spawn_blocking(move || read_diff(&cwd))
|
||||
.await
|
||||
.map_err(io::Error::other)?
|
||||
};
|
||||
match read {
|
||||
Ok(diff) => {
|
||||
write_msg(&mut send, &ok).await?;
|
||||
write_msg(&mut send, &diff).await?;
|
||||
}
|
||||
Err(e) => write_msg(&mut send, &denied(&e.to_string())).await?,
|
||||
}
|
||||
finish(send).await;
|
||||
Ok(())
|
||||
}
|
||||
Open::Upload {
|
||||
name,
|
||||
size,
|
||||
machine,
|
||||
} => {
|
||||
let refusal = if machine.is_some() {
|
||||
Some("files can only go to panes on this computer for now".to_string())
|
||||
} else if size > MAX_UPLOAD {
|
||||
Some(format!(
|
||||
"that file is {} MB; files up to {} MB can be sent",
|
||||
size.div_ceil(1 << 20),
|
||||
MAX_UPLOAD >> 20
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(reason) = refusal {
|
||||
write_msg(&mut send, &denied(&reason)).await?;
|
||||
finish(send).await;
|
||||
return Ok(());
|
||||
}
|
||||
write_msg(&mut send, &ok).await?;
|
||||
// A failure after `Ok` is said in the place of the answer.
|
||||
match receive_upload(&name, size, &mut recv).await {
|
||||
Ok(path) => write_msg(&mut send, &Uploaded { path }).await?,
|
||||
Err(e) => write_msg(&mut send, &denied(&e.to_string())).await?,
|
||||
}
|
||||
finish(send).await;
|
||||
Ok(())
|
||||
}
|
||||
Open::Pane { pane_id, machine } => {
|
||||
let feed = {
|
||||
let (backend, machine) = (backend.clone(), machine.clone());
|
||||
@@ -230,6 +281,126 @@ async fn serve_stream(
|
||||
}
|
||||
}
|
||||
|
||||
/// The changes in the git working tree `cwd` is in, against its last commit
|
||||
/// (against nothing, in a repository with no commits yet).
|
||||
fn read_diff(cwd: &str) -> io::Result<Diff> {
|
||||
let git = |args: &[&str]| -> io::Result<std::process::Output> {
|
||||
std::process::Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(cwd)
|
||||
.args(args)
|
||||
.stdin(std::process::Stdio::null())
|
||||
.output()
|
||||
};
|
||||
let top = git(&["rev-parse", "--show-toplevel"])?;
|
||||
if !top.status.success() {
|
||||
return Err(io::Error::other(format!(
|
||||
"{cwd} is not in a git repository"
|
||||
)));
|
||||
}
|
||||
let root = String::from_utf8_lossy(&top.stdout).trim().to_string();
|
||||
let args = ["--no-pager", "diff", "--no-color", "--no-ext-diff", "-M"];
|
||||
let mut out = git(&[&args[..], &["HEAD"]].concat())?;
|
||||
if !out.status.success() {
|
||||
// No commit yet: what is staged is all there is to show.
|
||||
out = git(&[&args[..], &["--cached"]].concat())?;
|
||||
}
|
||||
if !out.status.success() {
|
||||
return Err(io::Error::other(
|
||||
String::from_utf8_lossy(&out.stderr).trim().to_string(),
|
||||
));
|
||||
}
|
||||
let truncated = out.stdout.len() > MAX_DIFF;
|
||||
let mut patch = out.stdout;
|
||||
patch.truncate(MAX_DIFF);
|
||||
let untracked = git(&[
|
||||
"ls-files",
|
||||
"--others",
|
||||
"--exclude-standard",
|
||||
"--full-name",
|
||||
":/",
|
||||
])?;
|
||||
Ok(Diff {
|
||||
root,
|
||||
patch: String::from_utf8_lossy(&patch).into_owned(),
|
||||
untracked: String::from_utf8_lossy(&untracked.stdout)
|
||||
.lines()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
truncated,
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads a file the phone sends and writes it where the desktop keeps pasted
|
||||
/// images, in a directory of its own so its name can stay as it was.
|
||||
async fn receive_upload(name: &str, size: u64, recv: &mut RecvStream) -> io::Result<String> {
|
||||
let mut bytes = Vec::with_capacity(usize::try_from(size).unwrap_or(0));
|
||||
while let Some(frame) = read_frame(recv).await? {
|
||||
match frame {
|
||||
Frame::Bytes(chunk) => bytes.extend_from_slice(&chunk),
|
||||
Frame::Json(_) => {
|
||||
return Err(io::Error::other("the phone sent a message, not the file"));
|
||||
}
|
||||
}
|
||||
if bytes.len() as u64 > size {
|
||||
return Err(io::Error::other("the file was longer than the phone said"));
|
||||
}
|
||||
}
|
||||
if bytes.len() as u64 != size {
|
||||
return Err(io::Error::other("the file stopped partway; send it again"));
|
||||
}
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_nanos());
|
||||
let dir = std::env::temp_dir()
|
||||
.join("tty7-clipboard")
|
||||
.join(format!("phone-{nanos:x}"));
|
||||
let path = dir.join(upload_file_name(name));
|
||||
let written = path.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
std::fs::write(&written, bytes)
|
||||
})
|
||||
.await
|
||||
.map_err(io::Error::other)??;
|
||||
Ok(path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// A name that is safe to write and to type into a shell unquoted: the last
|
||||
/// path component, letters (any script), digits and `.-_` kept, the rest
|
||||
/// made `_`, and no leading dot to hide it.
|
||||
fn upload_file_name(name: &str) -> String {
|
||||
let base = name.rsplit(['/', '\\']).next().unwrap_or("");
|
||||
let safe: String = base
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_alphanumeric() || matches!(c, '.' | '-' | '_') {
|
||||
c
|
||||
} else {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let safe = safe.trim_start_matches('.');
|
||||
if safe.is_empty() {
|
||||
return "file".into();
|
||||
}
|
||||
// Long names are cut from the middle of the stem; the extension is what
|
||||
// tells an agent what the file is.
|
||||
let chars: Vec<char> = safe.chars().collect();
|
||||
if chars.len() <= 100 {
|
||||
return safe.to_string();
|
||||
}
|
||||
let ext = safe.rfind('.').map_or("", |i| &safe[i..]);
|
||||
let ext: String = if ext.chars().count() <= 10 {
|
||||
ext.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let stem: String = chars[..100 - ext.chars().count()].iter().collect();
|
||||
format!("{stem}{ext}")
|
||||
}
|
||||
|
||||
fn denied(reason: &str) -> OpenReply {
|
||||
OpenReply::Denied {
|
||||
reason: reason.to_string(),
|
||||
@@ -557,6 +728,20 @@ fn short(id: &str) -> &str {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn an_upload_keeps_a_name_a_shell_can_take() {
|
||||
assert_eq!(upload_file_name("IMG_0042.HEIC"), "IMG_0042.HEIC");
|
||||
assert_eq!(upload_file_name("截图 2026.png"), "截图_2026.png");
|
||||
assert_eq!(upload_file_name("../../etc/passwd"), "passwd");
|
||||
assert_eq!(upload_file_name("a$(rm -rf ~).txt"), "a__rm_-rf___.txt");
|
||||
assert_eq!(upload_file_name(".env"), "env");
|
||||
assert_eq!(upload_file_name(""), "file");
|
||||
let long = format!("{}.png", "x".repeat(300));
|
||||
let cut = upload_file_name(&long);
|
||||
assert_eq!(cut.chars().count(), 100);
|
||||
assert!(cut.ends_with(".png"), "{cut}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_server_names_the_fix() {
|
||||
for kind in [io::ErrorKind::NotFound, io::ErrorKind::ConnectionRefused] {
|
||||
|
||||
@@ -487,6 +487,77 @@ async fn a_paired_phone_opens_a_tab() {
|
||||
assert!(err.to_string().contains("no workspace nope"), "{err}");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn a_paired_phone_sends_a_file_and_learns_where_it_went() {
|
||||
let rig = Rig::new().await;
|
||||
let session = within(rig.paired()).await;
|
||||
|
||||
// Bigger than one frame of the upload, so it arrives in pieces.
|
||||
let bytes: Vec<u8> = (0..700_000u32).map(|i| i as u8).collect();
|
||||
let path = within(session.upload(None, "../screen shot.png", &bytes))
|
||||
.await
|
||||
.unwrap();
|
||||
let path = std::path::PathBuf::from(path);
|
||||
assert!(path.is_absolute(), "{}", path.display());
|
||||
assert_eq!(path.file_name().unwrap(), "screen_shot.png");
|
||||
assert_eq!(std::fs::read(&path).unwrap(), bytes);
|
||||
std::fs::remove_dir_all(path.parent().unwrap()).unwrap();
|
||||
|
||||
// Only this machine takes files for now.
|
||||
let err = within(session.upload(Some("me@build-box:22"), "a.txt", b"hi"))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("this computer"), "{err}");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn a_paired_phone_reads_a_working_trees_changes() {
|
||||
let rig = Rig::new().await;
|
||||
let session = within(rig.paired()).await;
|
||||
|
||||
let repo = std::env::temp_dir().join(format!("tty7-diff-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&repo);
|
||||
std::fs::create_dir_all(repo.join("src")).unwrap();
|
||||
let run = |args: &[&str]| {
|
||||
let ok = std::process::Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&repo)
|
||||
.args([
|
||||
"-c",
|
||||
"user.name=t",
|
||||
"-c",
|
||||
"user.email=t@t",
|
||||
"-c",
|
||||
"commit.gpgsign=false",
|
||||
])
|
||||
.args(args)
|
||||
.output()
|
||||
.unwrap()
|
||||
.status
|
||||
.success();
|
||||
assert!(ok, "{args:?}");
|
||||
};
|
||||
run(&["init", "-q"]);
|
||||
std::fs::write(repo.join("src/a.txt"), "one\n").unwrap();
|
||||
run(&["add", "."]);
|
||||
run(&["commit", "-qm", "first"]);
|
||||
std::fs::write(repo.join("src/a.txt"), "one\ntwo\n").unwrap();
|
||||
std::fs::write(repo.join("new.txt"), "hi\n").unwrap();
|
||||
|
||||
// Asked from a directory inside the repository, as a pane's cwd often is.
|
||||
let cwd = repo.join("src").to_string_lossy().into_owned();
|
||||
let diff = within(session.diff(None, &cwd)).await.unwrap();
|
||||
assert!(diff.patch.contains("+two"), "{}", diff.patch);
|
||||
assert_eq!(diff.untracked, vec!["new.txt".to_string()]);
|
||||
assert!(!diff.truncated);
|
||||
|
||||
let err = within(session.diff(None, &std::env::temp_dir().to_string_lossy()))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("not in a git repository"), "{err}");
|
||||
std::fs::remove_dir_all(&repo).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn an_unpaired_phone_cannot_open_a_tab() {
|
||||
let rig = Rig::new().await;
|
||||
|
||||
@@ -14,9 +14,9 @@ use iroh::{Endpoint, EndpointAddr, EndpointId, RelayUrl, SecretKey};
|
||||
use iroh_mdns_address_lookup::MdnsAddressLookup;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tty7_mobile_proto::{
|
||||
ALPN, ControlEvent, ControlRequest, Frame, GridSize, MDNS_SERVICE, Open, OpenReply,
|
||||
PROTOCOL_VERSION, PairCode, PaneEvent, PaneRequest, TabCreated, read_frame, write_bytes,
|
||||
write_msg,
|
||||
ALPN, ControlEvent, ControlRequest, Diff, Frame, GridSize, MAX_UPLOAD, MDNS_SERVICE, Open,
|
||||
OpenReply, PROTOCOL_VERSION, PairCode, PaneEvent, PaneRequest, TabCreated, Uploaded,
|
||||
read_frame, write_bytes, write_msg,
|
||||
};
|
||||
|
||||
/// How long to wait for a gateway to answer an [`Open`].
|
||||
@@ -210,6 +210,70 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Sends a file to the machine for a pane to be handed, returning where
|
||||
/// it landed. `machine` is where that pane runs, as on [`Session::pane`].
|
||||
pub async fn upload(&self, machine: Option<&str>, name: &str, bytes: &[u8]) -> Result<String> {
|
||||
if bytes.len() as u64 > MAX_UPLOAD {
|
||||
bail!(
|
||||
"that file is {} MB; files up to {} MB can be sent",
|
||||
(bytes.len() as u64).div_ceil(1 << 20),
|
||||
MAX_UPLOAD >> 20
|
||||
);
|
||||
}
|
||||
let ask = Open::Upload {
|
||||
name: name.to_string(),
|
||||
size: bytes.len() as u64,
|
||||
machine: machine.map(str::to_string),
|
||||
};
|
||||
let (mut send, mut recv) = open(&self.conn, &ask).await.map_err(|e| {
|
||||
if e.to_string().contains("without answering") {
|
||||
anyhow!("tty7 on this computer is too old to take files — update it")
|
||||
} else {
|
||||
e
|
||||
}
|
||||
})?;
|
||||
for chunk in bytes.chunks(UPLOAD_CHUNK) {
|
||||
write_bytes(&mut send, chunk).await?;
|
||||
}
|
||||
let _ = send.finish();
|
||||
let answer = read_frame(&mut recv)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("the machine did not say where the file went"))?;
|
||||
if let Ok(OpenReply::Denied { reason }) = answer.msg::<OpenReply>() {
|
||||
bail!(reason);
|
||||
}
|
||||
Ok(answer.msg::<Uploaded>()?.path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// What has changed in the repository `cwd` is in, on `machine`.
|
||||
pub async fn diff(&self, machine: Option<&str>, cwd: &str) -> Result<Diff> {
|
||||
let ask = Open::Diff {
|
||||
cwd: cwd.to_string(),
|
||||
machine: machine.map(str::to_string),
|
||||
};
|
||||
let (mut send, mut recv) = open(&self.conn, &ask).await.map_err(|e| {
|
||||
if e.to_string().contains("without answering") {
|
||||
anyhow!("tty7 on this computer is too old to show changes — update it")
|
||||
} else {
|
||||
e
|
||||
}
|
||||
})?;
|
||||
let _ = send.finish();
|
||||
let diff = read_frame(&mut recv)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("the machine did not send the changes"))?
|
||||
.msg::<Diff>()?;
|
||||
Ok(diff)
|
||||
}
|
||||
}
|
||||
|
||||
/// How much of a file goes in one frame: well under the frame limit, and small
|
||||
/// enough that a slow link shows progress rather than one long wait.
|
||||
const UPLOAD_CHUNK: usize = 256 << 10;
|
||||
|
||||
async fn open(conn: &Connection, open: &Open) -> Result<(SendStream, RecvStream)> {
|
||||
let (mut send, mut recv) = conn.open_bi().await.context("opening a stream")?;
|
||||
write_msg(&mut send, open).await?;
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
//! - a [`Open::Pane`] stream per terminal on screen: raw output down, raw
|
||||
//! keystrokes up, and a few [`PaneEvent`]s beside them;
|
||||
//! - a one-shot [`Open::NewTab`] stream that starts a shell in a new tab and
|
||||
//! answers with a [`TabCreated`].
|
||||
//! answers with a [`TabCreated`];
|
||||
//! - a one-shot [`Open::Upload`] stream that carries a file from the phone to
|
||||
//! the machine and answers with where it landed, an [`Uploaded`];
|
||||
//! - a one-shot [`Open::Diff`] stream that answers with a git working tree's
|
||||
//! changes, a [`Diff`].
|
||||
//!
|
||||
//! Every frame is `u32` little-endian length, a one-byte kind, then the
|
||||
//! payload — the same shape as the daemon's frames, so there is one framing
|
||||
@@ -38,6 +42,10 @@ pub const MDNS_SERVICE: &str = "tty7";
|
||||
/// can tell the user to update rather than misread a newer gateway.
|
||||
pub const PROTOCOL_VERSION: u32 = 1;
|
||||
|
||||
/// The largest file an [`Open::Upload`] may carry: photos, screenshots and
|
||||
/// small files, not a way to move a disk image.
|
||||
pub const MAX_UPLOAD: u64 = 20 << 20;
|
||||
|
||||
/// The largest frame either side will accept. Output is forwarded in the chunks
|
||||
/// the daemon sends, and a replay segment is capped well below this on the
|
||||
/// daemon side; anything larger is a corrupt or hostile stream.
|
||||
@@ -88,6 +96,33 @@ pub enum Open {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
machine: Option<String>,
|
||||
},
|
||||
/// Put a file from the phone on the machine, for a pane to be handed its
|
||||
/// path. After `Ok` the phone sends the file as bytes frames and finishes
|
||||
/// its side; the gateway answers [`Uploaded`] once the file is written.
|
||||
/// `Denied` before any bytes: too big, or a machine files cannot go to.
|
||||
///
|
||||
/// A gateway older than this variant cannot parse it and drops the stream
|
||||
/// unanswered.
|
||||
/// What has changed in the git working tree `cwd` is in, against its last
|
||||
/// commit: `Ok`, then a [`Diff`]. `Denied` when it is not in a repository.
|
||||
///
|
||||
/// A gateway older than this variant cannot parse it and drops the stream
|
||||
/// unanswered.
|
||||
Diff {
|
||||
cwd: String,
|
||||
/// As on [`Open::Pane`].
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
machine: Option<String>,
|
||||
},
|
||||
Upload {
|
||||
/// The file's name on the phone; the gateway keeps what it safely can.
|
||||
name: String,
|
||||
/// Its length in bytes, at most [`MAX_UPLOAD`].
|
||||
size: u64,
|
||||
/// As on [`Open::Pane`]: where the pane it is for runs.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
machine: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -103,6 +138,32 @@ pub struct TabCreated {
|
||||
pub pane_id: u64,
|
||||
}
|
||||
|
||||
/// The answer on a [`Open::Diff`] stream.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Diff {
|
||||
/// The repository's top directory.
|
||||
pub root: String,
|
||||
/// `git diff HEAD`: staged and unstaged together, as a unified diff.
|
||||
pub patch: String,
|
||||
/// Files git does not track yet (and does not ignore), by path from `root`.
|
||||
#[serde(default)]
|
||||
pub untracked: Vec<String>,
|
||||
/// The patch was cut at [`MAX_DIFF`] bytes.
|
||||
#[serde(default)]
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
/// The most of a patch a [`Diff`] carries: enough to review an agent's work
|
||||
/// on a phone, not a vendored dependency's.
|
||||
pub const MAX_DIFF: usize = 2 << 20;
|
||||
|
||||
/// The answer on a [`Open::Upload`] stream once the file is written.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Uploaded {
|
||||
/// Where the file is, absolute, on the machine the pane runs on.
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// The gateway's answer to an [`Open`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
@@ -562,6 +623,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_upload_names_its_file_and_size() {
|
||||
let open = Open::Upload {
|
||||
name: "shot.png".into(),
|
||||
size: 42,
|
||||
machine: None,
|
||||
};
|
||||
let wire = serde_json::to_value(&open).unwrap();
|
||||
assert_eq!(
|
||||
wire,
|
||||
serde_json::json!({"type": "upload", "name": "shot.png", "size": 42})
|
||||
);
|
||||
assert_eq!(serde_json::from_value::<Open>(wire).unwrap(), open);
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
#[tokio::test]
|
||||
async fn async_reader_matches_the_decoder() {
|
||||
|
||||
@@ -2,3 +2,4 @@ node_modules/
|
||||
dist/
|
||||
src-tauri/target/
|
||||
src-tauri/gen/schemas/
|
||||
build/
|
||||
|
||||
@@ -71,6 +71,16 @@ npm run tauri ios init # once: generates src-tauri/gen/apple
|
||||
npm run tauri ios dev # simulator, or pick a connected device
|
||||
```
|
||||
|
||||
To send a build to TestFlight (Xcode signed in to an account on the team):
|
||||
|
||||
```sh
|
||||
scripts/testflight.sh # archive, sign for the App Store, upload
|
||||
scripts/testflight.sh --no-upload # a signed .ipa in build/testflight/ instead
|
||||
```
|
||||
|
||||
The build is `<version>.<n>`; `n` defaults to the time, so each upload is higher than the
|
||||
last. `--build-number 7` picks it.
|
||||
|
||||
### Android
|
||||
|
||||
Needs Android Studio's SDK and NDK, with `ANDROID_HOME` and `NDK_HOME` set.
|
||||
|
||||
Generated
+27
@@ -10,6 +10,9 @@
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-barcode-scanner": "^2.5.0",
|
||||
"@tauri-apps/plugin-biometric": "^2.4.0",
|
||||
"@tauri-apps/plugin-opener": "^2.6.0",
|
||||
"@xterm/addon-search": "^0.16.0",
|
||||
"@xterm/addon-webgl": "^0.19.0",
|
||||
"@xterm/xterm": "^6.0.0"
|
||||
},
|
||||
@@ -527,6 +530,30 @@
|
||||
"@tauri-apps/api": "^2.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-biometric": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tauri-apps/plugin-biometric/-/plugin-biometric-2.4.0.tgz",
|
||||
"integrity": "sha512-soFvDJ+oFLScaHJKgLecXLMcuuvHEbJKvClsZO24lkDl8JLLcxcyfFBqTDJjJh0azNUw/yLyJ+r7w/Z499t6UA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-opener": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tauri-apps/plugin-opener/-/plugin-opener-2.6.0.tgz",
|
||||
"integrity": "sha512-KIl9qkBGyAUrbzfCtdk+NRPTXUOwpMyUKsKN57H6/qA0q8auVywXpfUyWClqX6I/iH8PHmqAUa/KK9YZPWVQew==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/addon-search": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@xterm/addon-search/-/addon-search-0.16.0.tgz",
|
||||
"integrity": "sha512-9OeuBFu0/uZJPu+9AHKY6g/w0Czyb/Ut0A5t79I4ULoU4IfU5BEpPFVGQxP4zTTMdfZEYkVIRYbHBX1xWwjeSA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xterm/addon-webgl": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0.tgz",
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-barcode-scanner": "^2.5.0",
|
||||
"@tauri-apps/plugin-biometric": "^2.4.0",
|
||||
"@tauri-apps/plugin-opener": "^2.6.0",
|
||||
"@xterm/addon-search": "^0.16.0",
|
||||
"@xterm/addon-webgl": "^0.19.0",
|
||||
"@xterm/xterm": "^6.0.0"
|
||||
},
|
||||
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# Builds the iOS app and uploads it to App Store Connect for TestFlight.
|
||||
#
|
||||
# Usage: scripts/testflight.sh [--build-number N] [--no-upload]
|
||||
# --build-number N the build is <version>.N (default: the time, yyjjjHHMM)
|
||||
# --no-upload export a signed .ipa into build/testflight/ instead
|
||||
#
|
||||
# Needs Xcode signed in (Settings → Accounts) to an account on team 78SZC3DQ7B.
|
||||
#
|
||||
# Why not just `tauri ios build --export-method app-store-connect`:
|
||||
# - The archive is signed with a development profile first, and the team has
|
||||
# no registered devices to make one from. So the archive is built unsigned
|
||||
# and Xcode signs it for the App Store at export, where no device is needed.
|
||||
#
|
||||
# The default build number is the time — year, day of the year, hour, minute —
|
||||
# so each upload is higher than the last without keeping a counter, and it
|
||||
# stays under 2^31.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
UPLOAD=1
|
||||
N=$(date +%y%j%H%M)
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--no-upload) UPLOAD=0 ;;
|
||||
--build-number) N="${2:?--build-number needs a number}"; shift ;;
|
||||
*) echo "usage: $0 [--build-number N] [--no-upload]" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
TEAM=78SZC3DQ7B
|
||||
PROJECT=src-tauri/gen/apple/tty7-mobile.xcodeproj/project.pbxproj
|
||||
ARCHIVE=src-tauri/gen/apple/build/tty7-mobile_iOS.xcarchive
|
||||
OUT=build/testflight
|
||||
mkdir -p "$OUT"
|
||||
|
||||
[ -f "$PROJECT" ] || npm run tauri ios init
|
||||
|
||||
# Release signing off for the archive only; the project is put back however
|
||||
# this exits.
|
||||
cp "$PROJECT" "$OUT/project.pbxproj.orig"
|
||||
trap 'cp "$OUT/project.pbxproj.orig" "$PROJECT"' EXIT
|
||||
perl -0pi -e 's{(/\* release \*/ = \{\n\t+isa = XCBuildConfiguration;\n\t+buildSettings = \{\n)(?=(?:(?!\n\t+\};).)*?CODE_SIGN_ENTITLEMENTS)}{$1\t\t\t\tCODE_SIGNING_ALLOWED = NO;\n}sg' "$PROJECT"
|
||||
if [ "$(grep -c 'CODE_SIGNING_ALLOWED = NO' "$PROJECT")" != 1 ]; then
|
||||
echo "could not find the app target's release configuration in $PROJECT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Archiving build number $N"
|
||||
rm -rf "$ARCHIVE"
|
||||
npx tauri ios build --archive-only --build-number "$N"
|
||||
|
||||
APP=$(ls -d "$ARCHIVE"/Products/Applications/*.app)
|
||||
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$APP/Info.plist")
|
||||
BUILD=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$APP/Info.plist")
|
||||
|
||||
DESTINATION=$([ "$UPLOAD" = 1 ] && echo upload || echo export)
|
||||
cat > "$OUT/ExportOptions.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key><string>app-store-connect</string>
|
||||
<key>destination</key><string>$DESTINATION</string>
|
||||
<key>teamID</key><string>$TEAM</string>
|
||||
<key>signingStyle</key><string>automatic</string>
|
||||
<key>manageAppVersionAndBuildNumber</key><false/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
echo "==> Signing$([ "$UPLOAD" = 1 ] && echo " and uploading") $VERSION ($BUILD)"
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$ARCHIVE" \
|
||||
-exportOptionsPlist "$OUT/ExportOptions.plist" \
|
||||
-exportPath "$OUT" \
|
||||
-allowProvisioningUpdates
|
||||
|
||||
if [ "$UPLOAD" = 1 ]; then
|
||||
echo "==> Uploaded $VERSION ($BUILD); it shows in TestFlight once App Store Connect has processed it"
|
||||
else
|
||||
echo "==> Signed .ipa in $OUT"
|
||||
fi
|
||||
Generated
+599
-46
@@ -112,6 +112,137 @@ version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async_io_stream"
|
||||
version = "0.3.3"
|
||||
@@ -281,6 +412,19 @@ dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "9.0.0"
|
||||
@@ -475,7 +619,7 @@ dependencies = [
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -513,6 +657,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
@@ -1086,6 +1239,12 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||
|
||||
[[package]]
|
||||
name = "enum-assoc"
|
||||
version = "1.4.1"
|
||||
@@ -1097,6 +1256,27 @@ dependencies = [
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -1124,6 +1304,26 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
|
||||
dependencies = [
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
@@ -1444,8 +1644,8 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1713,6 +1913,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
@@ -1874,7 +2080,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2082,7 +2288,7 @@ dependencies = [
|
||||
"socket2",
|
||||
"widestring",
|
||||
"windows-registry",
|
||||
"windows-result",
|
||||
"windows-result 0.4.1",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -2275,6 +2481,25 @@ dependencies = [
|
||||
"ws_stream_wasm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-wsl"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||
dependencies = [
|
||||
"is-docker",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -2318,7 +2543,7 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2388,7 +2613,7 @@ dependencies = [
|
||||
"simd_cesu8",
|
||||
"thiserror 2.0.21",
|
||||
"walkdir",
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2539,6 +2764,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.3"
|
||||
@@ -2958,8 +3189,8 @@ dependencies = [
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"web-sys",
|
||||
"windows",
|
||||
"windows-result",
|
||||
"windows 0.62.2",
|
||||
"windows-result 0.4.1",
|
||||
"wmi",
|
||||
]
|
||||
|
||||
@@ -3341,6 +3572,17 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"is-wsl",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
@@ -3353,6 +3595,16 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -3414,7 +3666,7 @@ dependencies = [
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3527,6 +3779,17 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.11.0"
|
||||
@@ -3582,6 +3845,20 @@ dependencies = [
|
||||
"miniz_oxide 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.6.2"
|
||||
@@ -3946,6 +4223,19 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.45"
|
||||
@@ -4774,8 +5064,8 @@ dependencies = [
|
||||
"tao-macros",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
]
|
||||
@@ -4845,7 +5135,7 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"window-vibrancy",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4940,6 +5230,43 @@ dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-biometric"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84fd0e82d20c43b18fc4b2681092b48b25cc27a1381e37b14d71aa5beb2d5465"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4cf2c5723e6fe265a907591d5fae6b8b94edde39ad3876fdf425acf8d71f9b6"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"glob",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"open",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.21",
|
||||
"url",
|
||||
"windows 0.61.3",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.12.0"
|
||||
@@ -4962,7 +5289,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4987,7 +5314,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
"wry",
|
||||
]
|
||||
|
||||
@@ -5040,6 +5367,19 @@ dependencies = [
|
||||
"toml 1.1.6+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tendril"
|
||||
version = "0.5.1"
|
||||
@@ -5476,6 +5816,8 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-barcode-scanner",
|
||||
"tauri-plugin-biometric",
|
||||
"tauri-plugin-opener",
|
||||
"tokio",
|
||||
"tty7-mobile-client",
|
||||
"tty7-mobile-proto",
|
||||
@@ -5483,7 +5825,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tty7-mobile-client"
|
||||
version = "26.9.3"
|
||||
version = "26.9.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"iroh",
|
||||
@@ -5495,7 +5837,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tty7-mobile-proto"
|
||||
version = "26.9.3"
|
||||
version = "26.9.4"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde",
|
||||
@@ -5515,6 +5857,17 @@ version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
@@ -5835,8 +6188,8 @@ checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6"
|
||||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5857,8 +6210,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5914,16 +6267,38 @@ dependencies = [
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
||||
dependencies = [
|
||||
"windows-collections 0.2.0",
|
||||
"windows-core 0.61.2",
|
||||
"windows-future 0.2.1",
|
||||
"windows-link 0.1.3",
|
||||
"windows-numerics 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
"windows-collections 0.3.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-future 0.3.2",
|
||||
"windows-numerics 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5932,7 +6307,20 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5943,9 +6331,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link 0.1.3",
|
||||
"windows-threading 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5954,9 +6353,9 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
"windows-threading 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5981,20 +6380,36 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6003,9 +6418,18 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6014,7 +6438,16 @@ version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6023,7 +6456,7 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6059,7 +6492,7 @@ version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6093,13 +6526,22 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6108,7 +6550,7 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6246,8 +6688,8 @@ dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"thiserror 2.0.21",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6294,8 +6736,8 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
]
|
||||
@@ -6378,6 +6820,76 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"libc",
|
||||
"ordered-stream",
|
||||
"rustix",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"winnow 1.0.4",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "4.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"winnow 1.0.4",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zcheapstr"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.59"
|
||||
@@ -6483,3 +6995,44 @@ name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"winnow 1.0.4",
|
||||
"zcheapstr",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "5.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 3.0.6",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
@@ -31,9 +31,12 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri = { version = "2", features = [] }
|
||||
tokio = { version = "1", features = ["sync", "time", "macros"] }
|
||||
tauri-plugin-opener = "2"
|
||||
|
||||
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
|
||||
tauri-plugin-barcode-scanner = "2"
|
||||
# Face ID / passcode before the app shows anything, when turned on.
|
||||
tauri-plugin-biometric = "2"
|
||||
|
||||
# Setting one WKWebView property the Tauri API does not reach (`edge_to_edge`).
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
Settings → Mobile (tauri-plugin-barcode-scanner). -->
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>tty7 uses the camera to scan the pairing code your computer shows.</string>
|
||||
<!-- The optional app lock (Settings → Lock with Face ID;
|
||||
tauri-plugin-biometric). -->
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>tty7 uses Face ID to unlock the app when you turn on the lock in Settings.</string>
|
||||
<!-- The only encryption is standard TLS/QUIC (rustls, iroh), which is
|
||||
exempt from export documentation. Saying so here spares the
|
||||
compliance question on every TestFlight upload. -->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "The app's single window: its own commands and the core IPC they ride on.",
|
||||
"description": "The app's single window: its own commands, the core IPC they ride on, and opening a tapped link in the browser.",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default"]
|
||||
"permissions": ["core:default", "opener:default"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"barcode-scanner:default"
|
||||
"barcode-scanner:default",
|
||||
"biometric:default"
|
||||
]
|
||||
}
|
||||
+140
-21
@@ -35,8 +35,10 @@ struct AppState {
|
||||
dir: PathBuf,
|
||||
endpoint: OnceCell<Endpoint>,
|
||||
sessions: Mutex<HashMap<String, Session>>,
|
||||
/// The refresh half of each machine's control stream, by host id.
|
||||
controls: Mutex<HashMap<String, ControlSender>>,
|
||||
/// The refresh half of each open watch, by watch id, with its host id.
|
||||
/// Each screen's watch is its own: one never ends another's stream.
|
||||
controls: Mutex<HashMap<u32, (String, ControlSender)>>,
|
||||
next_watch: AtomicU32,
|
||||
panes: Mutex<HashMap<u32, mpsc::UnboundedSender<Up>>>,
|
||||
next_pane: AtomicU32,
|
||||
}
|
||||
@@ -49,6 +51,18 @@ enum Up {
|
||||
Request(PaneRequest),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn names_come_back_from_their_header_encoding() {
|
||||
assert_eq!(
|
||||
super::percent_decode("%E6%88%AA%E5%9B%BE%201.png"),
|
||||
"截图 1.png"
|
||||
);
|
||||
assert_eq!(super::percent_decode("100%"), "100%");
|
||||
}
|
||||
}
|
||||
|
||||
fn err(e: impl std::fmt::Display) -> String {
|
||||
// `{:#}` walks anyhow's chain, so "could not reach studio: no route"
|
||||
// reaches the user rather than just the outermost context.
|
||||
@@ -93,9 +107,11 @@ impl AppState {
|
||||
|
||||
/// The live session to a machine, dialing a new one if there is none or
|
||||
/// the last one has dropped (the app was backgrounded, the network moved).
|
||||
///
|
||||
/// The lock is not held while dialing: a machine that is offline takes a
|
||||
/// long time to fail, and `forget` must not wait behind it.
|
||||
async fn session(&self, host_id: &str) -> CmdResult<Session> {
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
if let Some(s) = sessions.get(host_id)
|
||||
if let Some(s) = self.sessions.lock().await.get(host_id)
|
||||
&& !s.is_closed()
|
||||
{
|
||||
return Ok(s.clone());
|
||||
@@ -103,6 +119,19 @@ impl AppState {
|
||||
let host = self.host(host_id)?;
|
||||
let endpoint = self.endpoint().await.map_err(err)?;
|
||||
let session = Session::connect(endpoint, &host).await.map_err(err)?;
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
// Forgotten while dialing: the session is not wanted.
|
||||
if self.host(host_id).is_err() {
|
||||
session.close();
|
||||
return Err("that machine is not paired".into());
|
||||
}
|
||||
// Another call dialed it first: keep that one.
|
||||
if let Some(s) = sessions.get(host_id)
|
||||
&& !s.is_closed()
|
||||
{
|
||||
session.close();
|
||||
return Ok(s.clone());
|
||||
}
|
||||
sessions.insert(host_id.to_string(), session.clone());
|
||||
Ok(session)
|
||||
}
|
||||
@@ -149,12 +178,21 @@ async fn pair(
|
||||
|
||||
#[tauri::command]
|
||||
async fn forget(state: State<'_, Arc<AppState>>, host_id: String) -> CmdResult<()> {
|
||||
// Off the list first, so a dial still under way finds it gone.
|
||||
let mut hosts = state.load_hosts();
|
||||
hosts.retain(|h| h.id != host_id);
|
||||
state.save_hosts(&hosts).map_err(err)?;
|
||||
if let Some(s) = state.sessions.lock().await.remove(&host_id) {
|
||||
s.close();
|
||||
}
|
||||
let mut hosts = state.load_hosts();
|
||||
hosts.retain(|h| h.id != host_id);
|
||||
state.save_hosts(&hosts).map_err(err)
|
||||
// After the close, so a refresh in flight on it fails rather than holds
|
||||
// the lock.
|
||||
state
|
||||
.controls
|
||||
.lock()
|
||||
.await
|
||||
.retain(|_, (host, _)| *host != host_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// What a machine's screen hears about it.
|
||||
@@ -174,14 +212,16 @@ async fn watch(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
host_id: String,
|
||||
on_event: Channel<TreeMsg>,
|
||||
) -> CmdResult<()> {
|
||||
) -> CmdResult<u32> {
|
||||
let session = state.session(&host_id).await?;
|
||||
let (sender, mut events) = session.control().await.map_err(err)?.split();
|
||||
state.controls.lock().await.insert(host_id.clone(), sender);
|
||||
let id = state.next_watch.fetch_add(1, Ordering::Relaxed);
|
||||
state.controls.lock().await.insert(id, (host_id, sender));
|
||||
let app_state = state.inner().clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut link = tokio::time::interval(LINK_REPORT);
|
||||
loop {
|
||||
'stream: loop {
|
||||
tokio::select! {
|
||||
event = events.next() => {
|
||||
// A gateway-side error (it lost the daemon) keeps the
|
||||
@@ -196,32 +236,41 @@ async fn watch(
|
||||
Err(e) => (TreeMsg::Error { message: err(e) }, true),
|
||||
};
|
||||
if on_event.send(msg).is_err() {
|
||||
return;
|
||||
break 'stream;
|
||||
}
|
||||
if last {
|
||||
let _ = on_event.send(TreeMsg::Closed);
|
||||
return;
|
||||
break 'stream;
|
||||
}
|
||||
}
|
||||
_ = link.tick() => {
|
||||
if on_event.send(TreeMsg::Link { link: session.link() }).is_err() {
|
||||
return;
|
||||
break 'stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
app_state.controls.lock().await.remove(&id);
|
||||
});
|
||||
Ok(())
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn refresh(state: State<'_, Arc<AppState>>, host_id: String) -> CmdResult<()> {
|
||||
match state.controls.lock().await.get_mut(&host_id) {
|
||||
Some(sender) => sender.refresh().await.map_err(err),
|
||||
async fn refresh(state: State<'_, Arc<AppState>>, watch: u32) -> CmdResult<()> {
|
||||
match state.controls.lock().await.get_mut(&watch) {
|
||||
Some((_, sender)) => sender.refresh().await.map_err(err),
|
||||
None => Err("not watching that machine".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ends a watch: dropping its sender finishes the stream, the gateway stops
|
||||
/// sending, and the reading task ends with it.
|
||||
#[tauri::command]
|
||||
async fn unwatch(state: State<'_, Arc<AppState>>, watch: u32) -> CmdResult<()> {
|
||||
state.controls.lock().await.remove(&watch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Starts a shell in a new tab at the end of a workspace. The frontend opens
|
||||
/// the returned pane like any other.
|
||||
#[tauri::command]
|
||||
@@ -240,6 +289,72 @@ async fn tab_new(
|
||||
.map_err(err)
|
||||
}
|
||||
|
||||
/// Sends a file to the machine for a pane, returning its path there. The
|
||||
/// file is the request's raw body, not JSON (a 20 MB photo would be 80 MB as
|
||||
/// a number array); which machine and the file's name ride in headers, the
|
||||
/// name percent-encoded since a header is ASCII.
|
||||
#[tauri::command]
|
||||
async fn upload(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
request: tauri::ipc::Request<'_>,
|
||||
) -> CmdResult<String> {
|
||||
let header = |name: &str| {
|
||||
request
|
||||
.headers()
|
||||
.get(name)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(percent_decode)
|
||||
};
|
||||
let host_id = header("x-host").ok_or("no machine given")?;
|
||||
let name = header("x-name").unwrap_or_default();
|
||||
let machine = header("x-machine").filter(|m| !m.is_empty());
|
||||
let tauri::ipc::InvokeBody::Raw(bytes) = request.body() else {
|
||||
return Err("the file did not arrive as bytes".into());
|
||||
};
|
||||
let session = state.session(&host_id).await?;
|
||||
session
|
||||
.upload(machine.as_deref(), &name, bytes)
|
||||
.await
|
||||
.map_err(err)
|
||||
}
|
||||
|
||||
/// What has changed in the repository a pane's directory is in.
|
||||
#[tauri::command]
|
||||
async fn diff(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
host_id: String,
|
||||
machine: Option<String>,
|
||||
cwd: String,
|
||||
) -> CmdResult<tty7_mobile_proto::Diff> {
|
||||
let session = state.session(&host_id).await?;
|
||||
session.diff(machine.as_deref(), &cwd).await.map_err(err)
|
||||
}
|
||||
|
||||
/// Undoes `encodeURIComponent`. A malformed escape is kept as written.
|
||||
fn percent_decode(s: &str) -> String {
|
||||
let bytes = s.as_bytes();
|
||||
let mut out = Vec::with_capacity(bytes.len());
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
let hex = |b: u8| (b as char).to_digit(16);
|
||||
match (
|
||||
bytes[i],
|
||||
bytes.get(i + 1).copied().and_then(hex),
|
||||
bytes.get(i + 2).copied().and_then(hex),
|
||||
) {
|
||||
(b'%', Some(hi), Some(lo)) => {
|
||||
out.push((hi * 16 + lo) as u8);
|
||||
i += 3;
|
||||
}
|
||||
(b, _, _) => {
|
||||
out.push(b);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
String::from_utf8_lossy(&out).into_owned()
|
||||
}
|
||||
|
||||
/// Opens a pane. Output arrives on `on_output` as `ArrayBuffer`s of raw
|
||||
/// terminal bytes, interleaved in order with JSON [`PaneEvent`]s; the returned
|
||||
/// handle addresses [`pane_input`] and [`pane_close`].
|
||||
@@ -397,11 +512,14 @@ pub fn run() {
|
||||
// already in the tree. An `Err` means one is installed, which is fine.
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
let builder = tauri::Builder::default();
|
||||
// Links tapped in a pane open in the phone's browser.
|
||||
let builder = tauri::Builder::default().plugin(tauri_plugin_opener::init());
|
||||
// Scanning the pairing QR code. Phones only: the desktop dev build has no
|
||||
// camera to point and pastes the code instead.
|
||||
#[cfg(mobile)]
|
||||
let builder = builder.plugin(tauri_plugin_barcode_scanner::init());
|
||||
let builder = builder
|
||||
.plugin(tauri_plugin_barcode_scanner::init())
|
||||
.plugin(tauri_plugin_biometric::init());
|
||||
|
||||
builder
|
||||
.setup(|app| {
|
||||
@@ -417,12 +535,13 @@ pub fn run() {
|
||||
controls: Mutex::new(HashMap::new()),
|
||||
panes: Mutex::new(HashMap::new()),
|
||||
next_pane: AtomicU32::new(1),
|
||||
next_watch: AtomicU32::new(1),
|
||||
}));
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
hosts, pair, forget, watch, refresh, tab_new, pane_open, pane_input, pane_lease,
|
||||
pane_close, appearance
|
||||
hosts, pair, forget, watch, unwatch, refresh, tab_new, upload, diff, pane_open,
|
||||
pane_input, pane_lease, pane_close, appearance
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tty7");
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"frameworks": [
|
||||
"SystemConfiguration"
|
||||
],
|
||||
"developmentTeam": "78SZC3DQ7B"
|
||||
"developmentTeam": "78SZC3DQ7B",
|
||||
"minimumSystemVersion": "15.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+32
-2
@@ -89,10 +89,13 @@ export const appearance = (style: "system" | "light" | "dark") => invoke<void>("
|
||||
export function watch(hostId: string, onMsg: (msg: TreeMsg) => void) {
|
||||
const onEvent = new Channel<TreeMsg>();
|
||||
onEvent.onmessage = onMsg;
|
||||
return invoke<void>("watch", { hostId, onEvent });
|
||||
return invoke<number>("watch", { hostId, onEvent });
|
||||
}
|
||||
|
||||
export const refresh = (hostId: string) => invoke<void>("refresh", { hostId });
|
||||
/** Ends a watch by the id `watch` resolved with. */
|
||||
export const unwatch = (watch: number) => invoke<void>("unwatch", { watch });
|
||||
|
||||
export const refresh = (watch: number) => invoke<void>("refresh", { watch });
|
||||
|
||||
/** Output arrives as ArrayBuffers of raw terminal bytes, events as objects. */
|
||||
/** `machine` is a remote's key, or null for the paired machine itself. */
|
||||
@@ -131,4 +134,31 @@ export const paneInput = (handle: number, data: string) =>
|
||||
export const paneLease = (handle: number, size: { cols: number; rows: number } | null) =>
|
||||
invoke<void>("pane_lease", { handle, size });
|
||||
|
||||
/** The largest file the gateway takes (`tty7_mobile_proto::MAX_UPLOAD`),
|
||||
* checked here too so a big video is refused before it is read in. */
|
||||
export const MAX_UPLOAD = 20 * 2 ** 20;
|
||||
|
||||
/** Sends a file to the machine a pane runs on; resolves with its path there.
|
||||
* The bytes go as the raw body; the rest rides in headers, encoded, since a
|
||||
* header is ASCII and a file name need not be. */
|
||||
export const upload = (hostId: string, machine: string | null, name: string, bytes: Uint8Array) =>
|
||||
invoke<string>("upload", bytes, {
|
||||
headers: {
|
||||
"x-host": encodeURIComponent(hostId),
|
||||
"x-name": encodeURIComponent(name),
|
||||
"x-machine": encodeURIComponent(machine ?? ""),
|
||||
},
|
||||
});
|
||||
|
||||
export interface Diff {
|
||||
root: string;
|
||||
patch: string;
|
||||
untracked: string[];
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
/** What has changed in the repository `cwd` is in. */
|
||||
export const diff = (hostId: string, machine: string | null, cwd: string) =>
|
||||
invoke<Diff>("diff", { hostId, machine, cwd });
|
||||
|
||||
export const paneClose = (handle: number) => invoke<void>("pane_close", { handle });
|
||||
|
||||
@@ -42,6 +42,9 @@ export const icon = {
|
||||
settings: stroke(
|
||||
`<path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2.2"/><circle cx="9" cy="17" r="2.2"/>`,
|
||||
),
|
||||
attach: stroke(
|
||||
`<path d="m19.5 11.8-7.3 7.3a4.7 4.7 0 0 1-6.6-6.6l7.6-7.6a3.1 3.1 0 0 1 4.4 4.4l-7.6 7.6a1.5 1.5 0 0 1-2.1-2.1l7-7"/>`,
|
||||
),
|
||||
history: stroke(`<path d="M4.5 12a7.5 7.5 0 1 0 2.2-5.3L4.5 9"/><path d="M4.5 4.5V9H9"/><path d="M12 8v4.2l2.8 1.8"/>`),
|
||||
alert: stroke(`<path d="M12 4 2.8 19.5h18.4Z"/><path d="M12 10v4M12 17h.01"/>`),
|
||||
};
|
||||
|
||||
+897
-83
File diff suppressed because it is too large
Load Diff
@@ -310,6 +310,17 @@ code {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Sending: pulses until the machine says where the file went. */
|
||||
.round.busy {
|
||||
animation: busy 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes busy {
|
||||
to {
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -436,6 +447,66 @@ code {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
/* The workspace switcher: one row of choices that scrolls sideways, running
|
||||
to the screen's edges. */
|
||||
.ws-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: -6px 0 18px;
|
||||
padding: 2px 20px;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-x: contain;
|
||||
scrollbar-width: none;
|
||||
scroll-padding: 0 20px;
|
||||
}
|
||||
|
||||
.ws-bar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ws-chip {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 36px;
|
||||
max-width: 70vw;
|
||||
padding: 0 14px;
|
||||
border-radius: 18px;
|
||||
background: var(--fill);
|
||||
color: var(--ink);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.ws-chip .icon {
|
||||
flex: none;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.ws-chip-name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ws-chip-count {
|
||||
color: var(--ink-2);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.ws-chip.on {
|
||||
background: var(--ink);
|
||||
color: var(--canvas);
|
||||
}
|
||||
|
||||
.ws-chip.on .ws-chip-count {
|
||||
color: inherit;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.remote-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -638,6 +709,17 @@ code {
|
||||
mask: var(--mark) center / contain no-repeat;
|
||||
}
|
||||
|
||||
/* A hairline keeps a disc as dark as the page (Codex's black) a shape. */
|
||||
.avatar.branded {
|
||||
background: var(--brand-field);
|
||||
color: var(--brand-ink);
|
||||
box-shadow: inset 0 0 0 0.5px var(--hair);
|
||||
}
|
||||
|
||||
.avatar.branded .mark {
|
||||
background: var(--brand-ink);
|
||||
}
|
||||
|
||||
.avatar .glyph {
|
||||
font: 600 12px var(--mono);
|
||||
letter-spacing: -0.06em;
|
||||
@@ -793,6 +875,11 @@ code {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.button.danger {
|
||||
background: var(--danger);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.button.small {
|
||||
height: 34px;
|
||||
padding: 0 14px;
|
||||
@@ -1612,6 +1699,18 @@ html.scanning #app {
|
||||
gap: 20px;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
/* Scrolling clips the sides too: room for a chosen card's ring there. */
|
||||
margin: 0 -4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.sheet-body:has(> .sheet-text) {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sheet-text {
|
||||
margin: 0 4px 10px;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.sheet-group .group-title {
|
||||
@@ -1797,6 +1896,63 @@ html[data-nav="pop"]::view-transition-new(root) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* An agent's choices over the key row, the first the way to say yes. */
|
||||
.answers {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
|
||||
.answers::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.answer {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 78%;
|
||||
height: 42px;
|
||||
padding: 0 14px 0 6px;
|
||||
border-radius: 10px;
|
||||
background: var(--cell);
|
||||
box-shadow:
|
||||
0 0 0 0.5px var(--hair),
|
||||
0 1px 0 var(--hair);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.answer b {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 7px;
|
||||
background: var(--fill);
|
||||
font: 600 14px var(--mono);
|
||||
}
|
||||
|
||||
.answer:first-child b {
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
|
||||
.answer span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.answer:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
flex: none;
|
||||
max-width: 78%;
|
||||
@@ -1852,3 +2008,244 @@ html[data-nav="pop"]::view-transition-new(root) {
|
||||
font-family: var(--mono);
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
/* Settings → Key bar */
|
||||
|
||||
.row-title.danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.row-title.accent {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.row.key-edit {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.row.key-edit .row-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* The key as the bar draws it, at a fixed size so the list lines up. */
|
||||
.key.key-sample {
|
||||
flex: none;
|
||||
width: 52px;
|
||||
height: 34px;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.row.key-edit .nav-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
box-shadow: none;
|
||||
background: var(--fill);
|
||||
}
|
||||
|
||||
.row.key-edit .nav-icon .icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.row.key-edit .nav-icon:disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.row.key-edit .nav-icon.danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.key-catalog {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-card .row.field {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.sheet-group .group-note {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.keys-note {
|
||||
margin: -8px 24px 24px;
|
||||
}
|
||||
|
||||
/* Find in a pane: a bar over the top of the terminal. */
|
||||
.find-bar {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px;
|
||||
border-radius: 22px;
|
||||
background: var(--canvas);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.find-bar .search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.find-count {
|
||||
flex: none;
|
||||
font-size: 13px;
|
||||
color: var(--ink-2);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.find-bar .nav-icon {
|
||||
flex: none;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
box-shadow: none;
|
||||
background: var(--fill);
|
||||
}
|
||||
|
||||
/* The app lock: everything covered until Face ID or the passcode. */
|
||||
.lock-cover {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 18px;
|
||||
background: var(--canvas);
|
||||
}
|
||||
|
||||
/* An on/off switch drawn as the system's. */
|
||||
.switch {
|
||||
position: relative;
|
||||
flex: none;
|
||||
width: 51px;
|
||||
height: 31px;
|
||||
margin-left: auto;
|
||||
border-radius: 16px;
|
||||
background: var(--fill);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.switch::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 6px rgb(0 0 0 / 0.2);
|
||||
transition: transform 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.switch.on {
|
||||
background: var(--done);
|
||||
}
|
||||
|
||||
.switch.on::after {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
/* Changes: a pane's repository, file by file. */
|
||||
.diff-body {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.diff-root {
|
||||
margin: 0 4px;
|
||||
font: 13px var(--mono);
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.diff-file {
|
||||
border-radius: 14px;
|
||||
background: var(--cell);
|
||||
box-shadow: 0 0 0 0.5px var(--hair);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.diff-file summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
list-style: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.diff-file summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.diff-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.diff-add,
|
||||
.diff-del {
|
||||
flex: none;
|
||||
font: 600 12px var(--mono);
|
||||
}
|
||||
|
||||
.diff-add {
|
||||
color: var(--done);
|
||||
}
|
||||
|
||||
.diff-del {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.diff-code {
|
||||
overflow-x: auto;
|
||||
padding-bottom: 6px;
|
||||
font: 11.5px/1.45 var(--mono);
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.diff-line {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
padding: 0 14px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.diff-line.add {
|
||||
background: color-mix(in srgb, var(--done) 16%, transparent);
|
||||
}
|
||||
|
||||
.diff-line.del {
|
||||
background: color-mix(in srgb, var(--danger) 14%, transparent);
|
||||
}
|
||||
|
||||
.diff-line.hunk {
|
||||
margin-top: 4px;
|
||||
color: var(--ink-2);
|
||||
background: var(--fill);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user