use crate::theme::{aether_dark, aether_light};
use crate::views;
use iced::{
widget::{column, container, horizontal_rule, row, scrollable, vertical_rule},
Application, Command, Element, Length, Subscription, Theme,
};
use aethermap_common::{
AnalogMode, CameraOutputMode, DeviceCapabilities, DeviceInfo, DeviceType, LayerConfigInfo,
LayerMode, LedPattern, LedZone, MacroEntry, MacroSettings, RemapEntry, RemapProfileInfo,
};
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::PathBuf;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tab {
Devices,
Macros,
Profiles,
}
#[derive(Debug, Clone)]
pub struct Notification {
pub message: String,
pub is_error: bool,
pub timestamp: Instant,
}
pub use views::keypad::{azeron_keypad_layout, KeypadButton};
pub use views::auto_switch::{AutoSwitchRule, AutoSwitchRulesView};
pub use views::hotkeys::{HotkeyBinding, HotkeyBindingsView};
pub use views::analog::{
AnalogCalibrationView, CalibrationConfig, DeadzoneShape, SensitivityCurve,
};
pub use views::led::LedState;
pub struct State {
pub devices: Vec<DeviceInfo>,
pub macros: Vec<MacroEntry>,
pub selected_device: Option<usize>,
pub status: String,
pub status_history: VecDeque<String>,
pub loading: bool,
pub recording: bool,
pub recording_macro_name: Option<String>,
pub daemon_connected: bool,
pub new_macro_name: String,
pub socket_path: PathBuf,
pub recently_updated_macros: HashMap<String, Instant>,
pub grabbed_devices: HashSet<String>,
pub profile_name: String,
pub active_tab: Tab,
pub notifications: VecDeque<Notification>,
pub recording_pulse: bool,
pub device_profiles: HashMap<String, Vec<String>>,
pub active_profiles: HashMap<String, String>,
pub remap_profiles: HashMap<String, Vec<RemapProfileInfo>>,
pub active_remap_profiles: HashMap<String, String>,
pub active_remaps: HashMap<String, (String, Vec<RemapEntry>)>,
pub keypad_layout: Vec<KeypadButton>,
pub keypad_view_device: Option<String>,
pub selected_button: Option<usize>,
pub device_capabilities: Option<DeviceCapabilities>,
pub active_layers: HashMap<String, usize>,
pub layer_configs: HashMap<String, Vec<LayerConfigInfo>>,
pub layer_config_dialog: Option<(String, usize, String, LayerMode)>,
pub analog_dpad_modes: HashMap<String, String>,
pub analog_deadzones_xy: HashMap<String, (u8, u8)>,
pub analog_outer_deadzones_xy: HashMap<String, (u8, u8)>,
pub led_states: HashMap<String, LedState>,
pub led_config_device: Option<String>,
pub selected_led_zone: Option<LedZone>,
pub pending_led_color: Option<(u8, u8, u8)>,
pub current_focus: Option<String>,
pub focus_tracking_active: bool,
pub auto_switch_view: Option<AutoSwitchRulesView>,
pub hotkey_view: Option<HotkeyBindingsView>,
pub analog_calibration_view: Option<AnalogCalibrationView>,
pub macro_settings: MacroSettings,
pub current_theme: Theme,
}
impl Default for State {
fn default() -> Self {
let socket_path = if cfg!(target_os = "linux") {
PathBuf::from("/run/aethermap/aethermap.sock")
} else if cfg!(target_os = "macos") {
PathBuf::from("/tmp/aethermap.sock")
} else {
std::env::temp_dir().join("aethermap.sock")
};
State {
devices: Vec::new(),
macros: Vec::new(),
selected_device: None,
status: "Initializing...".to_string(),
status_history: VecDeque::with_capacity(10),
loading: false,
recording: false,
recording_macro_name: None,
daemon_connected: false,
new_macro_name: String::new(),
socket_path,
recently_updated_macros: HashMap::new(),
grabbed_devices: HashSet::new(),
profile_name: "default".to_string(),
active_tab: Tab::Devices,
notifications: VecDeque::with_capacity(5),
recording_pulse: false,
device_profiles: HashMap::new(),
active_profiles: HashMap::new(),
remap_profiles: HashMap::new(),
active_remap_profiles: HashMap::new(),
active_remaps: HashMap::new(),
keypad_layout: Vec::new(),
keypad_view_device: None,
selected_button: None,
device_capabilities: None,
active_layers: HashMap::new(),
layer_configs: HashMap::new(),
layer_config_dialog: None,
analog_dpad_modes: HashMap::new(),
analog_deadzones_xy: HashMap::new(),
analog_outer_deadzones_xy: HashMap::new(),
led_states: HashMap::new(),
led_config_device: None,
selected_led_zone: None,
pending_led_color: None,
current_focus: None,
focus_tracking_active: false,
auto_switch_view: None,
hotkey_view: None,
analog_calibration_view: None,
macro_settings: MacroSettings {
latency_offset_ms: 0,
jitter_pct: 0.0,
capture_mouse: false,
},
current_theme: aether_dark(),
}
}
}
#[derive(Debug, Clone)]
pub enum Message {
SwitchTab(Tab),
ThemeChanged(iced::Theme),
LoadDevices,
DevicesLoaded(Result<Vec<DeviceInfo>, String>),
GrabDevice(String),
UngrabDevice(String),
DeviceGrabbed(Result<String, String>),
DeviceUngrabbed(Result<String, String>),
SelectDevice(usize),
UpdateMacroName(String),
StartRecording,
StopRecording,
RecordingStarted(Result<String, String>),
RecordingStopped(Result<MacroEntry, String>),
LoadMacros,
MacrosLoaded(Result<Vec<MacroEntry>, String>),
LoadMacroSettings,
MacroSettingsLoaded(Result<MacroSettings, String>),
SetMacroSettings(MacroSettings),
LatencyChanged(u32),
JitterChanged(f32),
CaptureMouseToggled(bool),
PlayMacro(String),
MacroPlayed(Result<String, String>),
DeleteMacro(String),
MacroDeleted(Result<String, String>),
UpdateProfileName(String),
SaveProfile,
ProfileSaved(Result<(String, usize), String>),
LoadProfile,
ProfileLoaded(Result<(String, usize), String>),
LoadDeviceProfiles(String),
DeviceProfilesLoaded(String, Result<Vec<String>, String>),
ActivateProfile(String, String),
ProfileActivated(String, String),
DeactivateProfile(String),
ProfileDeactivated(String),
ProfileError(String),
LoadRemapProfiles(String),
RemapProfilesLoaded(String, Result<Vec<RemapProfileInfo>, String>),
ActivateRemapProfile(String, String),
RemapProfileActivated(String, String),
DeactivateRemapProfile(String),
RemapProfileDeactivated(String),
LoadActiveRemaps(String),
ActiveRemapsLoaded(String, Result<Option<(String, Vec<RemapEntry>)>, String>),
CheckDaemonConnection,
DaemonStatusChanged(bool),
TickAnimations,
ShowNotification(String, bool),
RecordMouseEvent {
event_type: String,
button: Option<u16>,
x: i32,
y: i32,
delta: i32,
},
ShowKeypadView(String),
SelectKeypadButton(String),
DeviceCapabilitiesLoaded(String, Result<DeviceCapabilities, String>),
LayerStateChanged(String, usize),
LayerConfigRequested(String),
LayerActivateRequested(String, usize, LayerMode),
LayerConfigUpdated(String, LayerConfigInfo),
OpenLayerConfigDialog(String, usize),
LayerConfigNameChanged(String),
LayerConfigModeChanged(LayerMode),
SaveLayerConfig,
CancelLayerConfig,
RefreshLayers,
LayerListLoaded(String, Vec<LayerConfigInfo>),
AnalogDpadModeRequested(String),
AnalogDpadModeLoaded(String, String),
SetAnalogDpadMode(String, String),
AnalogDpadModeSet(Result<(), String>),
AnalogDeadzoneXYRequested(String),
AnalogDeadzoneXYLoaded(String, (u8, u8)),
SetAnalogDeadzoneXY(String, u8, u8),
AnalogDeadzoneXYSet(Result<(), String>),
AnalogOuterDeadzoneXYRequested(String),
AnalogOuterDeadzoneXYLoaded(String, (u8, u8)),
SetAnalogOuterDeadzoneXY(String, u8, u8),
AnalogOuterDeadzoneXYSet(Result<(), String>),
OpenLedConfig(String),
CloseLedConfig,
SelectLedZone(LedZone),
SetLedColor(String, LedZone, u8, u8, u8),
LedColorSet(Result<(), String>),
SetLedBrightness(String, Option<LedZone>, u8),
LedBrightnessSet(Result<(), String>),
SetLedPattern(String, LedPattern),
LedPatternSet(Result<(), String>),
RefreshLedState(String),
LedStateLoaded(String, Result<HashMap<LedZone, (u8, u8, u8)>, String>),
LedSliderChanged(u8, u8, u8),
StartFocusTracking,
FocusTrackingStarted(Result<bool, String>),
FocusChanged(String, Option<String>),
ShowAutoSwitchRules(String),
CloseAutoSwitchRules,
LoadAutoSwitchRules(String),
AutoSwitchRulesLoaded(Result<Vec<AutoSwitchRule>, String>),
EditAutoSwitchRule(usize),
AutoSwitchAppIdChanged(String),
AutoSwitchProfileNameChanged(String),
AutoSwitchLayerIdChanged(String),
AutoSwitchUseCurrentApp,
SaveAutoSwitchRule,
DeleteAutoSwitchRule(usize),
ShowHotkeyBindings(String),
CloseHotkeyBindings,
LoadHotkeyBindings(String),
HotkeyBindingsLoaded(Result<Vec<HotkeyBinding>, String>),
EditHotkeyBinding(usize),
ToggleHotkeyModifier(String),
HotkeyKeyChanged(String),
HotkeyProfileNameChanged(String),
HotkeyLayerIdChanged(String),
SaveHotkeyBinding,
DeleteHotkeyBinding(usize),
HotkeyBindingsUpdated(Vec<HotkeyBinding>),
OpenAnalogCalibration {
device_id: String,
layer_id: usize,
},
AnalogDeadzoneChanged(f32),
AnalogDeadzoneShapeChanged(DeadzoneShape),
AnalogSensitivityChanged(f32),
AnalogSensitivityCurveChanged(SensitivityCurve),
AnalogRangeMinChanged(i32),
AnalogRangeMaxChanged(i32),
AnalogInvertXToggled(bool),
AnalogInvertYToggled(bool),
AnalogModeChanged(AnalogMode),
CameraModeChanged(CameraOutputMode),
ApplyAnalogCalibration,
AnalogCalibrationLoaded(Result<aethermap_common::AnalogCalibrationConfig, String>),
AnalogCalibrationApplied(Result<(), String>),
CloseAnalogCalibration,
AnalogInputUpdated(f32, f32), }
#[allow(dead_code)]
pub enum _FutureMessage {
DismissNotification,
}
impl Application for State {
type Message = Message;
type Theme = Theme;
type Executor = iced::executor::Default;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) {
let initial_state = State::default();
let initial_commands = Command::batch([
Command::perform(async { Message::CheckDaemonConnection }, |msg| msg),
Command::perform(async { Message::LoadDevices }, |msg| msg),
Command::perform(async { Message::LoadMacroSettings }, |msg| msg),
]);
(initial_state, initial_commands)
}
fn title(&self) -> String {
String::from("Aethermap")
}
fn theme(&self) -> Theme {
self.current_theme.clone()
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::ThemeChanged(theme) => {
self.current_theme = theme;
Command::none()
}
Message::SwitchTab(tab) => {
self.active_tab = tab;
Command::none()
}
Message::SelectDevice(idx) => {
self.selected_device = Some(idx);
if let Some(device) = self.devices.get(idx) {
let device_id = format!("{:04x}:{:04x}", device.vendor_id, device.product_id);
if device.device_type == DeviceType::Gamepad
|| device.device_type == DeviceType::Keypad
{
let device_id_clone1 = device_id.clone();
let device_id_clone2 = device_id.clone();
let device_id_clone3 = device_id.clone();
return Command::batch(vec![
Command::none(),
Command::perform(async move { device_id_clone1 }, |id| {
Message::AnalogDpadModeRequested(id)
}),
Command::perform(async move { device_id_clone2 }, |id| {
Message::AnalogDeadzoneXYRequested(id)
}),
Command::perform(async move { device_id_clone3 }, |id| {
Message::AnalogOuterDeadzoneXYRequested(id)
}),
]);
}
}
Command::none()
}
Message::CheckDaemonConnection => {
let socket_path = self.socket_path.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.connect().await.is_ok()
},
Message::DaemonStatusChanged,
)
}
Message::DaemonStatusChanged(connected) => {
self.daemon_connected = connected;
if connected {
self.add_notification("Connected to daemon", false);
Command::perform(async { Message::StartFocusTracking }, |msg| msg)
} else {
self.add_notification("Daemon not running - start aethermapd", true);
Command::none()
}
}
Message::StartFocusTracking => {
Command::perform(
async move {
let wayland_available = std::env::var("WAYLAND_DISPLAY").is_ok();
if wayland_available {
tracing::info!("Focus tracking available (Wayland detected)");
} else {
tracing::warn!("Focus tracking unavailable (not on Wayland)");
}
wayland_available
},
|available| Message::FocusTrackingStarted(Ok(available)),
)
}
Message::FocusTrackingStarted(Ok(available)) => {
self.focus_tracking_active = available;
if available {
self.add_notification("Focus tracking enabled", false);
} else {
self.add_notification(
"Focus tracking unavailable (portal not connected)",
true,
);
}
Command::none()
}
Message::FocusTrackingStarted(Err(e)) => {
self.add_notification(&format!("Focus tracking error: {}", e), true);
self.focus_tracking_active = false;
Command::none()
}
Message::FocusChanged(app_id, window_title) => {
self.current_focus = Some(app_id.clone());
let socket_path = self.socket_path.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.send_focus_change(app_id, window_title).await
},
|result| match result {
Ok(()) => Message::TickAnimations, Err(e) => Message::ProfileError(format!("Focus change failed: {}", e)),
},
)
}
Message::ShowAutoSwitchRules(device_id) => {
crate::handlers::auto_switch::show(self, device_id)
}
Message::CloseAutoSwitchRules => crate::handlers::auto_switch::close(self),
Message::LoadAutoSwitchRules(_device_id) => crate::handlers::auto_switch::load(self),
Message::AutoSwitchRulesLoaded(Ok(rules)) => {
crate::handlers::auto_switch::loaded(self, rules)
}
Message::AutoSwitchRulesLoaded(Err(error)) => {
crate::handlers::auto_switch::load_error(self, error)
}
Message::EditAutoSwitchRule(index) => crate::handlers::auto_switch::edit(self, index),
Message::AutoSwitchAppIdChanged(value) => {
crate::handlers::auto_switch::app_id_changed(self, value)
}
Message::AutoSwitchProfileNameChanged(value) => {
crate::handlers::auto_switch::profile_name_changed(self, value)
}
Message::AutoSwitchLayerIdChanged(value) => {
crate::handlers::auto_switch::layer_id_changed(self, value)
}
Message::AutoSwitchUseCurrentApp => crate::handlers::auto_switch::use_current_app(self),
Message::SaveAutoSwitchRule => crate::handlers::auto_switch::save(self),
Message::DeleteAutoSwitchRule(index) => {
crate::handlers::auto_switch::delete(self, index)
}
Message::ShowHotkeyBindings(device_id) => {
crate::handlers::hotkeys::show(self, device_id)
}
Message::CloseHotkeyBindings => crate::handlers::hotkeys::close(self),
Message::LoadHotkeyBindings(device_id) => {
crate::handlers::hotkeys::load(self, device_id)
}
Message::HotkeyBindingsLoaded(Ok(bindings)) => {
crate::handlers::hotkeys::loaded(self, bindings)
}
Message::HotkeyBindingsLoaded(Err(error)) => {
crate::handlers::hotkeys::load_error(self, error)
}
Message::EditHotkeyBinding(index) => crate::handlers::hotkeys::edit(self, index),
Message::ToggleHotkeyModifier(modifier) => {
crate::handlers::hotkeys::toggle_modifier(self, modifier)
}
Message::HotkeyKeyChanged(value) => crate::handlers::hotkeys::key_changed(self, value),
Message::HotkeyProfileNameChanged(value) => {
crate::handlers::hotkeys::profile_name_changed(self, value)
}
Message::HotkeyLayerIdChanged(value) => {
crate::handlers::hotkeys::layer_id_changed(self, value)
}
Message::SaveHotkeyBinding => crate::handlers::hotkeys::save(self),
Message::DeleteHotkeyBinding(index) => crate::handlers::hotkeys::delete(self, index),
Message::HotkeyBindingsUpdated(bindings) => {
crate::handlers::hotkeys::bindings_updated(self, bindings)
}
Message::OpenAnalogCalibration {
device_id,
layer_id,
} => crate::handlers::analog::open(self, device_id, layer_id),
Message::AnalogCalibrationLoaded(Ok(calibration)) => {
crate::handlers::analog::loaded(self, calibration)
}
Message::AnalogCalibrationLoaded(Err(error)) => {
crate::handlers::analog::load_error(self, error)
}
Message::AnalogDeadzoneChanged(value) => {
crate::handlers::analog::deadzone_changed(self, value)
}
Message::AnalogDeadzoneShapeChanged(shape) => {
crate::handlers::analog::deadzone_shape_changed(self, shape)
}
Message::AnalogSensitivityChanged(value) => {
crate::handlers::analog::sensitivity_changed(self, value)
}
Message::AnalogSensitivityCurveChanged(curve) => {
crate::handlers::analog::sensitivity_curve_changed(self, curve)
}
Message::AnalogRangeMinChanged(value) => {
crate::handlers::analog::range_min_changed(self, value)
}
Message::AnalogRangeMaxChanged(value) => {
crate::handlers::analog::range_max_changed(self, value)
}
Message::AnalogInvertXToggled(checked) => {
crate::handlers::analog::invert_x_toggled(self, checked)
}
Message::AnalogInvertYToggled(checked) => {
crate::handlers::analog::invert_y_toggled(self, checked)
}
Message::AnalogModeChanged(mode) => {
crate::handlers::analog::analog_mode_changed(self, mode)
}
Message::CameraModeChanged(mode) => {
crate::handlers::analog::camera_mode_changed(self, mode)
}
Message::ApplyAnalogCalibration => crate::handlers::analog::apply(self),
Message::AnalogCalibrationApplied(Ok(())) => crate::handlers::analog::applied_ok(self),
Message::AnalogCalibrationApplied(Err(error)) => {
crate::handlers::analog::applied_error(self, error)
}
Message::CloseAnalogCalibration => crate::handlers::analog::close(self),
Message::AnalogInputUpdated(x, y) => crate::handlers::analog::input_updated(self, x, y),
Message::LoadDevices => {
let socket_path = self.socket_path.clone();
self.loading = true;
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.get_devices().await.map_err(|e| e.to_string())
},
Message::DevicesLoaded,
)
}
Message::DevicesLoaded(Ok(devices)) => {
let count = devices.len();
self.devices = devices;
self.loading = false;
self.add_notification(&format!("Found {} devices", count), false);
Command::perform(async { Message::LoadMacros }, |msg| msg)
}
Message::DevicesLoaded(Err(e)) => {
self.loading = false;
self.add_notification(&format!("Error: {}", e), true);
Command::none()
}
Message::LoadMacros => crate::handlers::macros::load(self),
Message::MacrosLoaded(Ok(macros)) => crate::handlers::macros::loaded(self, macros),
Message::MacrosLoaded(Err(e)) => crate::handlers::macros::load_error(self, e),
Message::LoadMacroSettings => crate::handlers::macros::load_settings(self),
Message::MacroSettingsLoaded(Ok(settings)) => {
crate::handlers::macros::settings_loaded(self, settings)
}
Message::MacroSettingsLoaded(Err(e)) => {
crate::handlers::macros::settings_load_error(self, e)
}
Message::SetMacroSettings(settings) => {
crate::handlers::macros::set_settings(self, settings)
}
Message::LatencyChanged(ms) => crate::handlers::macros::latency_changed(self, ms),
Message::JitterChanged(pct) => crate::handlers::macros::jitter_changed(self, pct),
Message::CaptureMouseToggled(enabled) => {
crate::handlers::macros::capture_mouse_toggled(self, enabled)
}
Message::PlayMacro(macro_name) => crate::handlers::macros::play(self, macro_name),
Message::MacroPlayed(Ok(name)) => crate::handlers::macros::played_ok(self, name),
Message::MacroPlayed(Err(e)) => crate::handlers::macros::played_error(self, e),
Message::UpdateMacroName(name) => crate::handlers::macros::update_name(self, name),
Message::UpdateProfileName(name) => {
crate::handlers::macros::update_profile_name(self, name)
}
Message::StartRecording => crate::handlers::macros::start_recording(self),
Message::RecordingStarted(Ok(name)) => {
crate::handlers::macros::recording_started_ok(self, name)
}
Message::RecordingStarted(Err(e)) => {
crate::handlers::macros::recording_started_error(self, e)
}
Message::StopRecording => crate::handlers::macros::stop_recording(self),
Message::RecordingStopped(Ok(macro_entry)) => {
crate::handlers::macros::recording_stopped_ok(self, macro_entry)
}
Message::RecordingStopped(Err(e)) => {
crate::handlers::macros::recording_stopped_error(self, e)
}
Message::DeleteMacro(macro_name) => crate::handlers::macros::delete(self, macro_name),
Message::MacroDeleted(Ok(name)) => crate::handlers::macros::deleted_ok(self, name),
Message::MacroDeleted(Err(e)) => crate::handlers::macros::deleted_error(self, e),
Message::SaveProfile => {
if self.profile_name.trim().is_empty() {
self.add_notification("Enter a profile name", true);
return Command::none();
}
let socket_path = self.socket_path.clone();
let name = self.profile_name.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.save_profile(&name).await.map_err(|e| e.to_string())
},
Message::ProfileSaved,
)
}
Message::ProfileSaved(Ok((name, count))) => {
self.add_notification(&format!("Saved '{}' ({} macros)", name, count), false);
Command::none()
}
Message::ProfileSaved(Err(e)) => {
self.add_notification(&format!("Save failed: {}", e), true);
Command::none()
}
Message::LoadProfile => {
if self.profile_name.trim().is_empty() {
self.add_notification("Enter a profile name to load", true);
return Command::none();
}
let socket_path = self.socket_path.clone();
let name = self.profile_name.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.load_profile(&name).await.map_err(|e| e.to_string())
},
Message::ProfileLoaded,
)
}
Message::ProfileLoaded(Ok((name, count))) => {
self.add_notification(&format!("Loaded '{}' ({} macros)", name, count), false);
Command::perform(async { Message::LoadMacros }, |msg| msg)
}
Message::ProfileLoaded(Err(e)) => {
self.add_notification(&format!("Load failed: {}", e), true);
Command::none()
}
Message::TickAnimations => {
let now = Instant::now();
self.recently_updated_macros
.retain(|_, timestamp| now.duration_since(*timestamp) < Duration::from_secs(3));
self.recording_pulse = !self.recording_pulse;
while let Some(notif) = self.notifications.front() {
if now.duration_since(notif.timestamp) > Duration::from_secs(5) {
self.notifications.pop_front();
} else {
break;
}
}
Command::none()
}
Message::ShowNotification(message, is_error) => {
self.add_notification(&message, is_error);
Command::none()
}
Message::GrabDevice(device_path) => {
let socket_path = self.socket_path.clone();
let path_clone = device_path.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client
.grab_device(&path_clone)
.await
.map(|_| path_clone)
.map_err(|e| e.to_string())
},
Message::DeviceGrabbed,
)
}
Message::UngrabDevice(device_path) => {
let socket_path = self.socket_path.clone();
let path_clone = device_path.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client
.ungrab_device(&path_clone)
.await
.map(|_| path_clone)
.map_err(|e| e.to_string())
},
Message::DeviceUngrabbed,
)
}
Message::DeviceGrabbed(Ok(device_path)) => {
self.grabbed_devices.insert(device_path.clone());
if let Some(idx) = self
.devices
.iter()
.position(|d| d.path.to_string_lossy() == device_path)
{
self.selected_device = Some(idx);
}
self.add_notification("Device grabbed - ready for recording", false);
Command::none()
}
Message::DeviceGrabbed(Err(e)) => {
self.add_notification(&format!("Grab failed: {}", e), true);
Command::none()
}
Message::DeviceUngrabbed(Ok(device_path)) => {
self.grabbed_devices.remove(&device_path);
self.add_notification("Device released", false);
Command::none()
}
Message::DeviceUngrabbed(Err(e)) => {
self.add_notification(&format!("Release failed: {}", e), true);
Command::none()
}
Message::LoadDeviceProfiles(device_id) => {
let socket_path = self.socket_path.clone();
let id = device_id.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
(id.clone(), client.get_device_profiles(id).await)
},
|(device_id, result)| {
Message::DeviceProfilesLoaded(device_id, result.map_err(|e| e.to_string()))
},
)
}
Message::DeviceProfilesLoaded(device_id, Ok(profiles)) => {
self.device_profiles.insert(device_id.clone(), profiles);
self.add_notification(
&format!(
"Loaded {} profiles for {}",
self.device_profiles
.get(&device_id)
.map(|p| p.len())
.unwrap_or(0),
device_id
),
false,
);
Command::none()
}
Message::DeviceProfilesLoaded(_device_id, Err(e)) => {
self.add_notification(&format!("Failed to load device profiles: {}", e), true);
Command::none()
}
Message::ActivateProfile(device_id, profile_name) => {
let socket_path = self.socket_path.clone();
let id = device_id.clone();
let name = profile_name.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.activate_profile(id.clone(), name.clone()).await
},
move |result| match result {
Ok(()) => Message::ProfileActivated(device_id, profile_name),
Err(e) => {
Message::ProfileError(format!("Failed to activate profile: {}", e))
}
},
)
}
Message::ProfileActivated(device_id, profile_name) => {
self.active_profiles
.insert(device_id.clone(), profile_name.clone());
self.add_notification(
&format!("Activated profile '{}' on {}", profile_name, device_id),
false,
);
Command::none()
}
Message::DeactivateProfile(device_id) => {
let socket_path = self.socket_path.clone();
let id = device_id.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.deactivate_profile(id.clone()).await
},
move |result| match result {
Ok(()) => Message::ProfileDeactivated(device_id),
Err(e) => {
Message::ProfileError(format!("Failed to deactivate profile: {}", e))
}
},
)
}
Message::ProfileDeactivated(device_id) => {
self.active_profiles.remove(&device_id);
self.add_notification(&format!("Deactivated profile on {}", device_id), false);
Command::none()
}
Message::ProfileError(msg) => {
self.add_notification(&msg, true);
Command::none()
}
Message::LoadRemapProfiles(device_path) => {
let socket_path = self.socket_path.clone();
let path = device_path.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
(path.clone(), client.list_remap_profiles(&path).await)
},
|(device_path, result)| {
Message::RemapProfilesLoaded(device_path, result.map_err(|e| e.to_string()))
},
)
}
Message::RemapProfilesLoaded(device_path, Ok(profiles)) => {
self.remap_profiles.insert(device_path.clone(), profiles);
self.add_notification(
&format!(
"Loaded {} remap profiles for {}",
self.remap_profiles
.get(&device_path)
.map(|p| p.len())
.unwrap_or(0),
device_path
),
false,
);
Command::none()
}
Message::RemapProfilesLoaded(_device_path, Err(e)) => {
self.add_notification(&format!("Failed to load remap profiles: {}", e), true);
Command::none()
}
Message::ActivateRemapProfile(device_path, profile_name) => {
let socket_path = self.socket_path.clone();
let path = device_path.clone();
let name = profile_name.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.activate_remap_profile(&path, &name).await
},
move |result| match result {
Ok(()) => Message::RemapProfileActivated(device_path, profile_name),
Err(e) => Message::ProfileError(format!(
"Failed to activate remap profile: {}",
e
)),
},
)
}
Message::RemapProfileActivated(device_path, profile_name) => {
self.active_remap_profiles
.insert(device_path.clone(), profile_name.clone());
self.add_notification(
&format!(
"Activated remap profile '{}' on {}",
profile_name, device_path
),
false,
);
Command::perform(async move { device_path.clone() }, |path| {
Message::LoadActiveRemaps(path)
})
}
Message::DeactivateRemapProfile(device_path) => {
let socket_path = self.socket_path.clone();
let path = device_path.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.deactivate_remap_profile(&path).await
},
move |result| match result {
Ok(()) => Message::RemapProfileDeactivated(device_path),
Err(e) => Message::ProfileError(format!(
"Failed to deactivate remap profile: {}",
e
)),
},
)
}
Message::RemapProfileDeactivated(device_path) => {
self.active_remap_profiles.remove(&device_path);
self.active_remaps.remove(&device_path);
self.add_notification(
&format!("Deactivated remap profile on {}", device_path),
false,
);
Command::none()
}
Message::LoadActiveRemaps(device_path) => {
let socket_path = self.socket_path.clone();
let path = device_path.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
(path.clone(), client.get_active_remaps(&path).await)
},
|(device_path, result)| {
Message::ActiveRemapsLoaded(device_path, result.map_err(|e| e.to_string()))
},
)
}
Message::ActiveRemapsLoaded(device_path, Ok(Some((profile_name, remaps)))) => {
self.active_remaps
.insert(device_path.clone(), (profile_name, remaps));
Command::none()
}
Message::ActiveRemapsLoaded(device_path, Ok(None)) => {
self.active_remaps.remove(&device_path);
Command::none()
}
Message::ActiveRemapsLoaded(_device_path, Err(e)) => {
self.add_notification(&format!("Failed to load active remaps: {}", e), true);
Command::none()
}
Message::RecordMouseEvent {
event_type,
button,
x,
y,
delta,
} => {
if self.recording {
let event_desc = match event_type.as_str() {
"button_press" => format!("Mouse button {} pressed", button.unwrap_or(0)),
"button_release" => {
format!("Mouse button {} released", button.unwrap_or(0))
}
"movement" => format!("Mouse moved to ({}, {})", x, y),
"scroll" => format!("Mouse scrolled {}", delta),
_ => format!("Unknown mouse event: {}", event_type),
};
self.status = event_desc;
}
Command::none()
}
Message::ShowKeypadView(device_path) => {
if device_path.is_empty() {
self.device_capabilities = None;
self.keypad_layout.clear();
self.keypad_view_device = None;
self.selected_button = None;
return Command::none();
}
self.keypad_view_device = Some(device_path.clone());
let socket_path = self.socket_path.clone();
let path_clone = device_path.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
(
path_clone.clone(),
client.get_device_capabilities(&path_clone).await,
)
},
|(device_path, result)| {
Message::DeviceCapabilitiesLoaded(
device_path,
result.map_err(|e| e.to_string()),
)
},
)
}
Message::DeviceCapabilitiesLoaded(device_path, Ok(capabilities)) => {
self.device_capabilities = Some(capabilities);
self.keypad_layout = azeron_keypad_layout();
if let Some((profile_name, remaps)) = self.active_remaps.get(&device_path) {
for remap in remaps {
if let Some(button) = self
.keypad_layout
.iter_mut()
.find(|b| b.id == remap.from_key)
{
button.current_remap = Some(remap.to_key.clone());
}
}
self.add_notification(
&format!("Loaded remaps from profile '{}'", profile_name),
false,
);
}
self.active_tab = Tab::Devices;
Command::none()
}
Message::DeviceCapabilitiesLoaded(_device_path, Err(e)) => {
self.add_notification(&format!("Failed to load device capabilities: {}", e), true);
Command::none()
}
Message::SelectKeypadButton(button_id) => {
self.selected_button = self.keypad_layout.iter().position(|b| b.id == button_id);
self.status = format!(
"Selected button: {} - Configure remapping in device profile",
button_id
);
Command::none()
}
Message::LayerStateChanged(device_id, layer_id) => {
self.active_layers.insert(device_id, layer_id);
Command::none()
}
Message::LayerConfigRequested(device_id) => {
let socket_path = self.socket_path.clone();
let id = device_id.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
(id.clone(), client.list_layers(&id).await)
},
|(device_id, result)| match result {
Ok(layers) => {
if let Some(active_layer) = layers.first() {
Message::LayerStateChanged(device_id, active_layer.layer_id)
} else {
Message::TickAnimations }
}
Err(e) => Message::ProfileError(format!("Failed to load layers: {}", e)),
},
)
}
Message::LayerActivateRequested(device_id, layer_id, mode) => {
let socket_path = self.socket_path.clone();
let id = device_id.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.activate_layer(&id, layer_id, mode).await
},
move |result| match result {
Ok(()) => Message::LayerStateChanged(device_id, layer_id),
Err(e) => Message::ProfileError(format!("Failed to activate layer: {}", e)),
},
)
}
Message::LayerConfigUpdated(device_id, config) => {
let socket_path = self.socket_path.clone();
let id = device_id.clone();
let layer_id = config.layer_id;
let name = config.name.clone();
let mode = config.mode;
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.set_layer_config(&id, layer_id, name, mode).await
},
move |result| match result {
Ok(()) => {
Message::LayerConfigRequested(device_id)
}
Err(e) => {
Message::ProfileError(format!("Failed to update layer config: {}", e))
}
},
)
}
Message::OpenLayerConfigDialog(device_id, layer_id) => {
let current_name = self
.layer_configs
.get(&device_id)
.and_then(|layers| layers.iter().find(|l| l.layer_id == layer_id))
.map(|l| l.name.clone())
.unwrap_or_else(|| format!("Layer {}", layer_id));
let current_mode = self
.layer_configs
.get(&device_id)
.and_then(|layers| layers.iter().find(|l| l.layer_id == layer_id))
.map(|l| l.mode)
.unwrap_or(LayerMode::Hold);
self.layer_config_dialog = Some((device_id, layer_id, current_name, current_mode));
Command::none()
}
Message::LayerConfigNameChanged(name) => {
if let Some((device_id, layer_id, _, mode)) = self.layer_config_dialog.take() {
self.layer_config_dialog = Some((device_id, layer_id, name, mode));
}
Command::none()
}
Message::LayerConfigModeChanged(mode) => {
if let Some((device_id, layer_id, name, _)) = self.layer_config_dialog.take() {
self.layer_config_dialog = Some((device_id, layer_id, name, mode));
}
Command::none()
}
Message::SaveLayerConfig => {
if let Some((device_id, layer_id, name, mode)) = self.layer_config_dialog.take() {
let config = LayerConfigInfo {
layer_id,
name: name.clone(),
mode,
remap_count: 0,
led_color: (0, 0, 255), led_zone: None, };
Command::perform(async move { (device_id, config) }, |(device_id, config)| {
Message::LayerConfigUpdated(device_id, config)
})
} else {
Command::none()
}
}
Message::CancelLayerConfig => {
self.layer_config_dialog = None;
Command::none()
}
Message::RefreshLayers => {
let mut commands = Vec::new();
for device_id in self.device_profiles.keys() {
let device_id = device_id.clone();
let socket_path = self.socket_path.clone();
commands.push(Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
(device_id.clone(), client.list_layers(&device_id).await)
},
|(device_id, result)| match result {
Ok(layers) => {
Message::LayerListLoaded(device_id, layers)
}
Err(_) => Message::TickAnimations, },
));
}
for device_id in self.active_layers.keys().cloned().collect::<Vec<_>>() {
let device_id = device_id.clone();
let socket_path = self.socket_path.clone();
commands.push(Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
(device_id.clone(), client.get_active_layer(&device_id).await)
},
|(device_id, result)| match result {
Ok(Some(layer_id)) => Message::LayerStateChanged(device_id, layer_id),
_ => Message::TickAnimations,
},
));
}
Command::batch(commands)
}
Message::LayerListLoaded(device_id, layers) => {
self.layer_configs.insert(device_id.clone(), layers);
Command::none()
}
Message::AnalogDpadModeRequested(device_id) => {
let socket_path = self.socket_path.clone();
let device_id_clone = device_id.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.get_analog_dpad_mode(&device_id_clone).await
},
move |result| match result {
Ok(mode) => Message::AnalogDpadModeLoaded(device_id, mode),
Err(e) => {
eprintln!("Failed to get D-pad mode: {}", e);
Message::TickAnimations }
},
)
}
Message::AnalogDpadModeLoaded(device_id, mode) => {
self.analog_dpad_modes.insert(device_id, mode);
Command::none()
}
Message::SetAnalogDpadMode(device_id, mode) => {
let socket_path = self.socket_path.clone();
let device_id_clone = device_id.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.set_analog_dpad_mode(&device_id_clone, &mode).await
},
|result| match result {
Ok(_) => Message::AnalogDpadModeSet(Ok(())),
Err(e) => Message::AnalogDpadModeSet(Err(e)),
},
)
}
Message::AnalogDpadModeSet(result) => {
match result {
Ok(_) => {
Command::none()
}
Err(e) => {
eprintln!("Failed to set D-pad mode: {}", e);
Command::none()
}
}
}
Message::AnalogDeadzoneXYRequested(device_id) => {
let socket_path = self.socket_path.clone();
let device_id_clone = device_id.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.get_analog_deadzone_xy(&device_id_clone).await
},
move |result| match result {
Ok((x_pct, y_pct)) => {
Message::AnalogDeadzoneXYLoaded(device_id, (x_pct, y_pct))
}
Err(e) => {
eprintln!("Failed to get per-axis deadzone: {}", e);
Message::TickAnimations }
},
)
}
Message::AnalogDeadzoneXYLoaded(device_id, (x_pct, y_pct)) => {
self.analog_deadzones_xy.insert(device_id, (x_pct, y_pct));
Command::none()
}
Message::SetAnalogDeadzoneXY(device_id, x_pct, y_pct) => {
let socket_path = self.socket_path.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client
.set_analog_deadzone_xy(&device_id, x_pct, y_pct)
.await
},
|result| match result {
Ok(_) => Message::AnalogDeadzoneXYSet(Ok(())),
Err(e) => Message::AnalogDeadzoneXYSet(Err(e)),
},
)
}
Message::AnalogDeadzoneXYSet(result) => {
match result {
Ok(_) => {
Command::none()
}
Err(e) => {
eprintln!("Failed to set per-axis deadzone: {}", e);
self.add_notification(&format!("Failed to set deadzone: {}", e), true);
Command::none()
}
}
}
Message::AnalogOuterDeadzoneXYRequested(device_id) => {
let socket_path = self.socket_path.clone();
let device_id_clone = device_id.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client.get_analog_outer_deadzone_xy(&device_id_clone).await
},
move |result| match result {
Ok((x_pct, y_pct)) => {
Message::AnalogOuterDeadzoneXYLoaded(device_id, (x_pct, y_pct))
}
Err(e) => {
eprintln!("Failed to get per-axis outer deadzone: {}", e);
Message::TickAnimations }
},
)
}
Message::AnalogOuterDeadzoneXYLoaded(device_id, (x_pct, y_pct)) => {
self.analog_outer_deadzones_xy
.insert(device_id, (x_pct, y_pct));
Command::none()
}
Message::SetAnalogOuterDeadzoneXY(device_id, x_pct, y_pct) => {
let socket_path = self.socket_path.clone();
Command::perform(
async move {
let client = crate::ipc::IpcClient::new(socket_path);
client
.set_analog_outer_deadzone_xy(&device_id, x_pct, y_pct)
.await
},
|result| match result {
Ok(_) => Message::AnalogOuterDeadzoneXYSet(Ok(())),
Err(e) => Message::AnalogOuterDeadzoneXYSet(Err(e)),
},
)
}
Message::AnalogOuterDeadzoneXYSet(result) => {
match result {
Ok(_) => {
Command::none()
}
Err(e) => {
eprintln!("Failed to set per-axis outer deadzone: {}", e);
self.add_notification(
&format!("Failed to set outer deadzone: {}", e),
true,
);
Command::none()
}
}
}
Message::OpenLedConfig(device_id) => crate::handlers::led::open(self, device_id),
Message::CloseLedConfig => crate::handlers::led::close(self),
Message::SelectLedZone(zone) => crate::handlers::led::select_zone(self, zone),
Message::RefreshLedState(device_id) => crate::handlers::led::refresh(self, device_id),
Message::LedStateLoaded(device_id, result) => {
crate::handlers::led::state_loaded(self, device_id, result)
}
Message::SetLedColor(device_id, zone, red, green, blue) => {
crate::handlers::led::set_color(self, device_id, zone, red, green, blue)
}
Message::LedColorSet(result) => crate::handlers::led::color_set(self, result),
Message::SetLedBrightness(device_id, zone, brightness) => {
crate::handlers::led::set_brightness(self, device_id, zone, brightness)
}
Message::LedBrightnessSet(result) => crate::handlers::led::brightness_set(self, result),
Message::SetLedPattern(device_id, pattern) => {
crate::handlers::led::set_pattern(self, device_id, pattern)
}
Message::LedPatternSet(result) => crate::handlers::led::pattern_set(self, result),
Message::LedSliderChanged(red, green, blue) => {
crate::handlers::led::slider_changed(self, red, green, blue)
}
}
}
fn view(&self) -> Element<'_, Message> {
let sidebar = self.view_sidebar();
let main_content = self.view_main_content();
let status_bar = self.view_status_bar();
let main_layout = row![
sidebar,
vertical_rule(1),
column![main_content, horizontal_rule(1), status_bar,].height(Length::Fill)
];
let base: Element<'_, Message> = container(main_layout)
.width(Length::Fill)
.height(Length::Fill)
.into();
if let Some(dialog) = views::devices::layer_config_dialog(self) {
container(column![base, dialog,].height(Length::Fill))
.width(Length::Fill)
.height(Length::Fill)
.into()
} else if let Some(led_dialog) = self.view_led_config() {
container(column![base, led_dialog,].height(Length::Fill))
.width(Length::Fill)
.height(Length::Fill)
.into()
} else if let Some(calib_dialog) = self.view_analog_calibration() {
container(column![base, calib_dialog,].height(Length::Fill))
.width(Length::Fill)
.height(Length::Fill)
.into()
} else {
base
}
}
fn subscription(&self) -> Subscription<Message> {
let timer = iced::time::every(Duration::from_millis(500)).map(|_| Message::TickAnimations);
let layer_refresh =
iced::time::every(Duration::from_secs(2)).map(|_| Message::RefreshLayers);
let mouse_events = iced::event::listen_with(|event, _status| {
match event {
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(
iced::mouse::Button::Left,
)) => {
Some(Message::RecordMouseEvent {
event_type: "button_press".to_string(),
button: Some(0x110), x: 0,
y: 0,
delta: 0,
})
}
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(
iced::mouse::Button::Right,
)) => {
Some(Message::RecordMouseEvent {
event_type: "button_press".to_string(),
button: Some(0x111), x: 0,
y: 0,
delta: 0,
})
}
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(
iced::mouse::Button::Middle,
)) => {
Some(Message::RecordMouseEvent {
event_type: "button_press".to_string(),
button: Some(0x112), x: 0,
y: 0,
delta: 0,
})
}
iced::Event::Mouse(iced::mouse::Event::ButtonReleased(_)) => {
Some(Message::RecordMouseEvent {
event_type: "button_release".to_string(),
button: Some(0),
x: 0,
y: 0,
delta: 0,
})
}
iced::Event::Mouse(iced::mouse::Event::WheelScrolled { delta }) => {
let scroll_delta = match delta {
iced::mouse::ScrollDelta::Lines { y, .. } => y as i32,
iced::mouse::ScrollDelta::Pixels { y, .. } => y as i32,
};
Some(Message::RecordMouseEvent {
event_type: "scroll".to_string(),
button: None,
x: 0,
y: 0,
delta: scroll_delta,
})
}
iced::Event::Mouse(iced::mouse::Event::CursorMoved { .. }) => {
Some(Message::RecordMouseEvent {
event_type: "movement".to_string(),
button: None,
x: 0,
y: 0,
delta: 0,
})
}
_ => None,
}
});
let mouse_subscription = if self.recording {
mouse_events
} else {
Subscription::none()
};
let theme_subscription = iced::subscription::unfold(
"ashpd-theme",
None,
|state: Option<
iced::futures::stream::BoxStream<'static, ashpd::desktop::settings::ColorScheme>,
>| async move {
use ashpd::desktop::settings::{ColorScheme, Settings};
use iced::futures::StreamExt;
let mut stream = match state {
Some(s) => s,
None => {
let settings = match Settings::new().await {
Ok(s) => s,
Err(_) => return iced::futures::future::pending().await,
};
let initial = settings
.color_scheme()
.await
.unwrap_or(ColorScheme::NoPreference);
let theme = match initial {
ColorScheme::PreferDark => aether_dark(),
ColorScheme::PreferLight => aether_light(),
ColorScheme::NoPreference => aether_dark(),
};
let s = match settings.receive_color_scheme_changed().await {
Ok(s) => s,
Err(_) => return (Message::ThemeChanged(theme), None),
};
return (Message::ThemeChanged(theme), Some(s.boxed()));
}
};
if let Some(scheme) = stream.next().await {
let theme = match scheme {
ColorScheme::PreferDark => aether_dark(),
ColorScheme::PreferLight => aether_light(),
ColorScheme::NoPreference => aether_dark(),
};
(Message::ThemeChanged(theme), Some(stream))
} else {
iced::futures::future::pending().await
}
},
);
Subscription::batch(vec![
timer,
layer_refresh,
mouse_subscription,
theme_subscription,
])
}
}
impl State {
pub(crate) fn add_notification(&mut self, message: &str, is_error: bool) {
self.notifications.push_back(Notification {
message: message.to_string(),
is_error,
timestamp: Instant::now(),
});
self.status = message.to_string();
self.status_history.push_back(message.to_string());
if self.status_history.len() > 10 {
self.status_history.pop_front();
}
if self.notifications.len() > 5 {
self.notifications.pop_front();
}
}
fn view_sidebar(&self) -> Element<'_, Message> {
views::sidebar::view(self)
}
fn view_main_content(&self) -> Element<'_, Message> {
let content = match self.active_tab {
Tab::Devices => self.view_devices_tab(),
Tab::Macros => self.view_macros_tab(),
Tab::Profiles => self.view_profiles_tab(),
};
container(scrollable(content))
.width(Length::Fill)
.height(Length::Fill)
.padding(24)
.into()
}
fn view_devices_tab(&self) -> Element<'_, Message> {
views::devices::view_devices_tab(self)
}
fn view_macros_tab(&self) -> Element<'_, Message> {
views::macros::view(self)
}
fn view_profiles_tab(&self) -> Element<'_, Message> {
views::profiles::view_profiles_tab(self)
}
fn view_status_bar(&self) -> Element<'_, Message> {
views::status_bar::view(self)
}
pub fn view_led_config(&self) -> Option<Element<'_, Message>> {
views::led::view(self)
}
pub fn view_analog_calibration(&self) -> Option<Element<'_, Message>> {
views::analog::overlay_view(self)
}
}