feat(input): page Logitech pilotee par l'agent OpenLogi (pile, DPI, SmartShift)
This commit is contained in:
parent
0fec31749b
commit
663c17ace3
8 changed files with 1368 additions and 43 deletions
798
Cargo.lock
generated
798
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -54,6 +54,10 @@ iced_winit.workspace = true
|
||||||
locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||||
sysinfo = { version = "=0.38.0", optional = true }
|
sysinfo = { version = "=0.38.0", optional = true }
|
||||||
mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", features = ["tokio"], optional = true }
|
mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", features = ["tokio"], optional = true }
|
||||||
|
# Intégration OpenLogi (page Logitech des périphériques d'entrée).
|
||||||
|
openlogi-core = { path = "../../OpenLogi/crates/openlogi-core" }
|
||||||
|
openlogi-ipc = { path = "../../OpenLogi/crates/openlogi-ipc" }
|
||||||
|
tarpc = { version = "0.38", features = ["serde1", "tokio1"] }
|
||||||
notify = "8.2.0"
|
notify = "8.2.0"
|
||||||
nmrs = { version = "3.5.2", optional = true }
|
nmrs = { version = "3.5.2", optional = true }
|
||||||
regex = "1.13.1"
|
regex = "1.13.1"
|
||||||
|
|
@ -68,7 +72,7 @@ slotmap = "1.1.1"
|
||||||
static_init = "1.0.4"
|
static_init = "1.0.4"
|
||||||
sunrise = "3.0.0"
|
sunrise = "3.0.0"
|
||||||
timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||||
tokio = { workspace = true, features = ["fs", "io-util", "process", "sync"] }
|
tokio = { workspace = true, features = ["fs", "io-util", "process", "sync", "time"] }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||||
udev = { version = "0.9.3", optional = true }
|
udev = { version = "0.9.3", optional = true }
|
||||||
|
|
|
||||||
|
|
@ -515,6 +515,13 @@ impl cosmic::Application for SettingsApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "page-input")]
|
||||||
|
crate::pages::Message::Logitech(message) => {
|
||||||
|
if let Some(page) = self.pages.page_mut::<input::logitech::Page>() {
|
||||||
|
return page.update(message).map(Into::into);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "page-input")]
|
#[cfg(feature = "page-input")]
|
||||||
crate::pages::Message::KeyboardShortcuts(message) => {
|
crate::pages::Message::KeyboardShortcuts(message) => {
|
||||||
if let Some(page) = self.pages.page_mut::<input::keyboard::shortcuts::Page>() {
|
if let Some(page) = self.pages.page_mut::<input::keyboard::shortcuts::Page>() {
|
||||||
|
|
|
||||||
551
cosmic-settings/src/pages/input/logitech.rs
Normal file
551
cosmic-settings/src/pages/input/logitech.rs
Normal file
|
|
@ -0,0 +1,551 @@
|
||||||
|
// Copyright 2026 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
//
|
||||||
|
// Page « Logitech » — intégration OpenLogi dans les périphériques d'entrée.
|
||||||
|
// Pilotée par l'agent OpenLogi (openlogi-agent) via son IPC tarpc local.
|
||||||
|
// Les réglages permanents (remappage complet, profils) restent dans la GUI
|
||||||
|
// OpenLogi ; cette page couvre le quotidien : pile, DPI, SmartShift.
|
||||||
|
|
||||||
|
use cosmic::widget::{self, button, row, settings, text};
|
||||||
|
use cosmic::{Apply, Element, Task};
|
||||||
|
use cosmic_settings_page::{self as page, Section, section};
|
||||||
|
use openlogi_core::device::{BatteryInfo, DeviceKind};
|
||||||
|
use openlogi_core::hid::{
|
||||||
|
DeviceRoute, Dpi, DpiInfo, SmartShiftAutoDisengage, SmartShiftMode, SmartShiftStatus,
|
||||||
|
SmartShiftThreshold, WriteError,
|
||||||
|
};
|
||||||
|
use openlogi_ipc::{client, AgentClient, AgentSnapshot};
|
||||||
|
use slotmap::SlotMap;
|
||||||
|
use tarpc::context;
|
||||||
|
|
||||||
|
/// Clé d'identification stable d'un appareil dans les caches de la page.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub struct DeviceKey {
|
||||||
|
pub receiver: String,
|
||||||
|
pub slot: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ce que la page a réussi à lire pour un appareil donné.
|
||||||
|
/// `None` = non supporté (on n'affiche pas le réglage) ; `Some(Err)` = erreur.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct DeviceReads {
|
||||||
|
pub dpi: Option<Result<DpiInfo, String>>,
|
||||||
|
pub smartshift: Option<Result<SmartShiftStatus, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Un appareil prêt à afficher : inventaire + lectures.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DeviceEntry {
|
||||||
|
pub key: DeviceKey,
|
||||||
|
pub route: Option<DeviceRoute>,
|
||||||
|
pub slot: u8,
|
||||||
|
pub name: String,
|
||||||
|
pub online: bool,
|
||||||
|
pub battery: Option<BatteryInfo>,
|
||||||
|
pub pointer: bool,
|
||||||
|
pub reads: DeviceReads,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SnapshotData {
|
||||||
|
pub agent_version: String,
|
||||||
|
pub hook_installed: bool,
|
||||||
|
pub devices: Vec<DeviceEntry>,
|
||||||
|
pub offline_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Message {
|
||||||
|
/// Résultat d'un rafraîchissement complet (None = agent injoignable).
|
||||||
|
Snapshot(Option<SnapshotData>),
|
||||||
|
/// Écriture DPI envoyée à l'agent ; maj optimiste du cache.
|
||||||
|
SetDpi(DeviceKey, DeviceRoute, Dpi),
|
||||||
|
/// Écriture SmartShift ; maj optimiste du cache.
|
||||||
|
SetSmartShift(DeviceKey, DeviceRoute, bool),
|
||||||
|
/// Confirmation (ou échec) d'une écriture, par relecture.
|
||||||
|
WriteConfirmed(DeviceKey, DeviceReads),
|
||||||
|
/// Lancer la GUI OpenLogi complète.
|
||||||
|
OpenApp,
|
||||||
|
/// Démarrer l'agent s'il ne tourne pas.
|
||||||
|
StartAgent,
|
||||||
|
/// Rafraîchir.
|
||||||
|
Refresh,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deliver(message: Message) -> crate::app::Message {
|
||||||
|
crate::app::Message::PageMessage(crate::pages::Message::Logitech(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Page {
|
||||||
|
entity: page::Entity,
|
||||||
|
on_enter_handle: Option<cosmic::iced::task::Handle>,
|
||||||
|
/// `None` : pas encore de réponse de l'agent. `Some(None)` : agent absent.
|
||||||
|
pub snapshot: Option<Option<SnapshotData>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl page::Page<crate::pages::Message> for Page {
|
||||||
|
fn set_id(&mut self, entity: page::Entity) {
|
||||||
|
self.entity = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> Task<crate::pages::Message> {
|
||||||
|
self.start_refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_leave(&mut self) -> Task<crate::pages::Message> {
|
||||||
|
if let Some(handle) = self.on_enter_handle.take() {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(
|
||||||
|
&self,
|
||||||
|
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||||
|
) -> Option<page::Content> {
|
||||||
|
Some(vec![sections.insert(devices()), sections.insert(more())])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn info(&self) -> page::Info {
|
||||||
|
page::Info::new("logitech", "input-mouse-symbolic")
|
||||||
|
.title(fl!("logitech"))
|
||||||
|
.description(fl!("logitech-comment"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||||
|
|
||||||
|
impl Page {
|
||||||
|
pub fn update(&mut self, message: Message) -> Task<crate::app::Message> {
|
||||||
|
match message {
|
||||||
|
Message::Snapshot(data) => {
|
||||||
|
self.snapshot = Some(data);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SetDpi(key, route, dpi) => {
|
||||||
|
let write_route = route.clone();
|
||||||
|
Self::write(write_route, key, move |client| {
|
||||||
|
let dpi = dpi;
|
||||||
|
async move {
|
||||||
|
client
|
||||||
|
.set_dpi(context::current(), route.clone(), dpi)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SetSmartShift(key, route, enabled) => {
|
||||||
|
let previous = self
|
||||||
|
.snapshot
|
||||||
|
.as_ref()
|
||||||
|
.and_then(Option::as_ref)
|
||||||
|
.and_then(|s| s.devices.iter().find(|d| d.key == key))
|
||||||
|
.and_then(|d| d.reads.smartshift.clone())
|
||||||
|
.and_then(|r| r.ok());
|
||||||
|
|
||||||
|
// SmartShift activé : molette libre avec seuil de bascule.
|
||||||
|
// Désactivé : cliquet permanent (comme Options+).
|
||||||
|
let status = match (enabled, previous) {
|
||||||
|
(true, Some(previous)) => SmartShiftStatus {
|
||||||
|
mode: SmartShiftMode::Free,
|
||||||
|
..previous
|
||||||
|
},
|
||||||
|
(false, Some(previous)) => SmartShiftStatus {
|
||||||
|
mode: SmartShiftMode::Ratchet,
|
||||||
|
auto_disengage: SmartShiftAutoDisengage::Permanent,
|
||||||
|
..previous
|
||||||
|
},
|
||||||
|
(true, None) => SmartShiftStatus {
|
||||||
|
mode: SmartShiftMode::Free,
|
||||||
|
auto_disengage: SmartShiftAutoDisengage::Threshold(
|
||||||
|
SmartShiftThreshold::from_rounded(10.0),
|
||||||
|
),
|
||||||
|
tunable_torque: None,
|
||||||
|
},
|
||||||
|
(false, None) => SmartShiftStatus {
|
||||||
|
mode: SmartShiftMode::Ratchet,
|
||||||
|
auto_disengage: SmartShiftAutoDisengage::Permanent,
|
||||||
|
tunable_torque: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let write_route = route.clone();
|
||||||
|
Self::write(write_route, key, move |client| {
|
||||||
|
let status = status;
|
||||||
|
async move {
|
||||||
|
client
|
||||||
|
.set_smartshift(context::current(), route.clone(), status)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::WriteConfirmed(..) => Task::none(),
|
||||||
|
|
||||||
|
Message::OpenApp => {
|
||||||
|
if let Err(err) = std::process::Command::new("openlogi-desktop").spawn() {
|
||||||
|
tracing::warn!("failed to launch openlogi-desktop: {err}");
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::StartAgent => Task::future(async move {
|
||||||
|
_ = tokio::process::Command::new("systemctl")
|
||||||
|
.args(["--user", "start", "openlogi-agent.service"])
|
||||||
|
.status()
|
||||||
|
.await;
|
||||||
|
// Laisse le socket de l'agent s'ouvrir avant de réessayer.
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(800)).await;
|
||||||
|
deliver(Message::Refresh)
|
||||||
|
}),
|
||||||
|
|
||||||
|
Message::Refresh => {
|
||||||
|
self.start_refresh().map(crate::app::Message::PageMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rafraîchit l'inventaire : connexion à l'agent, snapshot, puis lectures
|
||||||
|
/// DPI/SmartShift de chaque appareil pointeur en ligne.
|
||||||
|
fn start_refresh(&mut self) -> Task<crate::pages::Message> {
|
||||||
|
if let Some(handle) = self.on_enter_handle.take() {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (task, handle) = Task::future(async move {
|
||||||
|
crate::pages::Message::Logitech(Message::Snapshot(fetch_snapshot().await))
|
||||||
|
})
|
||||||
|
.abortable();
|
||||||
|
|
||||||
|
self.on_enter_handle = Some(handle);
|
||||||
|
task
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Envoie une écriture à l'agent, puis relit l'appareil pour confirmer.
|
||||||
|
fn write<F, Fut>(route: DeviceRoute, key: DeviceKey, f: F) -> Task<crate::app::Message>
|
||||||
|
where
|
||||||
|
F: FnOnce(AgentClient) -> Fut + Send + 'static,
|
||||||
|
Fut: std::future::Future<
|
||||||
|
Output = Result<Result<(), WriteError>, tarpc::client::RpcError>,
|
||||||
|
> + Send,
|
||||||
|
{
|
||||||
|
let read_route = route.clone();
|
||||||
|
|
||||||
|
Task::future(async move {
|
||||||
|
if let Ok(connection) = client::connect().await {
|
||||||
|
let result = f(connection.client).await;
|
||||||
|
if !matches!(result, Ok(Ok(()))) {
|
||||||
|
tracing::warn!("openlogi write failed: {result:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relit l'appareil : la valeur affichée redevient la vérité.
|
||||||
|
let mut reads = DeviceReads::default();
|
||||||
|
if let Ok(connection) = client::connect().await {
|
||||||
|
reads.dpi = read_dpi(&connection.client, &read_route).await;
|
||||||
|
reads.smartshift = read_smartshift(&connection.client, &read_route).await;
|
||||||
|
}
|
||||||
|
deliver(Message::WriteConfirmed(key, reads))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Une erreur qui signale « l'appareil n'a pas cette fonctionnalité » :
|
||||||
|
/// le réglage correspondant ne doit simplement pas s'afficher.
|
||||||
|
fn is_unsupported(err: &WriteError) -> bool {
|
||||||
|
matches!(
|
||||||
|
err,
|
||||||
|
WriteError::FeatureUnsupported { .. } | WriteError::EmptyDpiList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_dpi(client: &AgentClient, route: &DeviceRoute) -> Option<Result<DpiInfo, String>> {
|
||||||
|
match client.read_dpi(context::current(), route.clone()).await {
|
||||||
|
Ok(Ok(info)) => Some(Ok(info)),
|
||||||
|
Ok(Err(err)) if is_unsupported(&err) => None,
|
||||||
|
Ok(Err(err)) => Some(Err(err.to_string())),
|
||||||
|
Err(err) => Some(Err(err.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_smartshift(
|
||||||
|
client: &AgentClient,
|
||||||
|
route: &DeviceRoute,
|
||||||
|
) -> Option<Result<SmartShiftStatus, String>> {
|
||||||
|
match client
|
||||||
|
.read_smartshift(context::current(), route.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(status)) => Some(Ok(status)),
|
||||||
|
Ok(Err(err)) if is_unsupported(&err) => None,
|
||||||
|
Ok(Err(err)) => Some(Err(err.to_string())),
|
||||||
|
Err(err) => Some(Err(err.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connexion + snapshot + lectures par appareil, en une passe.
|
||||||
|
async fn fetch_snapshot() -> Option<SnapshotData> {
|
||||||
|
let connection = client::connect().await.ok()?;
|
||||||
|
let AgentSnapshot {
|
||||||
|
status, inventory, ..
|
||||||
|
} = connection.client.snapshot(context::current()).await.ok()?;
|
||||||
|
|
||||||
|
let mut devices = Vec::new();
|
||||||
|
let mut offline_count = 0usize;
|
||||||
|
|
||||||
|
for inv in &inventory {
|
||||||
|
let receiver = inv
|
||||||
|
.receiver
|
||||||
|
.unique_id
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| inv.receiver.name.clone());
|
||||||
|
|
||||||
|
for paired in &inv.paired {
|
||||||
|
if !paired.online {
|
||||||
|
offline_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let route = DeviceRoute::device_route_for(inv, paired.slot);
|
||||||
|
let pointer = paired.kind == DeviceKind::Mouse;
|
||||||
|
|
||||||
|
// Ne parle qu'aux pointeurs en ligne, via leur route.
|
||||||
|
let reads = if paired.online && pointer {
|
||||||
|
match &route {
|
||||||
|
Some(route) => DeviceReads {
|
||||||
|
dpi: read_dpi(&connection.client, route).await,
|
||||||
|
smartshift: read_smartshift(&connection.client, route).await,
|
||||||
|
},
|
||||||
|
None => DeviceReads::default(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DeviceReads::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
devices.push(DeviceEntry {
|
||||||
|
key: DeviceKey {
|
||||||
|
receiver: receiver.clone(),
|
||||||
|
slot: paired.slot,
|
||||||
|
},
|
||||||
|
route,
|
||||||
|
slot: paired.slot,
|
||||||
|
name: paired.codename.clone().unwrap_or_else(|| {
|
||||||
|
fl!("logitech", "device-slot", slot = u32::from(paired.slot))
|
||||||
|
}),
|
||||||
|
online: paired.online,
|
||||||
|
battery: paired.battery.clone(),
|
||||||
|
pointer,
|
||||||
|
reads,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(SnapshotData {
|
||||||
|
agent_version: status.agent_version,
|
||||||
|
hook_installed: status.hook_installed,
|
||||||
|
devices,
|
||||||
|
offline_count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// La page ne se lie que si du matériel Logitech est présent (comme le
|
||||||
|
/// touchpad) : périphérique HID dont le vendor ID est 046d.
|
||||||
|
pub fn system_has_logitech() -> bool {
|
||||||
|
let Ok(mut enumerator) = udev::Enumerator::new() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let _res = enumerator.match_subsystem("hid");
|
||||||
|
|
||||||
|
let Ok(mut devices) = enumerator.scan_devices() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
devices.any(|device| {
|
||||||
|
device
|
||||||
|
.property_value("HID_ID")
|
||||||
|
.and_then(|value| value.to_str())
|
||||||
|
.is_some_and(|id| {
|
||||||
|
// HID_ID = "bustype:vendor:product" en hexadécimal.
|
||||||
|
id.split(':')
|
||||||
|
.nth(1)
|
||||||
|
.is_some_and(|vendor| vendor.eq_ignore_ascii_case("0000046D"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn devices() -> Section<crate::pages::Message> {
|
||||||
|
crate::slab!(descriptions {
|
||||||
|
dpi = fl!("logitech", "dpi");
|
||||||
|
smartshift = fl!("logitech", "smartshift");
|
||||||
|
smartshift_desc = fl!("logitech", "smartshift-desc");
|
||||||
|
battery = fl!("logitech", "battery");
|
||||||
|
offline = fl!("logitech", "offline");
|
||||||
|
online = fl!("logitech", "online");
|
||||||
|
no_agent = fl!("logitech", "no-agent");
|
||||||
|
no_agent_desc = fl!("logitech", "no-agent-desc");
|
||||||
|
start_agent = fl!("logitech", "start-agent");
|
||||||
|
refresh = fl!("logitech", "refresh");
|
||||||
|
no_devices = fl!("logitech", "no-devices");
|
||||||
|
});
|
||||||
|
|
||||||
|
Section::default()
|
||||||
|
.title(fl!("logitech"))
|
||||||
|
.descriptions(descriptions)
|
||||||
|
.view::<Page>(move |binder, _page, section| {
|
||||||
|
let descriptions = §ion.descriptions;
|
||||||
|
let logitech = binder.page::<Page>().expect("logitech page not found");
|
||||||
|
|
||||||
|
let mut list = settings::section().title(§ion.title);
|
||||||
|
|
||||||
|
match &logitech.snapshot {
|
||||||
|
// Pas encore de réponse : proposition de rafraîchissement.
|
||||||
|
None => {
|
||||||
|
list = list.add(settings::item(
|
||||||
|
"",
|
||||||
|
button::text(&descriptions[refresh])
|
||||||
|
.on_press(crate::pages::Message::Logitech(Message::Refresh)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// Agent injoignable : explication + remise en route.
|
||||||
|
Some(None) => {
|
||||||
|
let actions = row(vec![
|
||||||
|
button::text(&descriptions[start_agent])
|
||||||
|
.on_press(crate::pages::Message::Logitech(Message::StartAgent))
|
||||||
|
.into(),
|
||||||
|
button::text(fl!("logitech", "open-app"))
|
||||||
|
.on_press(crate::pages::Message::Logitech(Message::OpenApp))
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
|
.spacing(8);
|
||||||
|
|
||||||
|
list = list
|
||||||
|
.add(settings::item(
|
||||||
|
&descriptions[no_agent],
|
||||||
|
text::body(&descriptions[no_agent_desc]),
|
||||||
|
))
|
||||||
|
.add(settings::item("", actions));
|
||||||
|
}
|
||||||
|
Some(Some(snapshot)) => {
|
||||||
|
if snapshot.devices.is_empty() {
|
||||||
|
list = list
|
||||||
|
.add(settings::item(&descriptions[no_devices], text::body("")));
|
||||||
|
}
|
||||||
|
|
||||||
|
for device in &snapshot.devices {
|
||||||
|
let mut details = Vec::new();
|
||||||
|
if let Some(batt) = &device.battery {
|
||||||
|
details
|
||||||
|
.push(format!("{} · {} %", descriptions[battery], batt.percentage));
|
||||||
|
}
|
||||||
|
details.push(if device.online {
|
||||||
|
descriptions[online].to_string()
|
||||||
|
} else {
|
||||||
|
descriptions[offline].to_string()
|
||||||
|
});
|
||||||
|
let details = details.join(" · ");
|
||||||
|
|
||||||
|
let Some(route) = device.route.clone() else {
|
||||||
|
list = list
|
||||||
|
.add(settings::item(&device.name, text::body(details)));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let item;
|
||||||
|
|
||||||
|
if let Some(Ok(dpi_info)) = &device.reads.dpi {
|
||||||
|
let dpi_info = dpi_info.clone();
|
||||||
|
let values: Vec<String> = dpi_info
|
||||||
|
.capabilities
|
||||||
|
.values()
|
||||||
|
.iter()
|
||||||
|
.map(|dpi| dpi.to_string())
|
||||||
|
.collect();
|
||||||
|
let selected = dpi_info
|
||||||
|
.capabilities
|
||||||
|
.values()
|
||||||
|
.iter()
|
||||||
|
.position(|dpi| *dpi == dpi_info.current);
|
||||||
|
|
||||||
|
let key = device.key.clone();
|
||||||
|
let dpi_route = route.clone();
|
||||||
|
let dpi_control = widget::dropdown(values, selected, move |index| {
|
||||||
|
let Some(dpi) =
|
||||||
|
dpi_info.capabilities.values().get(index).copied()
|
||||||
|
else {
|
||||||
|
return crate::pages::Message::Logitech(Message::Refresh);
|
||||||
|
};
|
||||||
|
crate::pages::Message::Logitech(Message::SetDpi(
|
||||||
|
key.clone(),
|
||||||
|
dpi_route.clone(),
|
||||||
|
dpi,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
|
||||||
|
item = settings::item::builder(device.name.clone())
|
||||||
|
.description(details)
|
||||||
|
.control(dpi_control);
|
||||||
|
} else {
|
||||||
|
item = settings::item::builder(device.name.clone())
|
||||||
|
.description(details)
|
||||||
|
.control(text::body(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
list = list.add(item);
|
||||||
|
|
||||||
|
if let Some(Ok(status)) = &device.reads.smartshift {
|
||||||
|
let status = *status;
|
||||||
|
let enabled = status.mode == SmartShiftMode::Free;
|
||||||
|
let key = device.key.clone();
|
||||||
|
|
||||||
|
list = list.add(
|
||||||
|
settings::item::builder(&descriptions[smartshift])
|
||||||
|
.description(&descriptions[smartshift_desc])
|
||||||
|
.toggler(
|
||||||
|
enabled,
|
||||||
|
move |value| {
|
||||||
|
crate::pages::Message::Logitech(
|
||||||
|
Message::SetSmartShift(
|
||||||
|
key.clone(),
|
||||||
|
route.clone(),
|
||||||
|
value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.apply(Element::from)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn more() -> Section<crate::pages::Message> {
|
||||||
|
crate::slab!(descriptions {
|
||||||
|
open_app = fl!("logitech", "open-app");
|
||||||
|
open_app_desc = fl!("logitech", "open-app-desc");
|
||||||
|
});
|
||||||
|
|
||||||
|
Section::default()
|
||||||
|
.title(fl!("logitech", "more"))
|
||||||
|
.descriptions(descriptions)
|
||||||
|
.view::<Page>(move |_binder, _page, section| {
|
||||||
|
let descriptions = §ion.descriptions;
|
||||||
|
|
||||||
|
settings::section()
|
||||||
|
.title(§ion.title)
|
||||||
|
.add(
|
||||||
|
settings::item::builder(&descriptions[open_app])
|
||||||
|
.description(&descriptions[open_app_desc])
|
||||||
|
.control(button::text(&descriptions[open_app]).on_press(
|
||||||
|
crate::pages::Message::Logitech(Message::OpenApp),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.apply(Element::from)
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ use cosmic_settings_page as page;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
pub mod keyboard;
|
pub mod keyboard;
|
||||||
|
pub mod logitech;
|
||||||
pub mod mouse;
|
pub mod mouse;
|
||||||
pub mod touchpad;
|
pub mod touchpad;
|
||||||
|
|
||||||
|
|
@ -194,6 +195,12 @@ impl page::AutoBind<crate::pages::Message> for Page {
|
||||||
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
|
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
|
||||||
let insert = page.sub_page::<keyboard::Page>().sub_page::<mouse::Page>();
|
let insert = page.sub_page::<keyboard::Page>().sub_page::<mouse::Page>();
|
||||||
|
|
||||||
|
let insert = if logitech::system_has_logitech() {
|
||||||
|
insert.sub_page::<logitech::Page>()
|
||||||
|
} else {
|
||||||
|
insert
|
||||||
|
};
|
||||||
|
|
||||||
if system_has_touchpad() {
|
if system_has_touchpad() {
|
||||||
insert.sub_page::<touchpad::Page>()
|
insert.sub_page::<touchpad::Page>()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@ pub enum Message {
|
||||||
#[cfg(feature = "page-input")]
|
#[cfg(feature = "page-input")]
|
||||||
Keyboard(input::keyboard::Message),
|
Keyboard(input::keyboard::Message),
|
||||||
#[cfg(feature = "page-input")]
|
#[cfg(feature = "page-input")]
|
||||||
|
Logitech(input::logitech::Message),
|
||||||
|
#[cfg(feature = "page-input")]
|
||||||
KeyboardShortcuts(input::keyboard::shortcuts::Message),
|
KeyboardShortcuts(input::keyboard::shortcuts::Message),
|
||||||
#[cfg(feature = "page-legacy-applications")]
|
#[cfg(feature = "page-legacy-applications")]
|
||||||
LegacyApplications(applications::legacy_applications::Message),
|
LegacyApplications(applications::legacy_applications::Message),
|
||||||
|
|
|
||||||
20
i18n/en/cosmic_settings.ftl
vendored
20
i18n/en/cosmic_settings.ftl
vendored
|
|
@ -994,6 +994,26 @@ mouse = Mouse
|
||||||
.speed = Mouse speed
|
.speed = Mouse speed
|
||||||
.acceleration = Enable mouse acceleration
|
.acceleration = Enable mouse acceleration
|
||||||
|
|
||||||
|
## Input: Logitech (OpenLogi)
|
||||||
|
|
||||||
|
logitech = Logitech
|
||||||
|
.device-slot = Logitech device {$slot}
|
||||||
|
.battery = Battery
|
||||||
|
.online = Online
|
||||||
|
.offline = Offline
|
||||||
|
.dpi = Sensitivity (DPI)
|
||||||
|
.smartshift = SmartShift wheel
|
||||||
|
.smartshift-desc = Let the wheel switch freely to fast scrolling when spun
|
||||||
|
.no-agent = The OpenLogi agent is not running
|
||||||
|
.no-agent-desc = Start the agent to configure paired Logitech devices from here
|
||||||
|
.start-agent = Start the agent
|
||||||
|
.open-app = Open OpenLogi
|
||||||
|
.open-app-desc = Button remapping, profiles and advanced settings live in the OpenLogi app
|
||||||
|
.refresh = Refresh
|
||||||
|
.no-devices = No paired Logitech device was found
|
||||||
|
.more = More settings
|
||||||
|
logitech-comment = Logitech mice and keyboards, through OpenLogi
|
||||||
|
|
||||||
## Input: Touchpad
|
## Input: Touchpad
|
||||||
|
|
||||||
click-behavior = Click Behavior
|
click-behavior = Click Behavior
|
||||||
|
|
|
||||||
20
i18n/fr/cosmic_settings.ftl
vendored
20
i18n/fr/cosmic_settings.ftl
vendored
|
|
@ -766,6 +766,26 @@ mouse = Souris
|
||||||
.speed = Vitesse de la souris
|
.speed = Vitesse de la souris
|
||||||
.acceleration = Activer l'accélération de la souris
|
.acceleration = Activer l'accélération de la souris
|
||||||
|
|
||||||
|
## Input: Logitech (OpenLogi)
|
||||||
|
|
||||||
|
logitech = Logitech
|
||||||
|
.device-slot = Appareil Logitech {$slot}
|
||||||
|
.battery = Pile
|
||||||
|
.online = En ligne
|
||||||
|
.offline = Hors ligne
|
||||||
|
.dpi = Sensibilité (DPI)
|
||||||
|
.smartshift = Molette SmartShift
|
||||||
|
.smartshift-desc = La molette passe en défilement libre quand elle est lancée vite
|
||||||
|
.no-agent = L'agent OpenLogi n'est pas lancé
|
||||||
|
.no-agent-desc = Démarrez l'agent pour configurer les appareils Logitech appairés depuis ici
|
||||||
|
.start-agent = Démarrer l'agent
|
||||||
|
.open-app = Ouvrir OpenLogi
|
||||||
|
.open-app-desc = Remappage des boutons, profils et réglages avancés dans l'application OpenLogi
|
||||||
|
.refresh = Rafraîchir
|
||||||
|
.no-devices = Aucun appareil Logitech appairé trouvé
|
||||||
|
.more = Plus de réglages
|
||||||
|
logitech-comment = Souris et claviers Logitech, via OpenLogi
|
||||||
|
|
||||||
## Input: Touchpad
|
## Input: Touchpad
|
||||||
|
|
||||||
click-behavior = Comportement de clic
|
click-behavior = Comportement de clic
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue