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 }
|
||||
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 }
|
||||
# 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"
|
||||
nmrs = { version = "3.5.2", optional = true }
|
||||
regex = "1.13.1"
|
||||
|
|
@ -68,7 +72,7 @@ slotmap = "1.1.1"
|
|||
static_init = "1.0.4"
|
||||
sunrise = "3.0.0"
|
||||
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-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
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")]
|
||||
crate::pages::Message::KeyboardShortcuts(message) => {
|
||||
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;
|
||||
|
||||
pub mod keyboard;
|
||||
pub mod logitech;
|
||||
pub mod mouse;
|
||||
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> {
|
||||
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() {
|
||||
insert.sub_page::<touchpad::Page>()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ pub enum Message {
|
|||
#[cfg(feature = "page-input")]
|
||||
Keyboard(input::keyboard::Message),
|
||||
#[cfg(feature = "page-input")]
|
||||
Logitech(input::logitech::Message),
|
||||
#[cfg(feature = "page-input")]
|
||||
KeyboardShortcuts(input::keyboard::shortcuts::Message),
|
||||
#[cfg(feature = "page-legacy-applications")]
|
||||
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
|
||||
.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
|
||||
|
||||
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
|
||||
.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
|
||||
|
||||
click-behavior = Comportement de clic
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue