Add Live Activities as configurable bar items (#30)
* Add Live Activities as configurable bar items * Fix activity owner monitoring and Morph list interactions * Keep activity rows stable during updates and tighten their layout * Use subtle theme tint for activity cards * Tighten activity title and progress spacing * Use theme accent for active progress rings * Fit compact activities to measured titles and badge * Expose Live Activities display controls in Settings * Open activity terminals and streamline the Morph header * Give Morph continuous icons and prioritize workspace navigation * Show activity progress inside its ring * Inset activity progress rings and strengthen their stroke * Fix activity lifecycle, command output handling and list navigation * Reduce activity update work and harden publisher lifetimes * Set up Live Activities for the detected user shell during installation * Document Activities setup, daily use and troubleshooting * Preserve Morph idle width across empty activity updates * Fix dimming flicker, hide inactive music tab and consolidate Activities docs * Fix panel clearance across styles and match previews to theme settings * Prevent panel blur from repainting neighboring islands --------- Co-authored-by: Samuel Onoja <[email protected]>
This commit is contained in:
1 parent
7a7df0106c
commit
6b0d903ad0
107 files changed
+9340
-291
No files matched your search
@@ -189,3 +189,284 @@ fn event_stream_uses_the_selected_socket_and_prints_complete_json_lines() {
|
||||
stop.send(()).unwrap();
|
||||
worker.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn activity_start_uses_versioned_framing_and_parent_scope() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let socket = dir.path().join("activity.sock");
|
||||
let listener = UnixListener::bind(&socket).unwrap();
|
||||
let server = thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().unwrap();
|
||||
let request: Request = read_frame(&mut stream).unwrap();
|
||||
assert_eq!(request.version, ferese_ipc::VERSION);
|
||||
assert_eq!(request.command, "activity-publish");
|
||||
let wire: ferese_ipc::activities::Request = serde_json::from_value(request.args).unwrap();
|
||||
assert!(matches!(wire.owner, ferese_ipc::activities::OwnerScope::Parent));
|
||||
match wire.publish {
|
||||
ferese_ipc::activities::Publish::Start { local, data, .. } => {
|
||||
assert_eq!(local, "build");
|
||||
assert_eq!(data.value, ferese_activities::Value::Indeterminate);
|
||||
}
|
||||
_ => panic!("wrong activity action"),
|
||||
}
|
||||
write_frame(
|
||||
&mut stream,
|
||||
&Response::success(request.id, json!({"handle":{"generation":1}})),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
success(
|
||||
ctl(&[
|
||||
"--socket",
|
||||
socket.to_str().unwrap(),
|
||||
"activity",
|
||||
"start",
|
||||
"build",
|
||||
"--title",
|
||||
"Build",
|
||||
])
|
||||
.output()
|
||||
.unwrap(),
|
||||
);
|
||||
server.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_preserves_arguments_cwd_environment_output_and_exit_when_publishing_fails() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let output = ctl(&[
|
||||
"--socket",
|
||||
"/missing/ferese.sock",
|
||||
"run",
|
||||
"--title",
|
||||
"Test",
|
||||
"--",
|
||||
"sh",
|
||||
"-c",
|
||||
"printf '%s|%s|%s' \"$1\" \"$ACTIVITY_TEST_VALUE\" \"$PWD\"; printf stderr >&2; exit 17",
|
||||
"sh",
|
||||
"argument with spaces;$literal",
|
||||
])
|
||||
.env("ACTIVITY_TEST_VALUE", "preserved")
|
||||
.current_dir(dir.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(17));
|
||||
assert_eq!(
|
||||
String::from_utf8(output.stdout).unwrap(),
|
||||
format!("argument with spaces;$literal|preserved|{}", dir.path().display())
|
||||
);
|
||||
assert_eq!(output.stderr, b"stderr");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_preserves_child_exit_and_signal_status() {
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
let output = ctl(&[
|
||||
"run",
|
||||
"--",
|
||||
"sh",
|
||||
"-c",
|
||||
"printf '{\"reason\":\"build-finished\",\"success\":true}\\n'; exit 19",
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(19));
|
||||
let output = ctl(&["run", "--", "sh", "-c", "kill -TERM $$"]).output().unwrap();
|
||||
assert_eq!(output.status.signal(), Some(libc::SIGTERM));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrapper_forwards_termination_and_preserves_job_control_group() {
|
||||
use std::os::unix::process::{CommandExt, ExitStatusExt};
|
||||
let mut command = ctl(&["run", "--", "sh", "-c", "kill -STOP $$; sleep 30"]);
|
||||
unsafe {
|
||||
command.pre_exec(|| {
|
||||
libc::setpgid(0, 0);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
let mut child = command.spawn().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||
let pid = child.id() as i32;
|
||||
assert_eq!(unsafe { libc::kill(-pid, libc::SIGCONT) }, 0);
|
||||
assert_eq!(unsafe { libc::kill(pid, libc::SIGTERM) }, 0);
|
||||
assert_eq!(child.wait().unwrap().signal(), Some(libc::SIGTERM));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_ctrl_c_reaches_child_and_wrapper_returns_sigint() {
|
||||
use std::os::unix::process::{CommandExt, ExitStatusExt};
|
||||
let mut command = ctl(&["run", "--", "sleep", "30"]);
|
||||
unsafe {
|
||||
command.pre_exec(|| {
|
||||
libc::setpgid(0, 0);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
let mut child = command.spawn().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||
assert_eq!(unsafe { libc::kill(-(child.id() as i32), libc::SIGINT) }, 0);
|
||||
assert_eq!(child.wait().unwrap().signal(), Some(libc::SIGINT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_silent_broker_cannot_prevent_or_change_command_execution() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let socket = directory.path().join("blocked.sock");
|
||||
let listener = UnixListener::bind(&socket).unwrap();
|
||||
let worker = thread::spawn(move || {
|
||||
let (_stream, _) = listener.accept().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||
});
|
||||
let began = std::time::Instant::now();
|
||||
let result = ctl(&[
|
||||
"--socket",
|
||||
socket.to_str().unwrap(),
|
||||
"run",
|
||||
"--",
|
||||
"sh",
|
||||
"-c",
|
||||
"printf executed; exit 23",
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(result.stdout, b"executed");
|
||||
assert_eq!(result.status.code(), Some(23));
|
||||
assert!(began.elapsed() < std::time::Duration::from_secs(3));
|
||||
worker.join().unwrap();
|
||||
}
|
||||
|
||||
fn fake_cargo(dir: &Path, script: &str) -> String {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let path = dir.join("cargo");
|
||||
std::fs::write(&path, script).unwrap();
|
||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
format!("{}:{}", dir.display(), std::env::var("PATH").unwrap_or_default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cargo_adapter_completion_does_not_wait_for_descendant_stdout() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = fake_cargo(
|
||||
dir.path(),
|
||||
"#!/bin/sh\nsleep 5 &\necho $! > descendant\nprintf captured\nexit 23\n",
|
||||
);
|
||||
let output = dir.path().join("output");
|
||||
let mut child = ctl(&[
|
||||
"--socket",
|
||||
"/missing/activity.sock",
|
||||
"run",
|
||||
"--cargo-json",
|
||||
"--",
|
||||
"cargo",
|
||||
"build",
|
||||
])
|
||||
.env("PATH", path)
|
||||
.current_dir(dir.path())
|
||||
.stdout(std::fs::File::create(&output).unwrap())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||
let status = loop {
|
||||
if let Some(status) = child.try_wait().unwrap() {
|
||||
break Some(status);
|
||||
}
|
||||
if std::time::Instant::now() >= deadline {
|
||||
child.kill().unwrap();
|
||||
child.wait().unwrap();
|
||||
break None;
|
||||
}
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
};
|
||||
let descendant: i32 = std::fs::read_to_string(dir.path().join("descendant"))
|
||||
.unwrap()
|
||||
.trim()
|
||||
.parse()
|
||||
.unwrap();
|
||||
unsafe {
|
||||
libc::kill(descendant, libc::SIGTERM);
|
||||
}
|
||||
assert_eq!(
|
||||
status.and_then(|status| status.code()),
|
||||
Some(23),
|
||||
"waited for descendant EOF"
|
||||
);
|
||||
assert_eq!(std::fs::read(&output).unwrap(), b"captured");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stalled_progress_acknowledgements_do_not_block_cargo_stdout() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = fake_cargo(
|
||||
dir.path(),
|
||||
r##"#!/usr/bin/env python3
|
||||
import json, time, sys
|
||||
start = time.monotonic()
|
||||
time.sleep(.3)
|
||||
for _ in range(20000):
|
||||
print('{"reason":"compiler-artifact"}')
|
||||
sys.stdout.flush()
|
||||
with open('elapsed', 'w') as f:
|
||||
f.write(str(time.monotonic() - start))
|
||||
print('{"reason":"build-finished","success":true}')
|
||||
print('child output')
|
||||
print(json.dumps({'reason': 'compiler-message', 'message': {'rendered': 'diagnostic\n'}}))
|
||||
sys.exit(19)
|
||||
"##,
|
||||
);
|
||||
let socket = dir.path().join("broker.sock");
|
||||
let listener = UnixListener::bind(&socket).unwrap();
|
||||
listener.set_nonblocking(true).unwrap();
|
||||
let server = thread::spawn(move || {
|
||||
let mut stalled = Vec::new();
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||
while std::time::Instant::now() < deadline {
|
||||
let Ok((mut stream, _)) = listener.accept() else {
|
||||
thread::sleep(std::time::Duration::from_millis(5));
|
||||
continue;
|
||||
};
|
||||
stream
|
||||
.set_read_timeout(Some(std::time::Duration::from_secs(1)))
|
||||
.unwrap();
|
||||
let request: Request = read_frame(&mut stream).unwrap();
|
||||
match request.args["publish"]["action"].as_str().unwrap() {
|
||||
"run" => {
|
||||
let handle = ferese_activities::Id {
|
||||
owner: ferese_activities::Owner { pid: 1, birth: 1 },
|
||||
local: request.args["publish"]["local"].as_str().unwrap().into(),
|
||||
session: 1,
|
||||
generation: 1,
|
||||
};
|
||||
write_frame(
|
||||
&mut stream,
|
||||
&Response::success(request.id, json!({"handle":handle,"progress":"cargo-json"})),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
"update" => stalled.push(stream), // Accept the update but never acknowledge it.
|
||||
"end" => {
|
||||
assert_eq!(request.args["publish"]["result"], "failed");
|
||||
write_frame(&mut stream, &Response::success(request.id, json!({}))).unwrap();
|
||||
return stalled.len();
|
||||
}
|
||||
_ => panic!("unexpected publication"),
|
||||
}
|
||||
}
|
||||
panic!("completion was not published");
|
||||
});
|
||||
let output = ctl(&["--socket", socket.to_str().unwrap(), "run", "--", "cargo", "build"])
|
||||
.env("PATH", path)
|
||||
.current_dir(dir.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(19));
|
||||
assert_eq!(output.stdout, b"child output\ndiagnostic\n");
|
||||
assert!(server.join().unwrap() > 0, "probe must exercise progress publishing");
|
||||
let elapsed: f64 = std::fs::read_to_string(dir.path().join("elapsed"))
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap();
|
||||
assert!(elapsed < 1.5, "stdout reader stalled the child for {elapsed}s");
|
||||
}
|
||||
Reference in new issue
Block a user