feat: implement support for the infobar display on the Stream Deck Neo (#253)

Co-authored-by: nekename <63245705+nekename@users.noreply.github.com>
This commit is contained in:
evilbugandnekename authored and GitHub committed 2026-06-22 12:15:27 +01:00
1 parent b9e47358a7
commit 5719915bf7
12 files changed
+100 -16

No files matched your search

+19
View File
@@ -45,6 +45,13 @@ pub async fn update_image(context: &crate::shared::Context, image: Option<&str>)
&ImageRect::from_image_async(image::load_from_memory(&bytes)?.resize(72, 72, image::imageops::FilterType::Nearest))?,
)
.await?;
} else if context.controller == "Infobar" {
let img = image::load_from_memory(&bytes)?;
let Some(format) = device.kind().lcd_image_format() else {
return Err(anyhow::anyhow!("Failed to get LCD image format"));
};
let data = convert_image_with_format_async(format, img.resize_exact(248, 58, image::imageops::FilterType::Lanczos3))?;
device.write_lcd_fill(&data).await?;
} else if is_touch_point {
let (r, g, b) = extract_average_colour(&image::load_from_memory(&bytes)?);
device.set_touchpoint_color(context.position - key_count, r, g, b).await?;
@@ -55,6 +62,12 @@ pub async fn update_image(context: &crate::shared::Context, image: Option<&str>)
device
.write_lcd(context.position as u16 * 200, 0, &ImageRect::from_image_async(image::DynamicImage::new_rgb8(200, 100))?)
.await?;
} else if context.controller == "Infobar" {
let Some(format) = device.kind().lcd_image_format() else {
return Err(anyhow::anyhow!("Failed to get LCD image format"));
};
let data = convert_image_with_format_async(format, image::DynamicImage::new_rgb8(248, 58))?;
device.write_lcd_fill(&data).await?;
} else if is_touch_point {
device.set_touchpoint_color(context.position - key_count, 0, 0, 0).await?;
} else {
@@ -79,6 +92,10 @@ pub async fn clear_screen(id: &str) -> Result<(), anyhow::Error> {
device
.write_lcd_fill(&convert_image_with_format_async(device.kind().lcd_image_format().unwrap(), image::DynamicImage::new_rgb8(800, 100))?)
.await?;
} else if device.kind() == Kind::Neo {
device
.write_lcd_fill(&convert_image_with_format_async(device.kind().lcd_image_format().unwrap(), image::DynamicImage::new_rgb8(248, 58))?)
.await?;
}
clear_all_touchpoints(device).await;
device.flush().await?;
@@ -122,6 +139,7 @@ async fn init(device: AsyncStreamDeck, device_id: String) {
let reader = device.get_reader();
ELGATO_DEVICES.write().await.insert(device_id.clone(), device);
let _ = clear_screen(&device_id).await;
crate::events::inbound::devices::register_device(
"",
@@ -134,6 +152,7 @@ async fn init(device: AsyncStreamDeck, device_id: String) {
columns: kind.column_count(),
encoders: kind.encoder_count(),
touchpoints: kind.touchpoint_count(),
infobars: if kind == Kind::Neo { 1 } else { 0 },
r#type: device_type,
},
},
+14 -2
View File
@@ -52,7 +52,13 @@ pub async fn set_selected_profile(device: String, id: String) -> Result<(), Erro
if selected_profile != id {
let old_profile = &locks.profile_stores.get_profile_store(&DEVICES.get(&device).unwrap(), &selected_profile)?.value;
for instance in old_profile.keys.iter().flatten().chain(&mut old_profile.sliders.iter().flatten()) {
for instance in old_profile
.keys
.iter()
.flatten()
.chain(&mut old_profile.sliders.iter().flatten())
.chain(&mut old_profile.infobars.iter().flatten())
{
if !matches!(instance.action.uuid.as_str(), "opendeck.multiaction" | "opendeck.toggleaction") {
let _ = crate::events::outbound::will_appear::will_disappear(instance, false).await;
} else {
@@ -67,7 +73,13 @@ pub async fn set_selected_profile(device: String, id: String) -> Result<(), Erro
// We must use the mutable version of get_profile_store in order to create the store if it does not exist.
let store = locks.profile_stores.get_profile_store_mut(&DEVICES.get(&device).unwrap(), &id).await?;
let new_profile = &store.value;
for instance in new_profile.keys.iter().flatten().chain(&mut new_profile.sliders.iter().flatten()) {
for instance in new_profile
.keys
.iter()
.flatten()
.chain(&mut new_profile.sliders.iter().flatten())
.chain(&mut new_profile.infobars.iter().flatten())
{
if !matches!(instance.action.uuid.as_str(), "opendeck.multiaction" | "opendeck.toggleaction") {
let _ = crate::events::outbound::will_appear::will_appear(instance).await;
} else {
+16 -2
View File
@@ -27,7 +27,14 @@ pub async fn register_device(uuid: &str, mut event: PayloadEvent<crate::shared::
let mut locks = crate::store::profiles::acquire_locks_mut().await;
let selected_profile = locks.device_stores.get_selected_profile(&event.payload.id)?;
let profile = locks.profile_stores.get_profile_store(&DEVICES.get(&event.payload.id).unwrap(), &selected_profile)?;
for instance in profile.value.keys.iter().flatten().chain(profile.value.sliders.iter().flatten()) {
for instance in profile
.value
.keys
.iter()
.flatten()
.chain(profile.value.sliders.iter().flatten())
.chain(profile.value.infobars.iter().flatten())
{
let _ = crate::events::outbound::will_appear::will_appear(instance).await;
}
@@ -53,7 +60,14 @@ pub async fn deregister_device(uuid: &str, event: PayloadEvent<String>) -> Resul
let selected_profile = locks.device_stores.get_selected_profile(&event.payload)?;
let profile = locks.profile_stores.get_profile_store(&DEVICES.get(&event.payload).unwrap(), &selected_profile)?;
for instance in profile.value.keys.iter().flatten().chain(profile.value.sliders.iter().flatten()) {
for instance in profile
.value
.keys
.iter()
.flatten()
.chain(profile.value.sliders.iter().flatten())
.chain(profile.value.infobars.iter().flatten())
{
let _ = crate::events::outbound::will_appear::will_disappear(instance, false).await;
}
+4
View File
@@ -38,6 +38,8 @@ pub struct DeviceInfo {
pub encoders: u8,
#[serde_inline_default(0)]
pub touchpoints: u8,
#[serde_inline_default(0)]
pub infobars: u8,
pub r#type: u8,
}
@@ -312,6 +314,8 @@ pub struct Profile {
pub id: String,
pub keys: Vec<Option<ActionInstance>>,
pub sliders: Vec<Option<ActionInstance>>,
#[serde(default)]
pub infobars: Vec<Option<ActionInstance>>,
}
/// A map of category names to a list of actions in that category.
+6 -2
View File
@@ -39,11 +39,13 @@ impl ProfileStores {
id: id.to_owned(),
keys: Vec::new(),
sliders: Vec::new(),
infobars: Vec::new(),
};
let mut store = Store::new(&canonical_id, &config_dir().join("profiles"), default).context(format!("Failed to create store for profile {}", canonical_id))?;
store.value.keys.resize((device.rows * device.columns + device.touchpoints) as usize, None);
store.value.sliders.resize(device.encoders as usize, None);
store.value.infobars.resize(device.infobars as usize, None);
let categories = crate::shared::CATEGORIES.read().await;
let actions = categories.values().flat_map(|v| v.actions.iter()).collect::<Vec<_>>();
@@ -53,7 +55,7 @@ impl ProfileStores {
instance.action.plugin == "opendeck"
|| (plugins_dir.join(&instance.action.plugin).exists() && (!registered.contains(&instance.action.plugin) || actions.iter().any(|v| v.uuid == instance.action.uuid)))
};
for slot in store.value.keys.iter_mut().chain(store.value.sliders.iter_mut()) {
for slot in store.value.keys.iter_mut().chain(store.value.sliders.iter_mut()).chain(store.value.infobars.iter_mut()) {
if let Some(instance) = slot {
if !keep_instance(instance) {
*slot = None;
@@ -157,7 +159,7 @@ impl ProfileStores {
pub fn all_from_plugin(&self, plugin: &str) -> Vec<crate::shared::ActionContext> {
let mut all = vec![];
for store in self.stores.values() {
for instance in store.value.keys.iter().chain(&store.value.sliders).flatten() {
for instance in store.value.keys.iter().chain(&store.value.sliders).chain(&store.value.infobars).flatten() {
if instance.action.plugin == plugin {
all.push(instance.context.clone());
} else if let Some(children) = &instance.children {
@@ -301,6 +303,7 @@ pub async fn get_slot<'a>(context: &crate::shared::Context, locks: &'a Locks<'_>
let configured = match &context.controller[..] {
"Encoder" => store.value.sliders.get(context.position as usize).ok_or_else(|| anyhow!("index out of bounds"))?,
"Infobar" => store.value.infobars.get(context.position as usize).ok_or_else(|| anyhow!("index out of bounds"))?,
_ => store.value.keys.get(context.position as usize).ok_or_else(|| anyhow!("index out of bounds"))?,
};
@@ -313,6 +316,7 @@ pub async fn get_slot_mut<'a>(context: &crate::shared::Context, locks: &'a mut L
let configured = match &context.controller[..] {
"Encoder" => store.value.sliders.get_mut(context.position as usize).ok_or_else(|| anyhow!("index out of bounds"))?,
"Infobar" => store.value.infobars.get_mut(context.position as usize).ok_or_else(|| anyhow!("index out of bounds"))?,
_ => store.value.keys.get_mut(context.position as usize).ok_or_else(|| anyhow!("index out of bounds"))?,
};
@@ -190,6 +190,8 @@ impl DiskActionInstance {
pub struct DiskProfile {
pub keys: Vec<Option<DiskActionInstance>>,
pub sliders: Vec<Option<DiskActionInstance>>,
#[serde(default)]
pub infobars: Vec<Option<DiskActionInstance>>,
}
impl From<&Profile> for DiskProfile {
@@ -197,6 +199,7 @@ impl From<&Profile> for DiskProfile {
Self {
keys: value.keys.clone().into_iter().map(|x| x.map(|v| v.into())).collect(),
sliders: value.sliders.clone().into_iter().map(|x| x.map(|v| v.into())).collect(),
infobars: value.infobars.clone().into_iter().map(|x| x.map(|v| v.into())).collect(),
}
}
}
@@ -212,6 +215,7 @@ impl DiskProfile {
id,
keys: self.keys.into_iter().map(|x| x.map(|v| v.into_action_instance(path))).collect(),
sliders: self.sliders.into_iter().map(|x| x.map(|v| v.into_action_instance(path))).collect(),
infobars: self.infobars.into_iter().map(|x| x.map(|v| v.into_action_instance(path))).collect(),
}
}
}