262 lines
11 KiB
Rust
262 lines
11 KiB
Rust
#[component]
|
|
pub fn App() -> impl IntoView {
|
|
let devices = RwSignal::new(Vec::<DeviceSummary>::new());
|
|
let selected_path = RwSignal::new(String::new());
|
|
let selected_profile = RwSignal::new("direct".to_string());
|
|
let device_state = RwSignal::new(None::<DeviceState>);
|
|
let active_tab = RwSignal::new("basic".to_string());
|
|
let status = RwSignal::new("Scanning for a supported Basilisk mouse.".to_string());
|
|
let busy = RwSignal::new(false);
|
|
|
|
let connect_path = move |path: String| {
|
|
if path.is_empty() {
|
|
status.set("Select a device path before connecting.".to_string());
|
|
return;
|
|
}
|
|
|
|
busy.set(true);
|
|
status.set(format!("Connecting to {path}..."));
|
|
spawn_local(async move {
|
|
let args = ConnectArgs { path };
|
|
match invoke::<DeviceState, _>("connect_device", &args).await {
|
|
Ok(snapshot) => {
|
|
selected_profile.set(snapshot.basic.profile.clone());
|
|
status.set(format!("Connected to {}.", snapshot.device.supported_name));
|
|
device_state.set(Some(snapshot));
|
|
}
|
|
Err(error) => status.set(error),
|
|
}
|
|
busy.set(false);
|
|
});
|
|
};
|
|
|
|
let scan = move || {
|
|
busy.set(true);
|
|
status.set("Scanning /sys/class/hidraw for Basilisk V3 devices...".to_string());
|
|
spawn_local(async move {
|
|
match invoke_no_args::<Vec<DeviceSummary>>("list_supported_devices").await {
|
|
Ok(found) => {
|
|
if let Some(first) = found.first() {
|
|
let path = first.path.clone();
|
|
selected_path.set(path.clone());
|
|
devices.set(found);
|
|
status.set(format!("Found supported device. Connecting to {path}..."));
|
|
match invoke::<DeviceState, _>("connect_device", &ConnectArgs { path }).await {
|
|
Ok(snapshot) => {
|
|
selected_profile.set(snapshot.basic.profile.clone());
|
|
status.set(format!("Connected to {}.", snapshot.device.supported_name));
|
|
device_state.set(Some(snapshot));
|
|
}
|
|
Err(error) => status.set(error),
|
|
}
|
|
} else {
|
|
selected_path.set(String::new());
|
|
devices.set(found);
|
|
status.set("No supported Razer hidraw devices found.".to_string());
|
|
}
|
|
}
|
|
Err(error) => status.set(error),
|
|
}
|
|
busy.set(false);
|
|
});
|
|
};
|
|
|
|
Effect::new(move |_| scan());
|
|
|
|
let connect = move |_| {
|
|
let path = selected_path.get_untracked();
|
|
connect_path(path);
|
|
};
|
|
|
|
let refresh_profile = move |profile: String| {
|
|
selected_profile.set(profile.clone());
|
|
busy.set(true);
|
|
status.set(format!("Loading {profile} profile..."));
|
|
spawn_local(async move {
|
|
let args = ProfileArgs { profile };
|
|
match invoke::<DeviceState, _>("refresh_device_state", &args).await {
|
|
Ok(snapshot) => {
|
|
status.set("Profile settings loaded.".to_string());
|
|
device_state.set(Some(snapshot));
|
|
}
|
|
Err(error) => status.set(error),
|
|
}
|
|
busy.set(false);
|
|
});
|
|
};
|
|
|
|
view! {
|
|
<main class="app-shell">
|
|
<aside class="sidebar">
|
|
<div class="brand">
|
|
<img src="public/snakemouse.svg" alt="" />
|
|
<div>
|
|
<h1>"Razer Basilisk V3"</h1>
|
|
<p>"Onboard memory tools"</p>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="primary-action" type="button" on:click=move |_| scan() disabled=move || busy.get()>
|
|
"Scan"
|
|
</button>
|
|
|
|
<label class="field">
|
|
<span>"Device"</span>
|
|
<select
|
|
prop:value=move || selected_path.get()
|
|
on:change=move |ev| selected_path.set(event_target_value(&ev))
|
|
disabled=move || busy.get()
|
|
>
|
|
<option value="">"No supported device selected"</option>
|
|
{move || devices.get().into_iter().map(|device| {
|
|
let label = format!(
|
|
"{} - {} ({:04x}:{:04x}){}",
|
|
device.path,
|
|
device.supported_name,
|
|
0x1532,
|
|
device.product_id,
|
|
device
|
|
.serial
|
|
.as_ref()
|
|
.map(|serial| format!(" - {serial}"))
|
|
.unwrap_or_default()
|
|
);
|
|
view! {
|
|
<option value=device.path.clone()>{label}</option>
|
|
}
|
|
}).collect_view()}
|
|
</select>
|
|
</label>
|
|
|
|
<button class="primary-action" type="button" on:click=connect disabled=move || busy.get() || selected_path.get().is_empty()>
|
|
"Connect"
|
|
</button>
|
|
<nav>
|
|
<button class:active=move || active_tab.get() == "basic" on:click=move |_| active_tab.set("basic".to_string())>"Basic"</button>
|
|
<button class:active=move || active_tab.get() == "led" on:click=move |_| active_tab.set("led".to_string())>"LED"</button>
|
|
<button class:active=move || active_tab.get() == "button" on:click=move |_| active_tab.set("button".to_string())>"Button"</button>
|
|
<button class:active=move || active_tab.get() == "profile" on:click=move |_| active_tab.set("profile".to_string())>"Profiles"</button>
|
|
<button class:active=move || active_tab.get() == "macro" on:click=move |_| active_tab.set("macro".to_string())>"Macros"</button>
|
|
<button class:active=move || active_tab.get() == "sensor" on:click=move |_| active_tab.set("sensor".to_string())>"Sensor"</button>
|
|
<button class:active=move || active_tab.get() == "info" on:click=move |_| active_tab.set("info".to_string())>"Info"</button>
|
|
</nav>
|
|
</aside>
|
|
|
|
<section class="workspace">
|
|
<header class="topbar">
|
|
<div>
|
|
<p class="eyebrow">"Linux desktop configuration"</p>
|
|
<h2>{move || device_state.get().map(|state| state.device.supported_name).unwrap_or_else(|| "No device connected".to_string())}</h2>
|
|
</div>
|
|
<div class="status" class:busy=move || busy.get()>{move || status.get()}</div>
|
|
</header>
|
|
|
|
{move || match device_state.get() {
|
|
Some(snapshot) => view! {
|
|
<ConnectedView
|
|
snapshot=snapshot
|
|
active_tab=active_tab
|
|
selected_profile=selected_profile
|
|
refresh_profile=refresh_profile
|
|
set_snapshot=device_state.write_only()
|
|
set_status=status.write_only()
|
|
set_busy=busy.write_only()
|
|
/>
|
|
}.into_any(),
|
|
None => view! {
|
|
<section class="empty-state">
|
|
<h3>"Connect to a Basilisk V3 or V3 Pro"</h3>
|
|
<p>"This app uses Linux hidraw feature reports through the Tauri backend instead of WebHID and Pyodide."</p>
|
|
<p>"If your mouse is connected but does not appear, check that the current user can read and write the matching /dev/hidraw node."</p>
|
|
</section>
|
|
}.into_any()
|
|
}}
|
|
</section>
|
|
</main>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn ConnectedView(
|
|
snapshot: DeviceState,
|
|
active_tab: RwSignal<String>,
|
|
selected_profile: RwSignal<String>,
|
|
refresh_profile: impl Fn(String) + Copy + 'static,
|
|
set_snapshot: WriteSignal<Option<DeviceState>>,
|
|
set_status: WriteSignal<String>,
|
|
set_busy: WriteSignal<bool>,
|
|
) -> impl IntoView {
|
|
view! {
|
|
<div class="profile-row">
|
|
<span>"Profile"</span>
|
|
<div class="segments">
|
|
{snapshot.profiles.iter().map(|profile| {
|
|
let profile_name = profile.clone();
|
|
let profile_for_click = profile.clone();
|
|
view! {
|
|
<button
|
|
type="button"
|
|
class:active=move || selected_profile.get() == profile_name
|
|
on:click=move |_| refresh_profile(profile_for_click.clone())
|
|
>
|
|
{profile.clone()}
|
|
</button>
|
|
}
|
|
}).collect_view()}
|
|
</div>
|
|
</div>
|
|
|
|
{move || match active_tab.get().as_str() {
|
|
"basic" => view! {
|
|
<BasicPanel
|
|
snapshot=snapshot.clone()
|
|
selected_profile=selected_profile
|
|
set_snapshot=set_snapshot
|
|
set_status=set_status
|
|
set_busy=set_busy
|
|
/>
|
|
}.into_any(),
|
|
"info" => view! { <InfoPanel snapshot=snapshot.clone() /> }.into_any(),
|
|
"profile" => view! {
|
|
<ProfilePanel
|
|
snapshot=snapshot.clone()
|
|
selected_profile=selected_profile
|
|
set_snapshot=set_snapshot
|
|
set_status=set_status
|
|
set_busy=set_busy
|
|
/>
|
|
}.into_any(),
|
|
"led" => view! {
|
|
<LedPanel
|
|
snapshot=snapshot.clone()
|
|
selected_profile=selected_profile
|
|
set_status=set_status
|
|
set_busy=set_busy
|
|
/>
|
|
}.into_any(),
|
|
"button" => view! {
|
|
<ButtonPanel
|
|
snapshot=snapshot.clone()
|
|
selected_profile=selected_profile
|
|
set_status=set_status
|
|
set_busy=set_busy
|
|
/>
|
|
}.into_any(),
|
|
"macro" => view! {
|
|
<MacroPanel
|
|
snapshot=snapshot.clone()
|
|
set_status=set_status
|
|
set_busy=set_busy
|
|
/>
|
|
}.into_any(),
|
|
"sensor" => view! {
|
|
<SensorPanel
|
|
snapshot=snapshot.clone()
|
|
set_status=set_status
|
|
set_busy=set_busy
|
|
/>
|
|
}.into_any(),
|
|
_ => view! { <BacklogPanel title="Unknown Section" body="Select a section from the sidebar." /> }.into_any(),
|
|
}}
|
|
}
|
|
}
|