Format Rust sources at 100 columns

This commit is contained in:
Samuel Onoja committed 2026-10-22 01:28:03 +01:00
1 parent 4661f595f8
commit 610478f8c7
336 files changed
+23497 -7892

No files matched your search

+55 -15
View File
@@ -98,7 +98,11 @@ fn value(
message: &Option<String>,
remaining_ms: Option<u64>,
) -> Result<Option<Value>, Box<dyn Error>> {
if usize::from(progress.is_some()) + usize::from(message.is_some()) + usize::from(remaining_ms.is_some()) > 1 {
if usize::from(progress.is_some())
+ usize::from(message.is_some())
+ usize::from(remaining_ms.is_some())
> 1
{
return Err("choose one activity value".into());
}
Ok(progress
@@ -111,7 +115,11 @@ fn value(
})
}))
}
fn publish(socket: &Path, owner: OwnerScope, publish: Publish) -> Result<serde_json::Value, Box<dyn Error>> {
fn publish(
socket: &Path,
owner: OwnerScope,
publish: Publish,
) -> Result<serde_json::Value, Box<dyn Error>> {
publish_with_timeout(socket, owner, publish, std::time::Duration::from_secs(2))
}
fn publish_with_timeout(
@@ -207,18 +215,28 @@ pub fn command(action: &Action, socket: &Path, json_output: bool) -> Result<(),
},
message: message.clone(),
},
Action::Cancel { local, handle: token } => Publish::Cancel {
Action::Cancel {
local,
handle: token,
} => Publish::Cancel {
local: local.clone(),
handle: handle(token)?,
},
Action::Hook { local, command, cwd } => Publish::Hook {
Action::Hook {
local,
command,
cwd,
} => Publish::Hook {
local: local.clone(),
command: command.clone(),
cwd: cwd.clone(),
},
Action::List => {
let mut stream = std::os::unix::net::UnixStream::connect(socket)?;
return super::output::print(&super::request(&mut stream, "activity-get", json!({}))?, json_output);
return super::output::print(
&super::request(&mut stream, "activity-get", json!({}))?,
json_output,
);
}
};
let result = publish(socket, OwnerScope::Parent, request)?;
@@ -275,9 +293,12 @@ pub fn run(
}
let adapter = (configured_adapter || cargo_json)
&& program == "cargo"
&& args
.get(1)
.is_some_and(|operation| matches!(operation.as_str(), "build" | "check" | "test" | "run" | "clippy"));
&& args.get(1).is_some_and(|operation| {
matches!(
operation.as_str(),
"build" | "check" | "test" | "run" | "clippy"
)
});
let mut command = Command::new(program);
command.args(&args[1..]);
if adapter && !args.iter().any(|arg| arg.starts_with("--message-format")) {
@@ -296,7 +317,11 @@ pub fn run(
command
.stdin(Stdio::inherit())
.stderr(Stdio::inherit())
.stdout(if adapter { Stdio::piped() } else { Stdio::inherit() });
.stdout(if adapter {
Stdio::piped()
} else {
Stdio::inherit()
});
// Terminal-generated signals reach both processes in the shell's job group.
// The wrapper waits for the actual child result; the child resets inherited
// dispositions before exec. SIGTSTP/SIGCONT retain normal job control.
@@ -336,10 +361,15 @@ pub fn run(
let drain_deadline = output::Deadline::default();
let reader = child.stdout.take().map(|stdout| {
let deadline = drain_deadline.clone();
let updates = publisher.as_ref().map(|publisher| publisher.updates.clone());
let updates = publisher
.as_ref()
.map(|publisher| publisher.updates.clone());
let (done, finished) = std::sync::mpsc::sync_channel(1);
std::thread::spawn(move || {
let mut input = io::BufReader::new(output::Output { pipe: stdout, deadline });
let mut input = io::BufReader::new(output::Output {
pipe: stdout,
deadline,
});
let mut stdout = io::stdout().lock();
let mut units = 0_u64;
let mut last = std::time::Instant::now();
@@ -362,7 +392,9 @@ pub fn run(
}
} else if message["reason"] == "compiler-artifact" {
units = units.saturating_add(1);
} else if message["reason"] != "build-finished" && message["reason"] != "build-script-executed" {
} else if message["reason"] != "build-finished"
&& message["reason"] != "build-script-executed"
{
let _ = stdout.write_all(&bytes);
}
if last.elapsed() >= std::time::Duration::from_millis(200) {
@@ -373,7 +405,11 @@ pub fn run(
}
continue;
}
if stdout.write_all(&bytes).and_then(|_| stdout.flush()).is_err() {
if stdout
.write_all(&bytes)
.and_then(|_| stdout.flush())
.is_err()
{
break;
}
}
@@ -384,14 +420,18 @@ pub fn run(
});
let status = child.wait()?;
CHILD.store(0, std::sync::atomic::Ordering::Relaxed);
*drain_deadline.lock().unwrap() = Some(std::time::Instant::now() + std::time::Duration::from_millis(250));
*drain_deadline.lock().unwrap() =
Some(std::time::Instant::now() + std::time::Duration::from_millis(250));
if let Some(reader) = reader {
// Descendants may inherit the pipe after the direct child exits. Drain
// buffered output briefly, but never wait for those descendants' EOF.
let _ = reader.recv_timeout(std::time::Duration::from_millis(350));
}
if let Some(publisher) = publisher {
let result = if matches!(status.signal(), Some(libc::SIGINT | libc::SIGTERM | libc::SIGQUIT)) {
let result = if matches!(
status.signal(),
Some(libc::SIGINT | libc::SIGTERM | libc::SIGQUIT)
) {
State::Cancelled
} else if status.success() {
State::Done
+21 -6
View File
@@ -102,23 +102,34 @@ mod tests {
let (release, released) = mpsc::channel();
let server = std::thread::spawn(move || {
let (mut first, _) = listener.accept().unwrap();
first.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
first
.set_read_timeout(Some(Duration::from_secs(1)))
.unwrap();
let request: Request = read_frame(&mut first).unwrap();
seen.send(()).unwrap();
released.recv_timeout(Duration::from_secs(1)).unwrap();
// The progress timeout can already have elapsed on a loaded host.
let _ = write_frame(&mut first, &Response::success(request.id, serde_json::json!({})));
let _ = write_frame(
&mut first,
&Response::success(request.id, serde_json::json!({})),
);
let (mut latest, _) = listener.accept().unwrap();
let request: Request = read_frame(&mut latest).unwrap();
let wire: ferese_ipc::activities::Request = serde_json::from_value(request.args).unwrap();
let wire: ferese_ipc::activities::Request =
serde_json::from_value(request.args).unwrap();
assert!(
matches!(wire.publish, Publish::Update { value: Some(Value::Text(ref text)), .. } if text == "1000 units compiled")
);
write_frame(&mut latest, &Response::success(request.id, serde_json::json!({}))).unwrap();
write_frame(
&mut latest,
&Response::success(request.id, serde_json::json!({})),
)
.unwrap();
seen.send(()).unwrap();
let (mut end, _) = listener.accept().unwrap();
let request: Request = read_frame(&mut end).unwrap();
let wire: ferese_ipc::activities::Request = serde_json::from_value(request.args).unwrap();
let wire: ferese_ipc::activities::Request =
serde_json::from_value(request.args).unwrap();
assert!(matches!(
wire.publish,
Publish::End {
@@ -126,7 +137,11 @@ mod tests {
..
}
));
write_frame(&mut end, &Response::success(request.id, serde_json::json!({}))).unwrap();
write_frame(
&mut end,
&Response::success(request.id, serde_json::json!({})),
)
.unwrap();
});
let publisher = Publisher::new(
socket,
+51 -15
View File
@@ -8,7 +8,10 @@ fn prepare(source: &str) -> Result<String, Box<dyn Error>> {
return Ok(source.into());
}
if doc.get("panels").is_none() {
doc.set("panels", serde_json::to_value(vec![panel::Panel::default()])?)?;
doc.set(
"panels",
serde_json::to_value(vec![panel::Panel::default()])?,
)?;
}
let panels: Vec<panel::Panel> = serde_json::from_value(doc.get("panels").unwrap().clone())?;
panel::validate(&panels)?;
@@ -25,7 +28,11 @@ fn prepare(source: &str) -> Result<String, Box<dyn Error>> {
continue;
}
let mut id = "activities".to_string();
while groups.iter().flat_map(|group| &group.items).any(|item| item.id.0 == id) {
while groups
.iter()
.flat_map(|group| &group.items)
.any(|item| item.id.0 == id)
{
id.push('-');
}
let item = serde_json::to_value(panel::Item::new(&id, panel::ItemKind::Activities))?;
@@ -43,7 +50,10 @@ fn prepare(source: &str) -> Result<String, Box<dyn Error>> {
} else {
doc.add(
&format!("panels.{index}.end.groups"),
vec![("id".into(), json!("activities")), ("items".into(), json!([item]))],
vec![
("id".into(), json!("activities")),
("items".into(), json!([item])),
],
)?;
}
}
@@ -64,7 +74,10 @@ pub fn configure(path: &Path) -> Result<(), Box<dyn Error>> {
ferese_ipc::activities::now_ms()
));
file.replace(&desired, Some(&backup), |_| Ok(()))?;
println!("Configured Live Activities; previous config: {}", backup.display());
println!(
"Configured Live Activities; previous config: {}",
backup.display()
);
Ok(())
}
@@ -73,20 +86,29 @@ mod tests {
use super::*;
#[test]
fn setup_preserves_layout_comments_rules_and_explicit_disable() {
let source =
"// keep my layout\npanel main { style morph; end { group tools { item activities kind=clock; }; }; }\n";
let source = "// keep my layout\npanel main { style morph; end { group tools { item activities kind=clock; }; }; }\n";
let next = prepare(source).unwrap();
assert!(next.contains("// keep my layout"));
let doc = Document::parse(&next).unwrap();
assert_eq!(doc.get("panels.0.style"), Some(&json!("morph")));
assert_eq!(doc.get("panels.0.end.groups.0.items.0.kind"), Some(&json!("clock")));
assert_eq!(doc.get("panels.0.end.groups.0.items.1.id"), Some(&json!("activities-")));
assert_eq!(
doc.get("panels.0.end.groups.0.items.0.kind"),
Some(&json!("clock"))
);
assert_eq!(
doc.get("panels.0.end.groups.0.items.1.id"),
Some(&json!("activities-"))
);
assert_eq!(prepare(&next).unwrap(), next);
let disabled = "activities { sources { shell-hook #false; }; }\n";
assert_eq!(prepare(disabled).unwrap(), disabled);
let hidden = "panel main { end { group tools { item tasks kind=activities visible=#false; }; }; }\n";
let hidden =
"panel main { end { group tools { item tasks kind=activities visible=#false; }; }; }\n";
let hidden = Document::parse(&prepare(hidden).unwrap()).unwrap();
assert_eq!(hidden.get("panels.0.end.groups.0.items.0.visible"), Some(&json!(false)));
assert_eq!(
hidden.get("panels.0.end.groups.0.items.0.visible"),
Some(&json!(false))
);
assert_eq!(
hidden
.get("panels.0.end.groups.0.items")
@@ -97,9 +119,13 @@ mod tests {
1
);
assert!(prepare("").is_ok());
let partial = Document::parse(&prepare("activities { max-visible 1; }\n").unwrap()).unwrap();
let partial =
Document::parse(&prepare("activities { max-visible 1; }\n").unwrap()).unwrap();
assert_eq!(partial.get("activities.max_visible"), Some(&json!(1)));
assert_eq!(partial.get("activities.sources.shell_hook"), Some(&json!(true)));
assert_eq!(
partial.get("activities.sources.shell_hook"),
Some(&json!(true))
);
assert!(prepare("activities { unknown 1; }").is_err());
}
@@ -113,12 +139,22 @@ mod tests {
let backups: Vec<_> = std::fs::read_dir(directory.path())
.unwrap()
.flatten()
.filter(|file| file.file_name().to_string_lossy().contains(".before-activities-"))
.filter(|file| {
file.file_name()
.to_string_lossy()
.contains(".before-activities-")
})
.collect();
assert_eq!(backups.len(), 1);
assert_eq!(std::fs::read_to_string(backups[0].path()).unwrap(), "// original\n");
assert_eq!(
std::fs::read_to_string(backups[0].path()).unwrap(),
"// original\n"
);
std::fs::write(&path, "activities { bad #true; }\n").unwrap();
assert!(configure(&path).is_err());
assert_eq!(std::fs::read_to_string(&path).unwrap(), "activities { bad #true; }\n");
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
"activities { bad #true; }\n"
);
}
}
+10 -2
View File
@@ -62,7 +62,12 @@ impl Stream {
self.deadline
.checked_duration_since(Instant::now())
.filter(|remaining| !remaining.is_zero())
.ok_or_else(|| io::Error::new(io::ErrorKind::TimedOut, "activity publication deadline expired"))
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::TimedOut,
"activity publication deadline expired",
)
})
}
}
impl Read for Stream {
@@ -107,7 +112,10 @@ mod tests {
});
{
let mut stream = connect(&path, Duration::from_millis(100)).unwrap();
assert!(super::super::super::request(&mut stream, "activity-get", serde_json::json!({})).is_err());
assert!(
super::super::super::request(&mut stream, "activity-get", serde_json::json!({}))
.is_err()
);
}
closed
.recv_timeout(Duration::from_millis(400))
+16 -5
View File
@@ -33,14 +33,20 @@ pub(crate) fn run() -> Result<(), Box<dyn std::error::Error>> {
if system_directories.is_empty() {
system_directories.push(PathBuf::from("/etc/xdg"));
}
let directories = std::iter::once(home).chain(system_directories).collect::<Vec<_>>();
let directories = std::iter::once(home)
.chain(system_directories)
.collect::<Vec<_>>();
let mut entries = BTreeMap::new();
for directory in directories {
let Ok(files) = fs::read_dir(directory.join("autostart")) else {
continue;
};
for file in files.flatten() {
if file.path().extension().is_some_and(|extension| extension == "desktop") {
if file
.path()
.extension()
.is_some_and(|extension| extension == "desktop")
{
entries.entry(file.file_name()).or_insert(file.path());
}
}
@@ -100,7 +106,9 @@ fn read_entry(path: &Path) -> Option<BTreeMap<String, String>> {
return None;
}
let mut source = String::new();
file.take(1024 * 1024 + 1).read_to_string(&mut source).ok()?;
file.take(1024 * 1024 + 1)
.read_to_string(&mut source)
.ok()?;
if source.len() > 1024 * 1024 {
return None;
}
@@ -142,12 +150,15 @@ fn enabled(entry: &BTreeMap<String, String>, desktop: &str) -> bool {
fn available(program: &str) -> bool {
use std::os::unix::fs::PermissionsExt;
let executable = |path: &Path| {
fs::metadata(path).is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
fs::metadata(path)
.is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
};
if program.contains('/') {
executable(Path::new(program))
} else {
env::var_os("PATH").is_some_and(|paths| env::split_paths(&paths).any(|path| executable(&path.join(program))))
env::var_os("PATH").is_some_and(|paths| {
env::split_paths(&paths).any(|path| executable(&path.join(program)))
})
}
}
+19 -6
View File
@@ -273,7 +273,9 @@ impl Command {
Self::CycleColumnWidth => ("cycle-column-width", json!({})),
Self::CenterColumn => ("center-column", json!({})),
Self::ToggleColumnTabbed => ("toggle-column-tabbed", json!({})),
Self::JoinTab { direction } => ("join-tab", json!({"direction": value_name(direction)})),
Self::JoinTab { direction } => {
("join-tab", json!({"direction": value_name(direction)}))
}
Self::LeaveTab => ("leave-tab", json!({})),
Self::NextTab => ("next-tab", json!({})),
Self::PreviousTab => ("previous-tab", json!({})),
@@ -297,9 +299,14 @@ impl Command {
Self::Resize { direction } => ("resize", json!({"direction": value_name(direction)})),
Self::Workspace { index } => ("workspace", json!({"index": index})),
Self::MoveToWorkspace { index } => ("move-to-workspace", json!({"index": index})),
Self::OutputLayout { layout } => ("output-layout", json!({"layout": value_name(layout)})),
Self::OutputLayout { layout } => {
("output-layout", json!({"layout": value_name(layout)}))
}
Self::OutputProfile { name } => ("output-profile", json!({"name": name})),
Self::OutputInternal { state } => ("output-internal", json!({"enabled": matches!(state, Switch::On)})),
Self::OutputInternal { state } => (
"output-internal",
json!({"enabled": matches!(state, Switch::On)}),
),
Self::Screenshot {
geometry: Some(geometry),
} => ("screenshot", json!({"geometry": geometry})),
@@ -315,11 +322,17 @@ impl Command {
Self::Media {
action: None | Some(MediaAction::Get),
} => ("media-get", json!({})),
Self::Media { action: Some(action) } => {
Self::Media {
action: Some(action),
} => {
let args = match action {
MediaAction::Pin { player } => json!({"action": "pin", "player": player}),
MediaAction::Ignore { player } => json!({"action": "ignore", "player": player, "ignored": true}),
MediaAction::Unignore { player } => json!({"action": "ignore", "player": player, "ignored": false}),
MediaAction::Ignore { player } => {
json!({"action": "ignore", "player": player, "ignored": true})
}
MediaAction::Unignore { player } => {
json!({"action": "ignore", "player": player, "ignored": false})
}
MediaAction::PlayPause => json!({"action": "play-pause"}),
MediaAction::Next => json!({"action": "next"}),
MediaAction::Previous => json!({"action": "previous"}),
+68 -19
View File
@@ -71,7 +71,11 @@ fn main() -> Result<(), Box<dyn Error>> {
}
}
fn request(stream: &mut (impl io::Read + io::Write), command: &str, args: Value) -> Result<Value, Box<dyn Error>> {
fn request(
stream: &mut (impl io::Read + io::Write),
command: &str,
args: Value,
) -> Result<Value, Box<dyn Error>> {
write_frame(
stream,
&Request {
@@ -92,10 +96,16 @@ fn request(stream: &mut (impl io::Read + io::Write), command: &str, args: Value)
Ok(response.result.unwrap_or(Value::Null))
}
fn theme_command(action: &ThemeAction, socket: &Path, json_output: bool) -> Result<(), Box<dyn Error>> {
fn theme_command(
action: &ThemeAction,
socket: &Path,
json_output: bool,
) -> Result<(), Box<dyn Error>> {
let mut connection = ferese_ipc::Connection::connect_to(socket)?;
let result = match action {
ThemeAction::Get => serde_json::to_value(connection.get(ferese_config::families::builtins)?)?,
ThemeAction::Get => {
serde_json::to_value(connection.get(ferese_config::families::builtins)?)?
}
ThemeAction::Subscribe => {
let mut snapshot = connection.get(ferese_config::families::builtins)?;
let mut stdout = io::stdout().lock();
@@ -103,7 +113,8 @@ fn theme_command(action: &ThemeAction, socket: &Path, json_output: bool) -> Resu
if !output::write_json_line(&mut stdout, &snapshot)? {
return Ok(());
}
snapshot = connection.watch(snapshot.revision, ferese_config::families::builtins)?;
snapshot =
connection.watch(snapshot.revision, ferese_config::families::builtins)?;
}
}
ThemeAction::Mode { mode } => connection.call(
@@ -139,7 +150,8 @@ fn event_stream(socket: &Path) -> Result<(), Box<dyn Error>> {
#[cfg(test)]
fn parse_args(args: impl IntoIterator<Item = String>) -> Result<(String, Value), String> {
let cli = Cli::try_parse_from(std::iter::once("feresectl".to_owned()).chain(args)).map_err(|e| e.to_string())?;
let cli = Cli::try_parse_from(std::iter::once("feresectl".to_owned()).chain(args))
.map_err(|e| e.to_string())?;
cli.command
.ipc()
.map(|(command, args)| (command.into(), args))
@@ -224,7 +236,8 @@ mod tests {
#[test]
fn a_failed_x11_retry_surfaces_the_compositor_error_code() {
let response = Response::error(1, "x11_unavailable", "X11 is disabled in the configuration");
let response =
Response::error(1, "x11_unavailable", "X11 is disabled in the configuration");
let mut bytes = Vec::new();
write_frame(&mut bytes, &response).expect("encode");
let decoded: Response = read_frame(&mut bytes.as_slice()).expect("decode");
@@ -258,7 +271,9 @@ mod tests {
fn display_mode_commands_validate_layouts() {
for layout in ["internal-only", "external-only", "extend", "mirror"] {
assert_eq!(
parse_args(["output-layout".into(), layout.into()]).unwrap().1,
parse_args(["output-layout".into(), layout.into()])
.unwrap()
.1,
json!({"layout":layout})
);
}
@@ -287,11 +302,15 @@ mod tests {
#[test]
fn parses_direction_and_workspace_commands() {
assert_eq!(
parse_args(["focus".to_owned(), "left".to_owned()]).unwrap().1,
parse_args(["focus".to_owned(), "left".to_owned()])
.unwrap()
.1,
json!({ "direction": "left" })
);
assert_eq!(
parse_args(["workspace".to_owned(), "7".to_owned()]).unwrap().1,
parse_args(["workspace".to_owned(), "7".to_owned()])
.unwrap()
.1,
json!({ "index": 7 })
);
assert_eq!(
@@ -304,7 +323,10 @@ mod tests {
);
assert!(parse_args(["workspace-back-and-forth".into(), "2".into()]).is_err());
for command in ["focus-last-window", "focus-mru-next", "focus-mru-previous"] {
assert_eq!(parse_args([command.into()]).unwrap(), (command.into(), json!({})));
assert_eq!(
parse_args([command.into()]).unwrap(),
(command.into(), json!({}))
);
assert!(parse_args([command.into(), "2".into()]).is_err());
}
}
@@ -343,24 +365,40 @@ mod tests {
#[test]
fn output_commands_have_consistent_arguments() {
for command in ["outputs", "output-profiles", "output-confirm", "output-revert"] {
assert_eq!(parse_args([command.into()]).unwrap(), (command.into(), json!({})));
for command in [
"outputs",
"output-profiles",
"output-confirm",
"output-revert",
] {
assert_eq!(
parse_args([command.into()]).unwrap(),
(command.into(), json!({}))
);
assert!(parse_args([command.into(), "extra".into()]).is_err());
}
assert_eq!(
parse_args(["output-profile".into(), "auto".into()]).unwrap().1,
parse_args(["output-profile".into(), "auto".into()])
.unwrap()
.1,
json!({"name": "auto"})
);
assert_eq!(
parse_args(["output-profile".into(), "desk".into()]).unwrap().1,
parse_args(["output-profile".into(), "desk".into()])
.unwrap()
.1,
json!({"name": "desk"})
);
assert_eq!(
parse_args(["output-internal".into(), "off".into()]).unwrap().1,
parse_args(["output-internal".into(), "off".into()])
.unwrap()
.1,
json!({"enabled": false})
);
assert_eq!(
parse_args(["output-internal".into(), "on".into()]).unwrap().1,
parse_args(["output-internal".into(), "on".into()])
.unwrap()
.1,
json!({"enabled": true})
);
assert!(parse_args(["output-internal".into(), "maybe".into()]).is_err());
@@ -377,15 +415,26 @@ mod tests {
}
assert!(parse_args(["join-tab".into()]).is_err());
assert!(parse_args(["join-tab".into(), "diagonal".into()]).is_err());
for action in ["toggle-column-tabbed", "leave-tab", "next-tab", "previous-tab"] {
assert_eq!(parse_args([action.into()]).unwrap(), (action.into(), json!({})));
for action in [
"toggle-column-tabbed",
"leave-tab",
"next-tab",
"previous-tab",
] {
assert_eq!(
parse_args([action.into()]).unwrap(),
(action.into(), json!({}))
);
assert!(parse_args([action.into(), "extra".into()]).is_err());
}
}
#[test]
fn rejects_unknown_or_missing_arguments() {
assert_eq!(parse_args(["exit".to_owned()]).unwrap(), ("exit".to_owned(), json!({})));
assert_eq!(
parse_args(["exit".to_owned()]).unwrap(),
("exit".to_owned(), json!({}))
);
assert!(parse_args(["exit".to_owned(), "extra".to_owned()]).is_err());
assert!(parse_args(["focus".to_owned()]).is_err());
assert!(parse_args(["reload-config".to_owned()]).is_ok());
+4 -1
View File
@@ -63,7 +63,10 @@ fn terminal_text(text: &str) -> String {
}
/// Return false when a downstream consumer closes stdout.
pub fn write_json_line(writer: &mut impl Write, value: &impl serde::Serialize) -> Result<bool, Box<dyn Error>> {
pub fn write_json_line(
writer: &mut impl Write,
value: &impl serde::Serialize,
) -> Result<bool, Box<dyn Error>> {
let line = serde_json::to_vec(value)?;
match writer
.write_all(&line)