diff --git a/book/AGENTS.md b/book/AGENTS.md index 20b89876..820b25be 100644 --- a/book/AGENTS.md +++ b/book/AGENTS.md @@ -61,6 +61,12 @@ A third review pass fixed: - `outputs.md`: VRR variant3 description said "describes its content as" instead of the correct protocol term "describes its content type as". +A fourth update documented the new `--json` and `--all-json-fields` global +CLI flags, which enable machine-readable JSONL output from all query/status +subcommands. A new "JSON Output" section was added to `cli.md` listing all +supported commands, the JSONL format, field omission behavior, and `jq` +usage examples. + **Future work might include:** - Keeping the book in sync as Jay adds new features or changes behavior. diff --git a/book/src/cli.md b/book/src/cli.md index 9a2905e6..41e3b479 100644 --- a/book/src/cli.md +++ b/book/src/cli.md @@ -20,6 +20,79 @@ Every subcommand accepts a global `--log-level` option (`trace`, `debug`, --- +## JSON Output + +Most query and status commands can output machine-readable JSON instead of +human-readable text. Pass the global `--json` flag before the subcommand: + +```shell +~$ jay --json randr +~$ jay --json clients +~$ jay --json idle +``` + +Each command prints one or more JSON objects, one per line (JSONL format). This +makes it easy to process with tools like `jq`: + +```shell +~$ jay --json randr | jq '.drm_devices[].connectors[].name' +~$ jay --json clients | jq 'select(.pid != null) | .pid' +``` + +By default, fields that are empty arrays, `null`, or `false` are omitted from +the output to reduce noise. To include every field, pass `--all-json-fields`: + +```shell +~$ jay --all-json-fields --json randr +``` + +### Supported Commands + +The following commands support `--json`: + +`jay clients` +: One JSON object per client. + +`jay color-management status` +: Color management enabled/available status. + +`jay config path` +: The config file path as a JSON string. + +`jay idle status` +: Idle interval, grace period, and inhibitors. + +`jay input show`, `jay input seat show`, `jay input device show` +: Seats and input devices with all properties. + +`jay log --path` +: The log file path as a JSON string. + +`jay pid` +: The compositor PID as a JSON number. + +`jay randr show` +: DRM devices, connectors, outputs, modes, and display properties. + +`jay seat-test` +: Streaming JSONL -- one JSON object per input event (key, pointer, touch, + gesture, tablet, switch). + +`jay tree query` +: One JSON object per root node, with children nested recursively. + +`jay version` +: The version string as a JSON value. + +`jay xwayland status` +: Xwayland scaling mode and implied scale. + +> [!TIP] +> Mutating commands (e.g., `jay idle set`, `jay randr output ... enable`) +> produce no output, so `--json` has no effect on them. + +--- + ## Running ### `jay run` diff --git a/book/src/features.md b/book/src/features.md index 9f94f391..77438eab 100644 --- a/book/src/features.md +++ b/book/src/features.md @@ -42,7 +42,8 @@ There is a small but growing integration test suite that is used to ensure this. ## Command-Line Interface Jay has a comprehensive CLI that can be used to inspect and configure the -compositor at runtime: +compositor at runtime. All query commands support `--json` for machine-readable +output: ``` ~$ jay @@ -77,7 +78,9 @@ Commands: Options: --log-level The log level [default: info] [possible values: trace, debug, info, warn, error, off] - -h, --help Print help + --json Output data as JSONL + --all-json-fields Print all fields in JSON output + -h, --help Print help (see more with '--help') ``` See the full [Command-Line Interface](cli.md) reference for details. diff --git a/src/cli.rs b/src/cli.rs index 3a4608e2..ebb11f28 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,6 +8,7 @@ mod duration; mod generate; mod idle; mod input; +mod json; mod log; mod pid; mod quit; @@ -27,8 +28,9 @@ use { crate::{ cli::{ clients::ClientsArgs, color_management::ColorManagementArgs, config::ConfigArgs, - damage_tracking::DamageTrackingArgs, idle::IdleCmd, input::InputArgs, randr::RandrArgs, - reexec::ReexecArgs, run_tagged::RunTaggedArgs, tree::TreeArgs, xwayland::XwaylandArgs, + damage_tracking::DamageTrackingArgs, idle::IdleCmd, input::InputArgs, + json::VERBOSE_JSON, randr::RandrArgs, reexec::ReexecArgs, run_tagged::RunTaggedArgs, + tree::TreeArgs, xwayland::XwaylandArgs, }, compositor::{LogLevel, start_compositor}, format::{Format, ref_formats}, @@ -37,6 +39,7 @@ use { }, clap::{Args, Parser, Subcommand, ValueEnum, ValueHint, builder::PossibleValue}, clap_complete::Shell, + std::sync::atomic::Ordering::Relaxed, }; /// A wayland compositor. @@ -53,6 +56,14 @@ pub struct GlobalArgs { /// The log level. #[clap(value_enum, long, default_value_t)] pub log_level: LogLevel, + /// Output data as JSONL. + #[clap(long)] + pub json: bool, + /// Print all fields in JSON output. + /// + /// By default, some fields that are empty arrays, null, or false are omitted. + #[clap(long)] + pub all_json_fields: bool, } #[derive(Subcommand, Debug)] @@ -225,6 +236,9 @@ pub fn main() { if not_matches!(cli.command, Cmd::Run(_)) { drop_all_pr_caps(); } + if cli.global.all_json_fields { + VERBOSE_JSON.store(true, Relaxed); + } match cli.command { Cmd::Run(a) => start_compositor(cli.global, a), Cmd::GenerateCompletion(g) => generate::main(g), diff --git a/src/cli/clients.rs b/src/cli/clients.rs index c045245b..6af73f46 100644 --- a/src/cli/clients.rs +++ b/src/cli/clients.rs @@ -1,6 +1,9 @@ use { crate::{ - cli::GlobalArgs, + cli::{ + GlobalArgs, + json::{JsonClient, jsonl}, + }, tools::tool_client::{Handle, ToolClient, with_tool_client}, wire::{JayClientQueryId, jay_client_query, jay_compositor}, }, @@ -69,7 +72,7 @@ struct KillIdArgs { pub fn main(global: GlobalArgs, clients_args: ClientsArgs) { with_tool_client(global.log_level, |tc| async move { let clients = Rc::new(Clients { tc: tc.clone() }); - clients.run(clients_args).await; + clients.run(&global, clients_args).await; }); } @@ -78,7 +81,7 @@ struct Clients { } impl Clients { - async fn run(&self, args: ClientsArgs) { + async fn run(&self, global: &GlobalArgs, args: ClientsArgs) { let tc = &self.tc; let comp = tc.jay_compositor().await; let cmd = args @@ -113,13 +116,20 @@ impl Clients { let clients = handle_client_query(tc, id).await; let mut clients = clients.values().collect::>(); clients.sort_by_key(|c| c.id); - let mut prefix = " ".to_string(); - let mut printer = ClientPrinter { - prefix: &mut prefix, - }; - for client in clients { - println!("- client:"); - printer.print_client(client); + if global.json { + for client in clients { + let client = make_json_client(client); + jsonl(&client); + } + } else { + let mut prefix = " ".to_string(); + let mut printer = ClientPrinter { + prefix: &mut prefix, + }; + for client in clients { + println!("- client:"); + printer.print_client(client); + } } } ClientsCmd::Kill(a) => match a.cmd { @@ -246,3 +256,19 @@ impl ClientPrinter<'_> { opt!(tag, "tag"); } } + +pub fn make_json_client(client: &Client) -> JsonClient<'_> { + JsonClient { + client_id: client.id, + sandboxed: client.sandboxed, + sandbox_engine: client.sandbox_engine.as_deref(), + sandbox_app_id: client.sandbox_app_id.as_deref(), + sandbox_instance_id: client.sandbox_instance_id.as_deref(), + uid: client.uid, + pid: client.pid, + is_xwayland: client.is_xwayland, + comm: client.comm.as_deref(), + exe: client.exe.as_deref(), + tag: client.tag.as_deref(), + } +} diff --git a/src/cli/color_management.rs b/src/cli/color_management.rs index d23235cb..75908a84 100644 --- a/src/cli/color_management.rs +++ b/src/cli/color_management.rs @@ -1,6 +1,9 @@ use { crate::{ - cli::GlobalArgs, + cli::{ + GlobalArgs, + json::{JsonColorManagementStatus, jsonl}, + }, tools::tool_client::{Handle, ToolClient, with_tool_client}, wire::{JayColorManagementId, jay_color_management, jay_compositor}, }, @@ -28,7 +31,7 @@ pub enum ColorManagementCmd { pub fn main(global: GlobalArgs, args: ColorManagementArgs) { with_tool_client(global.log_level, |tc| async move { let cm = ColorManagement { tc: tc.clone() }; - cm.run(args).await; + cm.run(&global, args).await; }); } @@ -37,19 +40,19 @@ struct ColorManagement { } impl ColorManagement { - async fn run(self, args: ColorManagementArgs) { + async fn run(self, global: &GlobalArgs, args: ColorManagementArgs) { let tc = &self.tc; let comp = tc.jay_compositor().await; let id = tc.id(); tc.send(jay_compositor::GetColorManagement { self_id: comp, id }); match args.command.unwrap_or_default() { - ColorManagementCmd::Status => self.status(id).await, + ColorManagementCmd::Status => self.status(global, id).await, ColorManagementCmd::Enable => self.set_enabled(id, true).await, ColorManagementCmd::Disable => self.set_enabled(id, false).await, } } - async fn status(self, id: JayColorManagementId) { + async fn status(self, global: &GlobalArgs, id: JayColorManagementId) { let tc = &self.tc; tc.send(jay_color_management::Get { self_id: id }); let enabled = Rc::new(Cell::new(false)); @@ -61,14 +64,21 @@ impl ColorManagement { iv.set(msg.available != 0); }); tc.round_trip().await; - if enabled.get() { - print!("Enabled"); - if !available.get() { - print!(" (Unavailable)"); - } - println!(); + if global.json { + jsonl(&JsonColorManagementStatus { + enabled: enabled.get(), + available: available.get(), + }); } else { - println!("Disabled"); + if enabled.get() { + print!("Enabled"); + if !available.get() { + print!(" (Unavailable)"); + } + println!(); + } else { + println!("Disabled"); + } } } diff --git a/src/cli/config.rs b/src/cli/config.rs index f6aaa3da..b5de8def 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -1,5 +1,10 @@ use { - crate::{cli::GlobalArgs, compositor::config_dir, logger::Logger, utils::errorfmt::ErrorFmt}, + crate::{ + cli::{GlobalArgs, json::jsonl}, + compositor::config_dir, + logger::Logger, + utils::errorfmt::ErrorFmt, + }, clap::{Args, Subcommand}, jay_toml_config::CONFIG_TOML, std::path::Path, @@ -87,7 +92,12 @@ pub fn main(global: GlobalArgs, args: ConfigArgs) { ConfigCmd::Path => { let dir = dir(); let toml_path = Path::new(&dir).join(CONFIG_TOML); - println!("{}", toml_path.display()); + if global.json { + let path = toml_path.display().to_string(); + jsonl(&path); + } else { + println!("{}", toml_path.display()); + } } ConfigCmd::OpenDir => { const XDG_OPEN: &str = "xdg-open"; diff --git a/src/cli/idle.rs b/src/cli/idle.rs index d2c001e4..b11e35c1 100644 --- a/src/cli/idle.rs +++ b/src/cli/idle.rs @@ -1,6 +1,10 @@ use { crate::{ - cli::{GlobalArgs, IdleArgs, duration::parse_duration}, + cli::{ + GlobalArgs, IdleArgs, + duration::parse_duration, + json::{JsonIdle, JsonIdleInhibitor, jsonl}, + }, tools::tool_client::{Handle, ToolClient, with_tool_client}, utils::stack::Stack, wire::{JayIdleId, WlSurfaceId, jay_compositor, jay_idle}, @@ -53,7 +57,7 @@ pub struct IdleSetGracePeriodArgs { pub fn main(global: GlobalArgs, args: IdleArgs) { with_tool_client(global.log_level, |tc| async move { let idle = Idle { tc: tc.clone() }; - idle.run(args).await; + idle.run(&global, args).await; }); } @@ -62,7 +66,7 @@ struct Idle { } impl Idle { - async fn run(self, args: IdleArgs) { + async fn run(self, global: &GlobalArgs, args: IdleArgs) { let tc = &self.tc; let comp = tc.jay_compositor().await; let idle = tc.id(); @@ -71,13 +75,13 @@ impl Idle { id: idle, }); match args.command.unwrap_or_default() { - IdleCmd::Status => self.status(idle).await, + IdleCmd::Status => self.status(global, idle).await, IdleCmd::Set(args) => self.set(idle, args).await, IdleCmd::SetGracePeriod(args) => self.set_grace_period(idle, args).await, } } - async fn status(self, idle: JayIdleId) { + async fn status(self, global: &GlobalArgs, idle: JayIdleId) { let tc = &self.tc; tc.send(jay_idle::GetStatus { self_id: idle }); let timeout = Rc::new(Cell::new(0u64)); @@ -90,7 +94,7 @@ impl Idle { }); struct Inhibitor { surface: WlSurfaceId, - _client_id: u64, + client_id: u64, pid: u64, comm: String, } @@ -98,47 +102,63 @@ impl Idle { jay_idle::Inhibitor::handle(tc, idle, inhibitors.clone(), |iv, msg| { iv.push(Inhibitor { surface: msg.surface, - _client_id: msg.client_id, + client_id: msg.client_id, pid: msg.pid, comm: msg.comm.to_string(), }); }); tc.round_trip().await; - let interval = |iv: u64| { - fmt::from_fn(move |f| { - let minutes = iv / 60; - let seconds = iv % 60; - if minutes == 0 && seconds == 0 { - write!(f, " disabled")?; - } else { - if minutes > 0 { - write!(f, " {} minute", minutes)?; - if minutes > 1 { - write!(f, "s")?; - } - } - if seconds > 0 { - write!(f, " {} second", seconds)?; - if seconds > 1 { - write!(f, "s")?; - } - } - } - Ok(()) - }) - }; - println!("Interval:{}", interval(timeout.get())); - println!("Grace period:{}", interval(grace.get())); let mut inhibitors = inhibitors.take(); - inhibitors.sort_by_key(|i| i.pid); - inhibitors.sort_by_key(|i| i.surface); - if inhibitors.len() > 0 { - println!("Inhibitors:"); - for inhibitor in inhibitors { - println!( - " {}, surface {}, pid {}", - inhibitor.comm, inhibitor.surface, inhibitor.pid - ); + inhibitors.sort_by_key(|i| (i.pid, i.surface)); + if global.json { + let mut json = JsonIdle { + idle_sec: timeout.get(), + grace_sec: grace.get(), + inhibitors: vec![], + }; + for inhibitor in &inhibitors { + json.inhibitors.push(JsonIdleInhibitor { + surface: inhibitor.surface.raw(), + client_id: inhibitor.client_id, + pid: inhibitor.pid, + comm: &inhibitor.comm, + }); + } + jsonl(&json); + } else { + let interval = |iv: u64| { + fmt::from_fn(move |f| { + let minutes = iv / 60; + let seconds = iv % 60; + if minutes == 0 && seconds == 0 { + write!(f, " disabled")?; + } else { + if minutes > 0 { + write!(f, " {} minute", minutes)?; + if minutes > 1 { + write!(f, "s")?; + } + } + if seconds > 0 { + write!(f, " {} second", seconds)?; + if seconds > 1 { + write!(f, "s")?; + } + } + } + Ok(()) + }) + }; + println!("Interval:{}", interval(timeout.get())); + println!("Grace period:{}", interval(grace.get())); + if inhibitors.len() > 0 { + println!("Inhibitors:"); + for inhibitor in inhibitors { + println!( + " {}, surface {}, pid {}", + inhibitor.comm, inhibitor.surface, inhibitor.pid + ); + } } } } diff --git a/src/cli/input.rs b/src/cli/input.rs index 2df25758..7b1132fc 100644 --- a/src/cli/input.rs +++ b/src/cli/input.rs @@ -1,7 +1,10 @@ use { crate::{ backend::{InputDeviceAccelProfile, InputDeviceCapability, InputDeviceClickMethod}, - cli::GlobalArgs, + cli::{ + GlobalArgs, + json::{JsonInputData, JsonInputDevice, JsonSeat, jsonl}, + }, clientmem::ClientMem, libinput::consts::{ ConfigClickMethod, LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE, @@ -326,7 +329,7 @@ pub struct UseHardwareCursorArgs { pub fn main(global: GlobalArgs, args: InputArgs) { with_tool_client(global.log_level, |tc| async move { let idle = Rc::new(Input { tc: tc.clone() }); - idle.run(args).await; + idle.run(&global, args).await; }); } @@ -372,7 +375,7 @@ struct Input { } impl Input { - async fn run(self: &Rc, args: InputArgs) { + async fn run(self: &Rc, global: &GlobalArgs, args: InputArgs) { let tc = &self.tc; let comp = tc.jay_compositor().await; let input = tc.id(); @@ -381,9 +384,9 @@ impl Input { id: input, }); match args.command.unwrap_or_default() { - InputCmd::Show(args) => self.show(input, args).await, - InputCmd::Seat(args) => self.seat(input, args).await, - InputCmd::Device(args) => self.device(input, args).await, + InputCmd::Show(args) => self.show(global, input, args).await, + InputCmd::Seat(args) => self.seat(global, input, args).await, + InputCmd::Device(args) => self.device(global, input, args).await, } } @@ -436,7 +439,7 @@ impl Input { data.take() } - async fn seat(self: &Rc, input: JayInputId, args: SeatArgs) { + async fn seat(self: &Rc, global: &GlobalArgs, input: JayInputId, args: SeatArgs) { let tc = &self.tc; match args.command.unwrap_or_default() { SeatCommand::Show(a) => { @@ -448,7 +451,11 @@ impl Input { name: &args.seat, }); let data = self.get(input).await; - self.print_data(data, a.verbose); + if global.json { + self.print_data_json(data); + } else { + self.print_data(data, a.verbose); + } } SeatCommand::SetRepeatRate(a) => { self.handle_error(input, |e| { @@ -543,7 +550,7 @@ impl Input { tc.round_trip().await; } - async fn device(self: &Rc, input: JayInputId, args: DeviceArgs) { + async fn device(self: &Rc, global: &GlobalArgs, input: JayInputId, args: DeviceArgs) { let tc = &self.tc; match args.command.unwrap_or_default() { DeviceCommand::Show => { @@ -555,8 +562,12 @@ impl Input { id: args.device, }); let data = self.get(input).await; - for device in &data.input_device { - self.print_device("", true, device); + if global.json { + self.print_data_json(data); + } else { + for device in &data.input_device { + self.print_device("", true, device); + } } } DeviceCommand::SetAccelProfile(a) => { @@ -779,10 +790,14 @@ impl Input { tc.round_trip().await; } - async fn show(self: &Rc, input: JayInputId, args: ShowArgs) { + async fn show(self: &Rc, global: &GlobalArgs, input: JayInputId, args: ShowArgs) { self.tc.send(jay_input::GetAll { self_id: input }); let data = self.get(input).await; - self.print_data(data, args.verbose); + if global.json { + self.print_data_json(data); + } else { + self.print_data(data, args.verbose); + } } fn print_data(self: &Rc, mut data: Data, verbose: bool) { @@ -911,6 +926,38 @@ impl Input { } } + fn print_data_json(&self, mut data: Data) { + data.seats.sort_by(|l, r| l.name.cmp(&r.name)); + data.input_device.sort_by_key(|l| l.id); + let mut seats = Vec::new(); + for seat in &data.seats { + let devices = data + .input_device + .iter() + .filter(|c| c.seat.as_ref() == Some(&seat.name)) + .map(make_json_device) + .collect(); + seats.push(JsonSeat { + name: &seat.name, + repeat_rate: seat.repeat_rate, + repeat_delay: seat.repeat_delay, + hardware_cursor: seat.hardware_cursor, + devices, + }); + } + let detached_devices = data + .input_device + .iter() + .filter(|c| c.seat.is_none()) + .map(make_json_device) + .collect(); + let json = JsonInputData { + seats, + detached_devices, + }; + jsonl(&json); + } + async fn get(self: &Rc, input: JayInputId) -> Data { let tc = &self.tc; let data = Rc::new(RefCell::new(Data::default())); @@ -1018,3 +1065,27 @@ impl Input { data.borrow_mut().clone() } } + +fn make_json_device(device: &InputDevice) -> JsonInputDevice<'_> { + JsonInputDevice { + input_device_id: device.id, + name: &device.name, + seat: device.seat.as_deref(), + syspath: device.syspath.as_deref(), + devnode: device.devnode.as_deref(), + capabilities: device.capabilities.iter().map(|c| c.text()).collect(), + accel_profile: device.accel_profile.as_ref().map(|v| v.text()), + accel_speed: device.accel_speed, + tap_enabled: device.tap_enabled, + tap_drag_enabled: device.tap_drag_enabled, + tap_drag_lock_enabled: device.tap_drag_lock_enabled, + left_handed: device.left_handed, + natural_scrolling: device.natural_scrolling_enabled, + px_per_wheel_scroll: device.px_per_wheel_scroll, + transform_matrix: device.transform_matrix, + output: device.output.as_deref(), + calibration_matrix: device.calibration_matrix, + click_method: device.click_method.as_ref().map(|v| v.text()), + middle_button_emulation: device.middle_button_emulation_enabled, + } +} diff --git a/src/cli/json.rs b/src/cli/json.rs new file mode 100644 index 00000000..f7369a54 --- /dev/null +++ b/src/cli/json.rs @@ -0,0 +1,606 @@ +use { + crate::ifs::jay_tree_query::{ + TREE_TY_CONTAINER, TREE_TY_DISPLAY, TREE_TY_FLOAT, TREE_TY_LAYER_SURFACE, + TREE_TY_LOCK_SURFACE, TREE_TY_OUTPUT, TREE_TY_PLACEHOLDER, TREE_TY_WORKSPACE, + TREE_TY_X_WINDOW, TREE_TY_XDG_POPUP, TREE_TY_XDG_TOPLEVEL, + }, + jay_config::video::{TearingMode, VrrMode}, + num_traits::Zero, + serde::{Serialize, Serializer}, + std::{ + io::{Write, stdout}, + sync::atomic::{AtomicBool, Ordering::Relaxed}, + }, + uapi::c, +}; + +pub static VERBOSE_JSON: AtomicBool = AtomicBool::new(false); + +fn quiet() -> bool { + !VERBOSE_JSON.load(Relaxed) +} + +fn is_none(t: &Option) -> bool { + quiet() && t.is_none() +} + +fn is_empty(t: &[T]) -> bool { + quiet() && t.is_empty() +} + +fn is_false(v: &bool) -> bool { + quiet() && !*v +} + +fn is_zero(v: &impl Zero) -> bool { + quiet() && v.is_zero() +} + +pub fn jsonl(value: &T) +where + T: ?Sized + Serialize, +{ + let mut writer = stdout().lock(); + serde_json::to_writer(&mut writer, value).unwrap(); + writer.write_all(b"\n").unwrap(); +} + +#[derive(Serialize)] +pub struct JsonClient<'a> { + pub client_id: u64, + #[serde(skip_serializing_if = "is_false")] + pub sandboxed: bool, + #[serde(skip_serializing_if = "is_none")] + pub sandbox_engine: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub sandbox_app_id: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub sandbox_instance_id: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub uid: Option, + #[serde(skip_serializing_if = "is_none")] + pub pid: Option, + #[serde(skip_serializing_if = "is_false")] + pub is_xwayland: bool, + #[serde(skip_serializing_if = "is_none")] + pub comm: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub exe: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub tag: Option<&'a str>, +} + +#[derive(Serialize)] +pub struct JsonColorManagementStatus { + pub enabled: bool, + pub available: bool, +} + +#[derive(Serialize)] +pub struct JsonIdle<'a> { + pub idle_sec: u64, + #[serde(skip_serializing_if = "is_zero")] + pub grace_sec: u64, + #[serde(skip_serializing_if = "is_empty")] + pub inhibitors: Vec>, +} + +#[derive(Serialize)] +pub struct JsonIdleInhibitor<'a> { + pub client_id: u64, + pub surface: u32, + pub pid: u64, + pub comm: &'a str, +} + +#[derive(Serialize)] +pub struct JsonRandrData<'a> { + #[serde(skip_serializing_if = "is_empty")] + pub drm_devices: Vec>, + #[serde(skip_serializing_if = "is_empty")] + pub unbound_connectors: Vec>, +} + +#[derive(Serialize)] +pub struct JsonDrmDevice<'a> { + pub devnode: &'a str, + pub syspath: &'a str, + pub vendor: u32, + pub vendor_name: &'a str, + pub model: u32, + pub model_name: &'a str, + pub gfx_api: &'a str, + #[serde(skip_serializing_if = "is_false")] + pub render_device: bool, + #[serde(skip_serializing_if = "is_empty")] + pub connectors: Vec>, +} + +#[derive(Serialize)] +pub struct JsonConnector<'a> { + pub name: &'a str, + pub enabled: bool, + #[serde(skip_serializing_if = "is_none")] + pub output: Option>, +} + +pub struct JsonVrrMode(pub VrrMode); + +impl Serialize for JsonVrrMode { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let s = match self.0 { + VrrMode::NEVER => "never", + VrrMode::ALWAYS => "always", + VrrMode::VARIANT_1 => "variant1", + VrrMode::VARIANT_2 => "variant2", + VrrMode::VARIANT_3 => "variant3", + n => return serializer.serialize_u32(n.0), + }; + serializer.serialize_str(s) + } +} + +pub struct JsonTearingMode(pub TearingMode); + +impl Serialize for JsonTearingMode { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let s = match self.0 { + TearingMode::NEVER => "never", + TearingMode::ALWAYS => "always", + TearingMode::VARIANT_1 => "variant1", + TearingMode::VARIANT_2 => "variant2", + TearingMode::VARIANT_3 => "variant3", + n => return serializer.serialize_u32(n.0), + }; + serializer.serialize_str(s) + } +} + +#[derive(Serialize)] +pub struct JsonOutput<'a> { + pub product: &'a str, + pub manufacturer: &'a str, + pub serial_number: &'a str, + #[serde(skip_serializing_if = "is_zero")] + pub width_mm: i32, + #[serde(skip_serializing_if = "is_zero")] + pub height_mm: i32, + #[serde(skip_serializing_if = "is_false")] + pub non_desktop: bool, + pub scale: f64, + pub x: i32, + pub y: i32, + pub width: i32, + pub height: i32, + pub transform: &'static str, + #[serde(skip_serializing_if = "is_none")] + pub mode: Option, + #[serde(skip_serializing_if = "is_none")] + pub format: Option<&'a str>, + #[serde(skip_serializing_if = "is_false")] + pub vrr_capable: bool, + #[serde(skip_serializing_if = "is_false")] + pub vrr_enabled: bool, + pub vrr_mode: JsonVrrMode, + #[serde(skip_serializing_if = "is_none")] + pub vrr_cursor_hz: Option, + pub tearing_mode: JsonTearingMode, + #[serde(skip_serializing_if = "is_none")] + pub flip_margin_ns: Option, + #[serde(skip_serializing_if = "is_empty")] + pub supported_color_spaces: Vec<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub current_color_space: Option<&'a str>, + #[serde(skip_serializing_if = "is_empty")] + pub supported_eotfs: Vec<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub current_eotf: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub min_brightness: Option, + #[serde(skip_serializing_if = "is_none")] + pub max_brightness: Option, + #[serde(skip_serializing_if = "is_none")] + pub brightness: Option, + #[serde(skip_serializing_if = "is_none")] + pub blend_space: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub native_gamut: Option, + #[serde(skip_serializing_if = "is_false")] + pub use_native_gamut: bool, + #[serde(skip_serializing_if = "is_false")] + pub arbitrary_modes: bool, + #[serde(skip_serializing_if = "is_empty")] + pub modes: Vec, + #[serde(skip_serializing_if = "is_empty")] + pub formats: Vec<&'a str>, +} + +#[derive(Serialize)] +pub struct JsonMode { + pub width: i32, + pub height: i32, + pub refresh_rate_millihz: u32, + #[serde(skip_serializing_if = "is_false")] + pub current: bool, +} + +#[derive(Serialize)] +pub struct JsonPrimaries { + pub r_x: f64, + pub r_y: f64, + pub g_x: f64, + pub g_y: f64, + pub b_x: f64, + pub b_y: f64, + pub w_x: f64, + pub w_y: f64, +} + +#[derive(Serialize)] +pub struct JsonInputData<'a> { + #[serde(skip_serializing_if = "is_empty")] + pub seats: Vec>, + #[serde(skip_serializing_if = "is_empty")] + pub detached_devices: Vec>, +} + +#[derive(Serialize)] +pub struct JsonSeat<'a> { + pub name: &'a str, + pub repeat_rate: i32, + pub repeat_delay: i32, + #[serde(skip_serializing_if = "is_false")] + pub hardware_cursor: bool, + #[serde(skip_serializing_if = "is_empty")] + pub devices: Vec>, +} + +#[derive(Serialize)] +pub struct JsonInputDevice<'a> { + pub input_device_id: u32, + pub name: &'a str, + #[serde(skip_serializing_if = "is_none")] + pub seat: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub syspath: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub devnode: Option<&'a str>, + #[serde(skip_serializing_if = "is_empty")] + pub capabilities: Vec<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub accel_profile: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub accel_speed: Option, + #[serde(skip_serializing_if = "is_none")] + pub tap_enabled: Option, + #[serde(skip_serializing_if = "is_none")] + pub tap_drag_enabled: Option, + #[serde(skip_serializing_if = "is_none")] + pub tap_drag_lock_enabled: Option, + #[serde(skip_serializing_if = "is_none")] + pub left_handed: Option, + #[serde(skip_serializing_if = "is_none")] + pub natural_scrolling: Option, + #[serde(skip_serializing_if = "is_none")] + pub px_per_wheel_scroll: Option, + #[serde(skip_serializing_if = "is_none")] + pub transform_matrix: Option<[[f64; 2]; 2]>, + #[serde(skip_serializing_if = "is_none")] + pub output: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub calibration_matrix: Option<[[f32; 3]; 2]>, + #[serde(skip_serializing_if = "is_none")] + pub click_method: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub middle_button_emulation: Option, +} + +pub struct JsonTreeNodeType(pub u32); + +impl Serialize for JsonTreeNodeType { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let s = match self.0 { + TREE_TY_DISPLAY => "display", + TREE_TY_OUTPUT => "output", + TREE_TY_WORKSPACE => "workspace", + TREE_TY_FLOAT => "float", + TREE_TY_CONTAINER => "container", + TREE_TY_PLACEHOLDER => "placeholder", + TREE_TY_XDG_TOPLEVEL => "xdg-toplevel", + TREE_TY_X_WINDOW => "x-window", + TREE_TY_XDG_POPUP => "xdg-popup", + TREE_TY_LAYER_SURFACE => "layer-surface", + TREE_TY_LOCK_SURFACE => "lock-surface", + n => return serializer.serialize_u32(n), + }; + serializer.serialize_str(s) + } +} + +#[derive(Serialize)] +pub struct JsonTreeNode<'a> { + #[serde(rename = "type")] + pub ty: JsonTreeNodeType, + #[serde(skip_serializing_if = "is_none")] + pub output: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub workspace: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub toplevel_id: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub placeholder_for: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub position: Option, + #[serde(skip_serializing_if = "is_none")] + pub client: Option>, + #[serde(skip_serializing_if = "is_none")] + pub title: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub app_id: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub tag: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub content_type: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub x_class: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub x_instance: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + pub x_role: Option<&'a str>, + #[serde(skip_serializing_if = "is_false")] + pub floating: bool, + #[serde(skip_serializing_if = "is_false")] + pub visible: bool, + #[serde(skip_serializing_if = "is_false")] + pub urgent: bool, + #[serde(skip_serializing_if = "is_false")] + pub fullscreen: bool, + #[serde(skip_serializing_if = "is_empty")] + pub children: Vec>, +} + +#[derive(Serialize)] +pub struct JsonRect { + pub x1: i32, + pub y1: i32, + pub x2: i32, + pub y2: i32, + pub width: i32, + pub height: i32, +} + +#[derive(Serialize)] +pub struct JsonXwaylandStatus<'a> { + pub scaling_mode: &'a str, + #[serde(skip_serializing_if = "is_none")] + pub implied_scale: Option, +} + +#[derive(Serialize)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum JsonSeatEvent<'a> { + Key { + seat: &'a str, + time_usec: u64, + key: u32, + state: u32, + }, + Modifiers { + seat: &'a str, + modifiers: u32, + group: u32, + }, + PointerAbs { + seat: &'a str, + time_usec: u64, + x: f64, + y: f64, + }, + PointerRel { + seat: &'a str, + time_usec: u64, + x: f64, + y: f64, + dx: f64, + dy: f64, + dx_unaccelerated: f64, + dy_unaccelerated: f64, + }, + Button { + seat: &'a str, + time_usec: u64, + button: u32, + state: u32, + }, + Axis { + seat: &'a str, + time_usec: u64, + #[serde(skip_serializing_if = "is_none")] + source: Option<&'a str>, + #[serde(skip_serializing_if = "is_none")] + horizontal: Option, + #[serde(skip_serializing_if = "is_none")] + vertical: Option, + }, + SwipeBegin { + seat: &'a str, + time_usec: u64, + fingers: u32, + }, + SwipeUpdate { + seat: &'a str, + time_usec: u64, + dx: f64, + dy: f64, + dx_unaccelerated: f64, + dy_unaccelerated: f64, + }, + SwipeEnd { + seat: &'a str, + time_usec: u64, + cancelled: bool, + }, + PinchBegin { + seat: &'a str, + time_usec: u64, + fingers: u32, + }, + PinchUpdate { + seat: &'a str, + time_usec: u64, + dx: f64, + dy: f64, + dx_unaccelerated: f64, + dy_unaccelerated: f64, + scale: f64, + rotation: f64, + }, + PinchEnd { + seat: &'a str, + time_usec: u64, + cancelled: bool, + }, + HoldBegin { + seat: &'a str, + time_usec: u64, + fingers: u32, + }, + HoldEnd { + seat: &'a str, + time_usec: u64, + cancelled: bool, + }, + Switch { + seat: &'a str, + time_usec: u64, + input_device: u32, + event: &'a str, + }, + TabletTool { + seat: &'a str, + time_usec: u64, + input_device: u32, + tool: u32, + #[serde(skip_serializing_if = "is_false")] + proximity_in: bool, + #[serde(skip_serializing_if = "is_false")] + proximity_out: bool, + #[serde(skip_serializing_if = "is_false")] + down: bool, + #[serde(skip_serializing_if = "is_false")] + up: bool, + #[serde(skip_serializing_if = "is_none")] + x: Option, + #[serde(skip_serializing_if = "is_none")] + y: Option, + #[serde(skip_serializing_if = "is_none")] + pressure: Option, + #[serde(skip_serializing_if = "is_none")] + distance: Option, + #[serde(skip_serializing_if = "is_none")] + tilt_x: Option, + #[serde(skip_serializing_if = "is_none")] + tilt_y: Option, + #[serde(skip_serializing_if = "is_none")] + rotation: Option, + #[serde(skip_serializing_if = "is_none")] + slider: Option, + #[serde(skip_serializing_if = "is_none")] + wheel_degrees: Option, + #[serde(skip_serializing_if = "is_none")] + wheel_clicks: Option, + #[serde(skip_serializing_if = "is_none")] + button: Option, + #[serde(skip_serializing_if = "is_none")] + button_state: Option<&'a str>, + }, + TabletPadModeSwitch { + seat: &'a str, + time_usec: u64, + input_device: u32, + mode: u32, + }, + TabletPadButton { + seat: &'a str, + time_usec: u64, + input_device: u32, + button: u32, + state: &'a str, + }, + TabletPadStrip { + seat: &'a str, + time_usec: u64, + input_device: u32, + strip: u32, + source: &'a str, + #[serde(skip_serializing_if = "is_none")] + position: Option, + #[serde(skip_serializing_if = "is_false")] + stop: bool, + }, + TabletPadRing { + seat: &'a str, + time_usec: u64, + input_device: u32, + ring: u32, + source: &'a str, + #[serde(skip_serializing_if = "is_none")] + degrees: Option, + #[serde(skip_serializing_if = "is_false")] + stop: bool, + }, + TabletPadDial { + seat: &'a str, + time_usec: u64, + input_device: u32, + dial: u32, + #[serde(skip_serializing_if = "is_none")] + delta120: Option, + }, + TouchDown { + seat: &'a str, + time_usec: u64, + id: i32, + x: f64, + y: f64, + }, + TouchUp { + seat: &'a str, + time_usec: u64, + id: i32, + }, + TouchMotion { + seat: &'a str, + time_usec: u64, + id: i32, + x: f64, + y: f64, + }, + TouchCancel { + seat: &'a str, + time_usec: u64, + id: i32, + }, +} + +#[derive(Serialize)] +pub struct JsonAxisData { + #[serde(skip_serializing_if = "is_none")] + pub px: Option, + #[serde(skip_serializing_if = "is_none")] + pub v120: Option, + #[serde(skip_serializing_if = "is_false")] + pub stop: bool, + #[serde(skip_serializing_if = "is_false")] + pub natural_scrolling: bool, +} diff --git a/src/cli/log.rs b/src/cli/log.rs index ece70ae6..e9d4f883 100644 --- a/src/cli/log.rs +++ b/src/cli/log.rs @@ -1,6 +1,6 @@ use { crate::{ - cli::{GlobalArgs, LogArgs}, + cli::{GlobalArgs, LogArgs, json::jsonl}, tools::tool_client::{Handle, ToolClient, with_tool_client}, utils::errorfmt::ErrorFmt, wire::{jay_compositor, jay_log_file}, @@ -24,7 +24,7 @@ pub fn main(global: GlobalArgs, args: LogArgs) { path: RefCell::new(None), args, }); - run(logger).await; + run(&global, logger).await; }); } @@ -34,7 +34,7 @@ struct Log { args: LogArgs, } -async fn run(log: Rc) { +async fn run(global: &GlobalArgs, log: Rc) { let tc = &log.tc; let comp = tc.jay_compositor().await; let log_file = tc.id(); @@ -52,7 +52,12 @@ async fn run(log: Rc) { _ => fatal!("Server did not send the path of the log file"), }; if log.args.path { - println!("{}", path); + if global.json { + let path = path.to_string(); + jsonl(&path); + } else { + println!("{}", path); + } process::exit(0); } let mut command = Command::new("less"); diff --git a/src/cli/pid.rs b/src/cli/pid.rs index ce75262f..dd95c2d7 100644 --- a/src/cli/pid.rs +++ b/src/cli/pid.rs @@ -1,6 +1,6 @@ use { crate::{ - cli::GlobalArgs, + cli::{GlobalArgs, json::jsonl}, tools::tool_client::{Handle, ToolClient, with_tool_client}, wire::jay_compositor::{GetPid, Pid}, }, @@ -10,7 +10,7 @@ use { pub fn main(global: GlobalArgs) { with_tool_client(global.log_level, |tc| async move { let pid = Rc::new(P { tc: tc.clone() }); - run(pid).await; + run(&global, pid).await; }); } @@ -18,12 +18,17 @@ struct P { tc: Rc, } -async fn run(p: Rc

) { +async fn run(global: &GlobalArgs, p: Rc

) { let tc = &p.tc; let comp = tc.jay_compositor().await; tc.send(GetPid { self_id: comp }); - Pid::handle(tc, comp, (), |_, pid| { - println!("{}", pid.pid); + let json = global.json; + Pid::handle(tc, comp, (), move |_, pid| { + if json { + jsonl(&pid.pid); + } else { + println!("{}", pid.pid); + } }); tc.round_trip().await; } diff --git a/src/cli/randr.rs b/src/cli/randr.rs index ca076a94..d77591ae 100644 --- a/src/cli/randr.rs +++ b/src/cli/randr.rs @@ -1,14 +1,20 @@ use { crate::{ backend::{BackendColorSpace, BackendEotfs}, - cli::GlobalArgs, + cli::{ + GlobalArgs, + json::{ + JsonConnector, JsonDrmDevice, JsonMode, JsonOutput, JsonPrimaries, JsonRandrData, + JsonTearingMode, JsonVrrMode, jsonl, + }, + }, cmm::cmm_primaries::Primaries, format::{Format, XRGB8888}, ifs::wl_output::BlendSpace, scale::Scale, tools::tool_client::{Handle, ToolClient, with_tool_client}, tree::Transform, - utils::{errorfmt::ErrorFmt, ordered_float::F64}, + utils::{errorfmt::ErrorFmt, ordered_float::F64, static_text::StaticText}, wire::{JayRandrId, jay_compositor, jay_randr}, }, clap::{ @@ -496,7 +502,7 @@ pub struct RemoveVirtualOutputArgs { pub fn main(global: GlobalArgs, args: RandrArgs) { with_tool_client(global.log_level, |tc| async move { let idle = Rc::new(Randr { tc: tc.clone() }); - idle.run(args).await; + idle.run(&global, args).await; }); } @@ -596,7 +602,7 @@ struct Randr { } impl Randr { - async fn run(self: &Rc, args: RandrArgs) { + async fn run(self: &Rc, global: &GlobalArgs, args: RandrArgs) { let tc = &self.tc; let comp = tc.jay_compositor().await; let randr = tc.id(); @@ -605,7 +611,7 @@ impl Randr { id: randr, }); match args.command.unwrap_or_default() { - RandrCmd::Show(args) => self.show(randr, args).await, + RandrCmd::Show(args) => self.show(global, randr, args).await, RandrCmd::Card(args) => self.card(randr, args).await, RandrCmd::Output(args) => self.output(randr, args).await, RandrCmd::VirtualOutput(args) => self.virtual_output(randr, args).await, @@ -957,9 +963,51 @@ impl Randr { tc.round_trip().await; } - async fn show(self: &Rc, randr: JayRandrId, args: ShowArgs) { + async fn show(self: &Rc, global: &GlobalArgs, randr: JayRandrId, args: ShowArgs) { let mut data = self.get(randr).await; data.drm_devices.sort_by(|l, r| l.devnode.cmp(&r.devnode)); + if global.json { + self.show_json(&data); + } else { + self.show_text(&data, &args); + } + } + + fn show_json(&self, data: &Data) { + let mut drm_devices = Vec::new(); + for dev in &data.drm_devices { + let mut connectors: Vec<_> = data + .connectors + .iter() + .filter(|c| c.drm_device == Some(dev.id)) + .collect(); + connectors.sort_by_key(|c| &c.name); + drm_devices.push(JsonDrmDevice { + devnode: &dev.devnode, + syspath: &dev.syspath, + vendor: dev.vendor, + vendor_name: &dev.vendor_name, + model: dev.model, + model_name: &dev.model_name, + gfx_api: &dev.gfx_api, + render_device: dev.render_device, + connectors: connectors.into_iter().map(make_json_connector).collect(), + }); + } + let mut unbound: Vec<_> = data + .connectors + .iter() + .filter(|c| c.drm_device.is_none()) + .collect(); + unbound.sort_by_key(|c| &c.name); + let json = JsonRandrData { + drm_devices, + unbound_connectors: unbound.into_iter().map(make_json_connector).collect(), + }; + jsonl(&json); + } + + fn show_text(&self, data: &Data, args: &ShowArgs) { if data.drm_devices.is_not_empty() { println!("drm devices:"); } @@ -1077,17 +1125,7 @@ impl Randr { println!(" scale: {}", o.scale); } if o.transform != Transform::None { - let name = match o.transform { - Transform::None => "none", - Transform::Rotate90 => "rotate-90", - Transform::Rotate180 => "rotate-180", - Transform::Rotate270 => "rotate-270", - Transform::Flip => "flip", - Transform::FlipRotate90 => "flip-rotate-90", - Transform::FlipRotate180 => "flip-rotate-180", - Transform::FlipRotate270 => "flip-rotate-270", - }; - println!(" transform: {}", name); + println!(" transform: {}", o.transform.text()); } if let Some(flip_margin_ns) = o.flip_margin_ns { println!( @@ -1361,3 +1399,77 @@ impl Randr { data.borrow_mut().clone() } } + +fn make_json_connector(c: &Connector) -> JsonConnector<'_> { + let output = c.output.as_ref().map(|o| { + let modes = o + .modes + .iter() + .map(|m| JsonMode { + width: m.width, + height: m.height, + refresh_rate_millihz: m.refresh_rate_millihz, + current: m.current, + }) + .collect(); + let formats = o.formats.iter().map(|f| f.as_str()).collect(); + JsonOutput { + product: &o.product, + manufacturer: &o.manufacturer, + serial_number: &o.serial_number, + width_mm: o.width_mm, + height_mm: o.height_mm, + non_desktop: o.non_desktop, + scale: o.scale, + x: o.x, + y: o.y, + width: o.width, + height: o.height, + transform: o.transform.text(), + mode: o.current_mode.map(|m| JsonMode { + width: m.width, + height: m.height, + refresh_rate_millihz: m.refresh_rate_millihz, + current: m.current, + }), + format: o.format.as_deref(), + vrr_capable: o.vrr_capable, + vrr_enabled: o.vrr_enabled, + vrr_mode: JsonVrrMode(o.vrr_mode), + vrr_cursor_hz: o.vrr_cursor_hz, + tearing_mode: JsonTearingMode(o.tearing_mode), + flip_margin_ns: o.flip_margin_ns, + supported_color_spaces: o + .supported_color_spaces + .iter() + .map(|s| s.as_str()) + .collect(), + current_color_space: o.current_color_space.as_deref(), + supported_eotfs: o.supported_eotfs.iter().map(|s| s.as_str()).collect(), + current_eotf: o.current_eotf.as_deref(), + min_brightness: o.brightness_range.map(|(min, _)| min), + max_brightness: o.brightness_range.map(|(_, max)| max), + brightness: o.brightness, + blend_space: o.blend_space.as_deref(), + native_gamut: o.native_gamut.as_ref().map(|p| JsonPrimaries { + r_x: p.r.0.0, + r_y: p.r.1.0, + g_x: p.g.0.0, + g_y: p.g.1.0, + b_x: p.b.0.0, + b_y: p.b.1.0, + w_x: p.wp.0.0, + w_y: p.wp.1.0, + }), + use_native_gamut: o.use_native_gamut, + arbitrary_modes: o.arbitrary_modes, + modes, + formats, + } + }); + JsonConnector { + name: &c.name, + enabled: c.enabled, + output, + } +} diff --git a/src/cli/seat_test.rs b/src/cli/seat_test.rs index 69a9ba98..c5b382b8 100644 --- a/src/cli/seat_test.rs +++ b/src/cli/seat_test.rs @@ -1,6 +1,9 @@ use { crate::{ - cli::{GlobalArgs, SeatTestArgs}, + cli::{ + GlobalArgs, SeatTestArgs, + json::{JsonAxisData, JsonSeatEvent, jsonl}, + }, fixed::Fixed, ifs::wl_seat::wl_pointer::{ CONTINUOUS, FINGER, HORIZONTAL_SCROLL, PendingScroll, VERTICAL_SCROLL, WHEEL, @@ -33,7 +36,7 @@ pub fn main(global: GlobalArgs, args: SeatTestArgs) { args, names: Default::default(), }); - run(screenshot).await; + run(&global, screenshot).await; }); } @@ -87,7 +90,7 @@ pub struct PendingTabletPadDial { value120: Option, } -async fn run(seat_test: Rc) { +async fn run(global: &GlobalArgs, seat_test: Rc) { let tc = &seat_test.tc; let comp = tc.jay_compositor().await; tc.send(GetSeats { self_id: comp }); @@ -98,6 +101,7 @@ async fn run(seat_test: Rc) { }); tc.round_trip().await; let all = seat_test.args.all; + let json = global.json; let mut seat = 0; if !all { seat = choose_seat(&seat_test); @@ -110,70 +114,118 @@ async fn run(seat_test: Rc) { let st = seat_test.clone(); Key::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::Key { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + key: ev.key, + state: ev.state, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Key: {}, State: {}", + time(ev.time_usec), + ev.key, + ev.state + ); } - println!( - "Time: {:.4}, Key: {}, State: {}", - time(ev.time_usec), - ev.key, - ev.state - ); } }); let st = seat_test.clone(); Modifiers::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::Modifiers { + seat: &st.name(ev.seat), + modifiers: ev.modifiers, + group: ev.group, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!("Modifiers: {:08b}, Group: {}", ev.modifiers, ev.group); } - println!("Modifiers: {:08b}, Group: {}", ev.modifiers, ev.group); } }); let st = seat_test.clone(); PointerAbs::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::PointerAbs { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + x: ev.x.to_f64(), + y: ev.y.to_f64(), + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Pointer: {}x{}", + time(ev.time_usec), + ev.x, + ev.y + ); } - println!( - "Time: {:.4}, Pointer: {}x{}", - time(ev.time_usec), - ev.x, - ev.y - ); } }); let st = seat_test.clone(); PointerRel::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::PointerRel { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + x: ev.x.to_f64(), + y: ev.y.to_f64(), + dx: ev.dx.to_f64(), + dy: ev.dy.to_f64(), + dx_unaccelerated: ev.dx_unaccelerated.to_f64(), + dy_unaccelerated: ev.dy_unaccelerated.to_f64(), + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Pointer: {:+.4}x{:+.4}, Rel: {:+.4}x{:+.4}, Unaccelerated: {:+.4}x{:+.4}", + time(ev.time_usec), + ev.x, + ev.y, + ev.dx, + ev.dy, + ev.dx_unaccelerated, + ev.dy_unaccelerated + ); } - println!( - "Time: {:.4}, Pointer: {:+.4}x{:+.4}, Rel: {:+.4}x{:+.4}, Unaccelerated: {:+.4}x{:+.4}", - time(ev.time_usec), - ev.x, - ev.y, - ev.dx, - ev.dy, - ev.dx_unaccelerated, - ev.dy_unaccelerated - ); } }); let st = seat_test.clone(); Button::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {:.4}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::Button { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + button: ev.button, + state: ev.state, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {}, Button: {}, State: {}", + time(ev.time_usec), + ev.button, + ev.state + ); } - println!( - "Time: {}, Button: {}, State: {}", - time(ev.time_usec), - ev.button, - ev.state - ); } }); let ps = Rc::new(PendingScroll::default()); @@ -204,189 +256,293 @@ async fn run(seat_test: Rc) { let inverted_x = ps.inverted[HORIZONTAL_SCROLL].get(); let inverted_y = ps.inverted[VERTICAL_SCROLL].get(); if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); - } - let mut need_comma = false; - macro_rules! comma { - () => { - if std::mem::take(&mut need_comma) { - print!(", "); + let source = source.map(|source| match source { + WHEEL => "wheel", + FINGER => "finger", + CONTINUOUS => "continuous", + _ => "unknown", + }); + if json { + let make_axis = + |px: Option, v120: Option, stop: bool, inverted: bool| { + if px.is_some() || v120.is_some() || stop || inverted { + Some(JsonAxisData { + px: px.map(|p| p.to_f64()), + v120, + stop, + natural_scrolling: inverted, + }) + } else { + None + } + }; + jsonl(&JsonSeatEvent::Axis { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + source, + horizontal: make_axis(px_x, v120_x, stop_x, inverted_x), + vertical: make_axis(px_y, v120_y, stop_y, inverted_y), + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + let mut need_comma = false; + macro_rules! comma { + () => { + if std::mem::take(&mut need_comma) { + print!(", "); + } + }; + } + print!("Time: {:.4}, ", time(ev.time_usec)); + if let Some(source) = source { + print!("Source: {}", source); + need_comma = true; + } + for (axis, px, steps, stop, inverted) in [ + ("horizontal", px_x, v120_x, stop_x, inverted_x), + ("vertical", px_y, v120_y, stop_y, inverted_y), + ] { + if px.is_some() || steps.is_some() || stop { + comma!(); + print!("Axis {}: ", axis); } - }; + if let Some(dist) = px { + print!("{:+.4}px", dist); + need_comma = true; + } + if let Some(dist) = steps { + comma!(); + print!("steps: {:+}/120", dist); + need_comma = true; + } + if stop { + comma!(); + print!("stop"); + need_comma = true; + } + if inverted { + comma!(); + print!("natural scrolling"); + need_comma = true; + } + } + println!(); } - print!("Time: {:.4}, ", time(ev.time_usec)); - if let Some(source) = source { - let source = match source { - WHEEL => "wheel", - FINGER => "finger", - CONTINUOUS => "continuous", - _ => "unknown", - }; - print!("Source: {}", source); - need_comma = true; - } - for (axis, px, steps, stop, inverted) in [ - ("horizontal", px_x, v120_x, stop_x, inverted_x), - ("vertical", px_y, v120_y, stop_y, inverted_y), - ] { - if px.is_some() || steps.is_some() || stop { - comma!(); - print!("Axis {}: ", axis); - } - if let Some(dist) = px { - print!("{:+.4}px", dist); - need_comma = true; - } - if let Some(dist) = steps { - comma!(); - print!("steps: {:+}/120", dist); - need_comma = true; - } - if stop { - comma!(); - print!("stop"); - need_comma = true; - } - if inverted { - comma!(); - print!("natural scrolling"); - need_comma = true; - } - } - println!(); } }); let st = seat_test.clone(); SwipeBegin::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::SwipeBegin { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + fingers: ev.fingers, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Swipe Begin: {} fingers", + time(ev.time_usec), + ev.fingers, + ); } - println!( - "Time: {:.4}, Swipe Begin: {} fingers", - time(ev.time_usec), - ev.fingers, - ); } }); let st = seat_test.clone(); SwipeUpdate::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::SwipeUpdate { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + dx: ev.dx.to_f64(), + dy: ev.dy.to_f64(), + dx_unaccelerated: ev.dx_unaccelerated.to_f64(), + dy_unaccelerated: ev.dy_unaccelerated.to_f64(), + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Swipe Update: {}x{}, Unaccelerated: {}x{}", + time(ev.time_usec), + ev.dx, + ev.dy, + ev.dx_unaccelerated, + ev.dy_unaccelerated, + ); } - println!( - "Time: {:.4}, Swipe Update: {}x{}, Unaccelerated: {}x{}", - time(ev.time_usec), - ev.dx, - ev.dy, - ev.dx_unaccelerated, - ev.dy_unaccelerated, - ); } }); let st = seat_test.clone(); SwipeEnd::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::SwipeEnd { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + cancelled: ev.cancelled != 0, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + print!("Time: {:.4}, Swipe End", time(ev.time_usec),); + if ev.cancelled != 0 { + print!(", cancelled"); + } + println!(); } - print!("Time: {:.4}, Swipe End", time(ev.time_usec),); - if ev.cancelled != 0 { - print!(", cancelled"); - } - println!(); } }); let st = seat_test.clone(); PinchBegin::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::PinchBegin { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + fingers: ev.fingers, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Pinch Begin: {} fingers", + time(ev.time_usec), + ev.fingers, + ); } - println!( - "Time: {:.4}, Pinch Begin: {} fingers", - time(ev.time_usec), - ev.fingers, - ); } }); let st = seat_test.clone(); PinchUpdate::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::PinchUpdate { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + dx: ev.dx.to_f64(), + dy: ev.dy.to_f64(), + dx_unaccelerated: ev.dx_unaccelerated.to_f64(), + dy_unaccelerated: ev.dy_unaccelerated.to_f64(), + scale: ev.scale.to_f64(), + rotation: ev.rotation.to_f64(), + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Pinch Update: {}x{}, Unaccelerated: {}x{}, Scale: {}, Rotation: {}", + time(ev.time_usec), + ev.dx, + ev.dy, + ev.dx_unaccelerated, + ev.dy_unaccelerated, + ev.scale, + ev.rotation, + ); } - println!( - "Time: {:.4}, Pinch Update: {}x{}, Unaccelerated: {}x{}, Scale: {}, Rotation: {}", - time(ev.time_usec), - ev.dx, - ev.dy, - ev.dx_unaccelerated, - ev.dy_unaccelerated, - ev.scale, - ev.rotation, - ); } }); let st = seat_test.clone(); PinchEnd::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::PinchEnd { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + cancelled: ev.cancelled != 0, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + print!("Time: {:.4}, Pinch End", time(ev.time_usec)); + if ev.cancelled != 0 { + print!(", cancelled"); + } + println!(); } - print!("Time: {:.4}, Pinch End", time(ev.time_usec)); - if ev.cancelled != 0 { - print!(", cancelled"); - } - println!(); } }); let st = seat_test.clone(); HoldBegin::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::HoldBegin { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + fingers: ev.fingers, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Hold Begin: {} fingers", + time(ev.time_usec), + ev.fingers, + ); } - println!( - "Time: {:.4}, Hold Begin: {} fingers", - time(ev.time_usec), - ev.fingers, - ); } }); let st = seat_test.clone(); HoldEnd::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::HoldEnd { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + cancelled: ev.cancelled != 0, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + print!("Time: {:.4}, Hold End", time(ev.time_usec)); + if ev.cancelled != 0 { + print!(", cancelled"); + } + println!(); } - print!("Time: {:.4}, Hold End", time(ev.time_usec)); - if ev.cancelled != 0 { - print!(", cancelled"); - } - println!(); } }); let st = seat_test.clone(); SwitchEvent::handle(tc, se, (), move |_, ev| { - let event = match ev.event { - 0 => "lid opened", - 1 => "lid closed", - 2 => "converted to laptop", - 3 => "converted to tablet", - _ => "unknown event", - }; if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + let event = match ev.event { + 0 => "lid opened", + 1 => "lid closed", + 2 => "converted to laptop", + 3 => "converted to tablet", + _ => "unknown event", + }; + if json { + jsonl(&JsonSeatEvent::Switch { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + input_device: ev.input_device, + event, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Device: {}, {event}", + time(ev.time_usec), + ev.input_device + ); } - println!( - "Time: {:.4}, Device: {}, {event}", - time(ev.time_usec), - ev.input_device - ); } }); let tt = Rc::new(RefCell::new(PendingTabletTool::default())); @@ -432,87 +588,131 @@ async fn run(seat_test: Rc) { if !all && ev.seat != seat { return; } - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::TabletTool { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + input_device: ev.input_device, + tool: ev.tool, + proximity_in: tt.proximity_in, + proximity_out: tt.proximity_out, + down: tt.down, + up: tt.up, + x: tt.pos.map(|(x, _)| x.to_f64()), + y: tt.pos.map(|(_, y)| y.to_f64()), + pressure: tt.pressure, + distance: tt.distance, + tilt_x: tt.tilt.map(|(x, _)| x), + tilt_y: tt.tilt.map(|(_, y)| y), + rotation: tt.rotation, + slider: tt.slider, + wheel_degrees: tt.wheel.map(|(d, _)| d), + wheel_clicks: tt.wheel.map(|(_, c)| c), + button: tt.button.map(|(b, _)| b), + button_state: tt.button.map(|(_, s)| if s == 0 { "up" } else { "down" }), + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + print!( + "Time: {:.4}, Device: {}, Tool: {}", + time(ev.time_usec), + ev.input_device, + ev.tool, + ); + if tt.proximity_in { + print!(", proximity in"); + } + if tt.proximity_out { + print!(", proximity out"); + } + if tt.down { + print!(", down"); + } + if tt.up { + print!(", up"); + } + if let Some((x, y)) = tt.pos { + print!(", pos: {x}x{y}"); + } + if let Some(val) = tt.pressure { + print!(", pressure: {val}"); + } + if let Some(val) = tt.distance { + print!(", distance: {val}"); + } + if let Some((x, y)) = tt.tilt { + print!(", tilt: {x}x{y}"); + } + if let Some(val) = tt.rotation { + print!(", rotation: {val}"); + } + if let Some(val) = tt.slider { + print!(", slider: {val}"); + } + if let Some((degrees, clicks)) = tt.wheel { + print!(", wheel degrees: {degrees}, wheel clicks: {clicks}"); + } + if let Some((button, state)) = tt.button { + let dir = match state { + 0 => "up", + _ => "down", + }; + print!(", button {button} {dir}"); + } + println!(); } - print!( - "Time: {:.4}, Device: {}, Tool: {}", - time(ev.time_usec), - ev.input_device, - ev.tool, - ); - if tt.proximity_in { - print!(", proximity in"); - } - if tt.proximity_out { - print!(", proximity out"); - } - if tt.down { - print!(", down"); - } - if tt.up { - print!(", up"); - } - if let Some((x, y)) = tt.pos { - print!(", pos: {x}x{y}"); - } - if let Some(val) = tt.pressure { - print!(", pressure: {val}"); - } - if let Some(val) = tt.distance { - print!(", distance: {val}"); - } - if let Some((x, y)) = tt.tilt { - print!(", tilt: {x}x{y}"); - } - if let Some(val) = tt.rotation { - print!(", rotation: {val}"); - } - if let Some(val) = tt.slider { - print!(", slider: {val}"); - } - if let Some((degrees, clicks)) = tt.wheel { - print!(", wheel degrees: {degrees}, wheel clicks: {clicks}"); - } - if let Some((button, state)) = tt.button { - let dir = match state { - 0 => "up", - _ => "down", - }; - print!(", button {button} {dir}"); - } - println!(); }); let st = seat_test.clone(); TabletPadModeSwitch::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::TabletPadModeSwitch { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + input_device: ev.input_device, + mode: ev.mode, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Device: {}, mode switch: {}", + time(ev.time_usec), + ev.input_device, + ev.mode, + ); } - println!( - "Time: {:.4}, Device: {}, mode switch: {}", - time(ev.time_usec), - ev.input_device, - ev.mode, - ); } }); let st = seat_test.clone(); TabletPadButton::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); - } let dir = match ev.state { 0 => "up", _ => "down", }; - println!( - "Time: {:.4}, Device: {}, Button {} {dir}", - time(ev.time_usec), - ev.input_device, - ev.button, - ); + if json { + jsonl(&JsonSeatEvent::TabletPadButton { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + input_device: ev.input_device, + button: ev.button, + state: dir, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Device: {}, Button {} {dir}", + time(ev.time_usec), + ev.input_device, + ev.button, + ); + } } }); let tt = Rc::new(RefCell::new(PendingTabletPadStrip::default())); @@ -531,27 +731,39 @@ async fn run(seat_test: Rc) { if !all && ev.seat != seat { return; } - if all { - print!("Seat: {}, ", st.name(ev.seat)); - } - print!( - "Time: {:.4}, Device: {}, Strip: {}", - time(ev.time_usec), - ev.input_device, - ev.strip, - ); let source = match tt.source { 1 => "finger", _ => "unknown", }; - print!(", source: {source}"); - if let Some(pos) = tt.pos { - print!(", pos: {pos}"); + if json { + jsonl(&JsonSeatEvent::TabletPadStrip { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + input_device: ev.input_device, + strip: ev.strip, + source, + position: tt.pos, + stop: tt.stop, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + print!( + "Time: {:.4}, Device: {}, Strip: {}", + time(ev.time_usec), + ev.input_device, + ev.strip, + ); + print!(", source: {source}"); + if let Some(pos) = tt.pos { + print!(", pos: {pos}"); + } + if tt.stop { + print!(", stop"); + } + println!(); } - if tt.stop { - print!(", stop"); - } - println!(); }); let tt = Rc::new(RefCell::new(PendingTabletPadRing::default())); TabletPadRingSource::handle(tc, se, tt.clone(), move |tt, ev| { @@ -569,27 +781,39 @@ async fn run(seat_test: Rc) { if !all && ev.seat != seat { return; } - if all { - print!("Seat: {}, ", st.name(ev.seat)); - } - print!( - "Time: {:.4}, Device: {}, Ring: {}", - time(ev.time_usec), - ev.input_device, - ev.ring, - ); let source = match tt.source { 1 => "finger", _ => "unknown", }; - print!(", source: {source}"); - if let Some(val) = tt.degrees { - print!(", degrees: {val}"); + if json { + jsonl(&JsonSeatEvent::TabletPadRing { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + input_device: ev.input_device, + ring: ev.ring, + source, + degrees: tt.degrees, + stop: tt.stop, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + print!( + "Time: {:.4}, Device: {}, Ring: {}", + time(ev.time_usec), + ev.input_device, + ev.ring, + ); + print!(", source: {source}"); + if let Some(val) = tt.degrees { + print!(", degrees: {val}"); + } + if tt.stop { + print!(", stop"); + } + println!(); } - if tt.stop { - print!(", stop"); - } - println!(); }); let tt = Rc::new(RefCell::new(PendingTabletPadDial::default())); TabletPadDialDelta::handle(tc, se, tt.clone(), move |tt, ev| { @@ -601,66 +825,112 @@ async fn run(seat_test: Rc) { if !all && ev.seat != seat { return; } - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::TabletPadDial { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + input_device: ev.input_device, + dial: ev.dial, + delta120: tt.value120, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + print!( + "Time: {:.4}, Device: {}, Dial: {}", + time(ev.time_usec), + ev.input_device, + ev.dial, + ); + if let Some(val) = tt.value120 { + print!(", delta: {val}/120"); + } + println!(); } - print!( - "Time: {:.4}, Device: {}, Dial: {}", - time(ev.time_usec), - ev.input_device, - ev.dial, - ); - if let Some(val) = tt.value120 { - print!(", delta: {val}/120"); - } - println!(); }); let st = seat_test.clone(); TouchDown::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::TouchDown { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + id: ev.id, + x: ev.x.to_f64(), + y: ev.y.to_f64(), + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Touch: {}, Down: {}x{}", + time(ev.time_usec), + ev.id, + ev.x, + ev.y + ); } - println!( - "Time: {:.4}, Touch: {}, Down: {}x{}", - time(ev.time_usec), - ev.id, - ev.x, - ev.y - ); } }); let st = seat_test.clone(); TouchUp::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::TouchUp { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + id: ev.id, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!("Time: {:.4}, Touch: {}, Up", time(ev.time_usec), ev.id); } - println!("Time: {:.4}, Touch: {}, Up", time(ev.time_usec), ev.id); } }); let st = seat_test.clone(); TouchMotion::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::TouchMotion { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + id: ev.id, + x: ev.x.to_f64(), + y: ev.y.to_f64(), + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Touch: {} Motion: {}x{}", + time(ev.time_usec), + ev.id, + ev.x, + ev.y + ); } - println!( - "Time: {:.4}, Touch: {} Motion: {}x{}", - time(ev.time_usec), - ev.id, - ev.x, - ev.y - ); } }); let st = seat_test.clone(); TouchCancel::handle(tc, se, (), move |_, ev| { if all || ev.seat == seat { - if all { - print!("Seat: {}, ", st.name(ev.seat)); + if json { + jsonl(&JsonSeatEvent::TouchCancel { + seat: &st.name(ev.seat), + time_usec: ev.time_usec, + id: ev.id, + }); + } else { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!("Time: {:.4}, Touch: {}, Cancel", time(ev.time_usec), ev.id); } - println!("Time: {:.4}, Touch: {}, Cancel", time(ev.time_usec), ev.id); } }); pending::<()>().await; diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 670267e3..333ba0da 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -2,7 +2,8 @@ use { crate::{ cli::{ GlobalArgs, - clients::{Client, ClientPrinter, handle_client_query}, + clients::{Client, ClientPrinter, handle_client_query, make_json_client}, + json::{JsonRect, JsonTreeNode, JsonTreeNodeType, jsonl}, }, ifs::jay_tree_query::{ TREE_TY_CONTAINER, TREE_TY_DISPLAY, TREE_TY_FLOAT, TREE_TY_LAYER_SURFACE, @@ -68,7 +69,7 @@ pub fn main(global: GlobalArgs, tree_args: TreeArgs) { tc: tc.clone(), comp, }); - tree.run(tree_args).await; + tree.run(&global, tree_args).await; }); } @@ -78,13 +79,13 @@ struct Tree { } impl Tree { - async fn run(&self, args: TreeArgs) { + async fn run(&self, global: &GlobalArgs, args: TreeArgs) { match &args.cmd { - TreeCmd::Query(a) => self.query(a).await, + TreeCmd::Query(a) => self.query(global, a).await, } } - async fn query(&self, args: &QueryArgs) { + async fn query(&self, global: &GlobalArgs, args: &QueryArgs) { let id = self.tc.id(); self.tc.send(jay_compositor::CreateTreeQuery { self_id: self.comp, @@ -95,7 +96,7 @@ impl Tree { tc: &self.tc, id, }; - query.run(args).await; + query.run(global, args).await; } } @@ -137,7 +138,7 @@ struct Node { } impl Query<'_> { - async fn run(&mut self, args: &QueryArgs) { + async fn run(&mut self, global: &GlobalArgs, args: &QueryArgs) { match &args.cmd { QueryCmd::Root => { self.tc.send(SetRootDisplay { self_id: self.id }); @@ -296,20 +297,68 @@ impl Query<'_> { tl.send(Execute { self_id: id }); handle_client_query(tl, id).await }; - let mut printer = Printer { - clients, - printed_clients: Default::default(), - verbose: args.all_clients, - prefix: "".to_string(), - output_depth: 0, - workspace_depth: 0, - }; - for node in &d.borrow().roots { - printer.print(node); + if global.json { + for node in &d.borrow().roots { + let node = make_json_tree_node(&clients, node); + jsonl(&node); + } + } else { + let mut printer = Printer { + clients, + printed_clients: Default::default(), + verbose: args.all_clients, + prefix: "".to_string(), + output_depth: 0, + workspace_depth: 0, + }; + for node in &d.borrow().roots { + printer.print(node); + } } } } +fn make_json_tree_node<'b>(clients: &'b AHashMap, node: &'b Node) -> JsonTreeNode<'b> { + let position = node.position.map(|r| JsonRect { + x1: r.x1(), + y1: r.y1(), + x2: r.x2(), + y2: r.y2(), + width: r.width(), + height: r.height(), + }); + let client = node + .client + .and_then(|client_id| clients.get(&client_id)) + .map(make_json_client); + let children = node + .children + .iter() + .map(|c| make_json_tree_node(clients, c)) + .collect(); + JsonTreeNode { + ty: JsonTreeNodeType(node.ty), + output: node.output.as_deref(), + workspace: node.workspace.as_deref(), + toplevel_id: node.toplevel_id.as_deref(), + placeholder_for: node.placeholder_for.as_deref(), + position, + client, + title: node.title.as_deref(), + app_id: node.app_id.as_deref(), + tag: node.tag.as_deref(), + content_type: node.content_type.as_deref(), + x_class: node.x_class.as_deref(), + x_instance: node.x_instance.as_deref(), + x_role: node.x_role.as_deref(), + floating: node.floating, + visible: node.visible, + urgent: node.urgent, + fullscreen: node.fullscreen, + children, + } +} + struct Printer { clients: AHashMap, printed_clients: AHashSet, diff --git a/src/cli/version.rs b/src/cli/version.rs index 068caaf2..ad7d78d9 100644 --- a/src/cli/version.rs +++ b/src/cli/version.rs @@ -1,5 +1,12 @@ -use crate::{cli::GlobalArgs, version::VERSION}; +use crate::{ + cli::{GlobalArgs, json::jsonl}, + version::VERSION, +}; -pub fn main(_global: GlobalArgs) { - println!("{VERSION}"); +pub fn main(global: GlobalArgs) { + if global.json { + jsonl(&VERSION); + } else { + println!("{VERSION}"); + } } diff --git a/src/cli/xwayland.rs b/src/cli/xwayland.rs index 6b188072..9416cc09 100644 --- a/src/cli/xwayland.rs +++ b/src/cli/xwayland.rs @@ -1,6 +1,9 @@ use { crate::{ - cli::GlobalArgs, + cli::{ + GlobalArgs, + json::{JsonXwaylandStatus, jsonl}, + }, tools::tool_client::{Handle, ToolClient, with_tool_client}, wire::{JayXwaylandId, jay_compositor, jay_xwayland}, }, @@ -41,7 +44,7 @@ pub enum CliScalingMode { pub fn main(global: GlobalArgs, args: XwaylandArgs) { with_tool_client(global.log_level, |tc| async move { let xwayland = Xwayland { tc: tc.clone() }; - xwayland.run(args).await; + xwayland.run(&global, args).await; }); } @@ -50,7 +53,7 @@ struct Xwayland { } impl Xwayland { - async fn run(self, args: XwaylandArgs) { + async fn run(self, global: &GlobalArgs, args: XwaylandArgs) { let tc = &self.tc; let comp = tc.jay_compositor().await; let xwayland = tc.id(); @@ -59,12 +62,12 @@ impl Xwayland { id: xwayland, }); match args.command.unwrap_or_default() { - XwaylandCmd::Status => self.status(xwayland).await, + XwaylandCmd::Status => self.status(global, xwayland).await, XwaylandCmd::SetScalingMode(args) => self.set_scaling_mode(xwayland, args).await, } } - async fn status(self, xwayland: JayXwaylandId) { + async fn status(self, global: &GlobalArgs, xwayland: JayXwaylandId) { let tc = &self.tc; tc.send(jay_xwayland::GetScaling { self_id: xwayland }); let mode = Rc::new(Cell::new(0)); @@ -85,9 +88,16 @@ impl Xwayland { &mode_str } }; - println!("scaling mode: {}", mode); - if let Some(scale) = scale.get() { - println!("implied scale: {}", scale); + if global.json { + jsonl(&JsonXwaylandStatus { + scaling_mode: mode, + implied_scale: scale.get().map(|s| s as f64), + }); + } else { + println!("scaling mode: {}", mode); + if let Some(scale) = scale.get() { + println!("implied scale: {}", scale); + } } } diff --git a/src/control_center/cc_outputs.rs b/src/control_center/cc_outputs.rs index 9c48cc83..5b11b10a 100644 --- a/src/control_center/cc_outputs.rs +++ b/src/control_center/cc_outputs.rs @@ -21,7 +21,7 @@ use { scale::{SCALE_BASE, SCALE_BASEF, Scale}, state::State, tree::{TearingMode, Transform, VrrMode}, - utils::errorfmt::ErrorFmt, + utils::{errorfmt::ErrorFmt, static_text::StaticText}, }, ahash::AHashMap, egui::{ @@ -1166,18 +1166,8 @@ fn show_transform(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool grid_label(ui, "Transform"); let mut v = effective!(m, t).transform; let mut changed = false; - let transform_name = |t: Transform| match t { - Transform::None => "none", - Transform::Rotate90 => "rotate-90", - Transform::Rotate180 => "rotate-180", - Transform::Rotate270 => "rotate-270", - Transform::Flip => "flip", - Transform::FlipRotate90 => "flip-rotate-90", - Transform::FlipRotate180 => "flip-rotate-180", - Transform::FlipRotate270 => "flip-rotate-270", - }; ComboBox::from_id_salt("transform") - .selected_text(transform_name(v)) + .selected_text(v.text()) .show_ui(ui, |ui| { let transforms = [ Transform::None, @@ -1190,7 +1180,7 @@ fn show_transform(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool Transform::FlipRotate270, ]; for s in transforms { - changed |= ui.selectable_value(&mut v, s, transform_name(s)).changed(); + changed |= ui.selectable_value(&mut v, s, s.text()).changed(); } }); if changed { @@ -1200,7 +1190,7 @@ fn show_transform(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool } let diff = m.transform != v; if diff { - ui.label(transform_name(m.transform)); + ui.label(m.transform.text()); } diff } diff --git a/src/tree.rs b/src/tree.rs index 6495f1b7..b33abdc6 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -101,6 +101,21 @@ pub enum Transform { FlipRotate270, } +impl StaticText for Transform { + fn text(&self) -> &'static str { + match self { + Transform::None => "none", + Transform::Rotate90 => "rotate-90", + Transform::Rotate180 => "rotate-180", + Transform::Rotate270 => "rotate-270", + Transform::Flip => "flip", + Transform::FlipRotate90 => "flip-rotate-90", + Transform::FlipRotate180 => "flip-rotate-180", + Transform::FlipRotate270 => "flip-rotate-270", + } + } +} + impl From for Transform { fn from(value: ConfigTransform) -> Self { match value {