diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4dbb880 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Tauri Development Debug", + "cargo": { + "args": [ + "build", + "--manifest-path=./src-tauri/Cargo.toml", + "--no-default-features" + ] + }, + // task for the `beforeDevCommand` if used, must be configured in `.vscode/tasks.json` + "preLaunchTask": "ui:dev" + }, + { + "type": "lldb", + "request": "launch", + "name": "Tauri Production Debug", + "cargo": { + "args": [ + "build", + "--release", + "--manifest-path=./src-tauri/Cargo.toml" + ] + }, + // task for the `beforeBuildCommand` if used, must be configured in `.vscode/tasks.json` + "preLaunchTask": "ui:build" + } + ] \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..8a8bdcf --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "ui:dev", + "type": "shell", + // `dev` keeps running in the background + // ideally you should also configure a `problemMatcher` + // see https://code.visualstudio.com/docs/editor/tasks#_can-a-background-task-be-used-as-a-prelaunchtask-in-launchjson + "isBackground": true, + // change this to your `beforeDevCommand`: + "command": "yarn", + "args": [ + "dev" + ] + }, + { + "label": "ui:build", + "type": "shell", + // change this to your `beforeBuildCommand`: + "command": "yarn", + "args": [ + "build" + ] + } + ] +} \ No newline at end of file diff --git a/src-tauri/src/ambient_light/config.rs b/src-tauri/src/ambient_light/config.rs index 62fe4a1..82d9b92 100644 --- a/src-tauri/src/ambient_light/config.rs +++ b/src-tauri/src/ambient_light/config.rs @@ -3,8 +3,9 @@ use std::env::current_dir; use paris::{error, info}; use serde::{Deserialize, Serialize}; use tauri::api::path::config_dir; +use tokio::sync::OnceCell; -#[derive(Clone, Copy, Serialize, Deserialize, Debug)] +#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq)] pub enum Border { Top, Bottom, @@ -29,7 +30,20 @@ pub struct LedStripConfig { pub len: usize, } +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct LedStripConfigGroup { + pub items: Vec, +} + + impl LedStripConfig { + pub async fn global() -> &'static Vec { + static LED_STRIP_CONFIGS_GLOBAL: OnceCell> = OnceCell::const_new(); + LED_STRIP_CONFIGS_GLOBAL + .get_or_init(|| async { Self::read_config().await.unwrap() }) + .await + } + pub async fn read_config() -> anyhow::Result> { // config path let path = config_dir() @@ -43,10 +57,10 @@ impl LedStripConfig { if exists { let config = tokio::fs::read_to_string(path).await?; - let config: Vec = toml::from_str(&config) + let config: LedStripConfigGroup = toml::from_str(&config) .map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?; - Ok(config) + Ok(config.items) } else { info!("config file not exist, fallback to default config"); Ok(Self::get_default_config().await?) @@ -58,12 +72,15 @@ impl LedStripConfig { .unwrap_or(current_dir().unwrap()) .join("led_strip_config.toml"); - let config = toml::to_string(configs) - .map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?; + let configs = LedStripConfigGroup { items: configs.clone() }; - tokio::fs::write(path, config) - .await - .map_err(|e| anyhow::anyhow!("Failed to write config file: {}", e))?; + let config = toml::to_string(&configs).map_err(|e| { + anyhow::anyhow!("Failed to parse config file: {}. configs: {:?}", e, configs) + })?; + + tokio::fs::write(&path, config).await.map_err(|e| { + anyhow::anyhow!("Failed to write config file: {}. path: {:?}", e, &path) + })?; Ok(()) } @@ -99,13 +116,10 @@ impl LedStripConfig { } #[derive(Clone, Copy, Serialize, Deserialize, Debug)] -pub struct DisplayConfig { +pub struct LedStripConfigOfDisplays { pub id: u32, pub index_of_display: usize, - pub display_width: usize, - pub display_height: usize, pub led_strip_of_borders: LedStripConfigOfBorders, - pub scale_factor: f32, } impl LedStripConfigOfBorders { @@ -119,21 +133,98 @@ impl LedStripConfigOfBorders { } } -impl DisplayConfig { - pub fn default( - id: u32, - index_of_display: usize, - display_width: usize, - display_height: usize, - scale_factor: f32, - ) -> Self { +impl LedStripConfigOfDisplays { + pub fn default(id: u32, index_of_display: usize) -> Self { Self { id, index_of_display, - display_width, - display_height, led_strip_of_borders: LedStripConfigOfBorders::default(), - scale_factor, } } + + pub async fn read_from_disk() -> anyhow::Result { + let path = config_dir() + .unwrap_or(current_dir().unwrap()) + .join("led_strip_config_of_displays.toml"); + + let exists = tokio::fs::try_exists(path.clone()) + .await + .map_err(|e| anyhow::anyhow!("Failed to check config file exists: {}", e))?; + + if exists { + let config = tokio::fs::read_to_string(path).await?; + + let config: Self = toml::from_str(&config) + .map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?; + + Ok(config) + } else { + info!("config file not exist, fallback to default config"); + Ok(Self::get_default_config().await?) + } + } + + pub async fn write_to_disk(&self) -> anyhow::Result<()> { + let path = config_dir() + .unwrap_or(current_dir().unwrap()) + .join("led_strip_config_of_displays.toml"); + + let config = toml::to_string(self).map_err(|e| { + anyhow::anyhow!("Failed to parse config file: {}. config: {:?}", e, self) + })?; + + tokio::fs::write(&path, config).await.map_err(|e| { + anyhow::anyhow!("Failed to write config file: {}. path: {:?}", e, &path) + })?; + + Ok(()) + } + + pub async fn get_default_config() -> anyhow::Result { + let displays = display_info::DisplayInfo::all().map_err(|e| { + error!("can not list display info: {}", e); + anyhow::anyhow!("can not list display info: {}", e) + })?; + + let mut configs = Vec::new(); + for (i, display) in displays.iter().enumerate() { + let config = Self { + id: display.id, + index_of_display: i, + led_strip_of_borders: LedStripConfigOfBorders { + top: Some(LedStripConfig { + index: i * 4 * 30, + display_id: display.id, + border: Border::Top, + start_pos: i * 4 * 30, + len: 30, + }), + bottom: Some(LedStripConfig { + index: i * 4 * 30 + 30, + display_id: display.id, + border: Border::Bottom, + start_pos: i * 4 * 30 + 30, + len: 30, + }), + left: Some(LedStripConfig { + index: i * 4 * 30 + 60, + display_id: display.id, + border: Border::Left, + start_pos: i * 4 * 30 + 60, + len: 30, + }), + right: Some(LedStripConfig { + index: i * 4 * 30 + 90, + display_id: display.id, + border: Border::Right, + start_pos: i * 4 * 30 + 90, + len: 30, + }), + } + }; + configs.push(config); + } + + Ok(configs[0]) + } } diff --git a/src-tauri/src/ambient_light/config_manager.rs b/src-tauri/src/ambient_light/config_manager.rs new file mode 100644 index 0000000..3b92283 --- /dev/null +++ b/src-tauri/src/ambient_light/config_manager.rs @@ -0,0 +1,88 @@ +use std::{sync::Arc, borrow::Borrow}; + +use tauri::async_runtime::RwLock; +use tokio::sync::OnceCell; + +use crate::ambient_light::{config, LedStripConfig}; + +use super::Border; + +pub struct ConfigManager { + configs: Arc>>, + config_update_receiver: tokio::sync::watch::Receiver>, + config_update_sender: tokio::sync::watch::Sender>, +} + +impl ConfigManager { + pub async fn global() -> &'static Self { + static CONFIG_MANAGER_GLOBAL: OnceCell = OnceCell::const_new(); + CONFIG_MANAGER_GLOBAL + .get_or_init(|| async { + let configs = LedStripConfig::read_config().await.unwrap(); + let (config_update_sender, config_update_receiver) = + tokio::sync::watch::channel(configs.clone()); + ConfigManager { + configs: Arc::new(RwLock::new(configs)), + config_update_receiver, + config_update_sender, + } + }) + .await + } + + pub async fn reload(&self) -> anyhow::Result<()> { + let mut configs = self.configs.write().await; + *configs = LedStripConfig::read_config().await?; + + Ok(()) + } + + pub async fn update(&self, configs: &Vec) -> anyhow::Result<()> { + LedStripConfig::write_config(configs).await?; + self.reload().await?; + + self.config_update_sender.send(configs.clone()).map_err(|e| { + anyhow::anyhow!("Failed to send config update: {}", e) + })?; + + log::info!("config updated: {:?}", configs); + + Ok(()) + } + + pub async fn configs(&self) -> Vec { + self.configs.read().await.clone() + } + + pub async fn patch_led_strip_len(&self, display_id: u32, border: Border, delta_len: i8) -> anyhow::Result<()> { + let mut configs = self.configs.write().await; + + for config in configs.iter_mut() { + if config.display_id == display_id && config.border == border { + let target = config.len as i64 + delta_len as i64; + if target < 0 || target > 1000 { + return Err(anyhow::anyhow!("Overflow. range: 0-1000, current: {}", target)); + } + config.len = target as usize; + } + } + + let cloned_config = configs.clone(); + + drop(configs); + + self.update(&cloned_config).await?; + + self.config_update_sender + .send(cloned_config) + .map_err(|e| anyhow::anyhow!("Failed to send config update: {}", e))?; + + Ok(()) + } + + pub fn clone_config_update_receiver( + &self, + ) -> tokio::sync::watch::Receiver> { + self.config_update_receiver.clone() + } +} diff --git a/src-tauri/src/ambient_light/mod.rs b/src-tauri/src/ambient_light/mod.rs index aa4926f..62abcb2 100644 --- a/src-tauri/src/ambient_light/mod.rs +++ b/src-tauri/src/ambient_light/mod.rs @@ -1,3 +1,5 @@ mod config; +mod config_manager; pub use config::*; +pub use config_manager::*; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fc3dee4..83220d1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -7,7 +7,7 @@ mod led_color; pub mod screenshot; mod screenshot_manager; -use ambient_light::LedStripConfig; +use ambient_light::{Border, LedStripConfig}; use core_graphics::display::{ kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay, }; @@ -17,7 +17,7 @@ use screenshot::Screenshot; use screenshot_manager::ScreenshotManager; use serde::{Deserialize, Serialize}; use serde_json::to_string; -use tauri::{http::ResponseBuilder, regex}; +use tauri::{http::ResponseBuilder, regex, Manager}; #[derive(Serialize, Deserialize)] #[serde(remote = "DisplayInfo")] @@ -105,8 +105,7 @@ async fn get_led_strips_sample_points( let screenshot = rx.borrow().clone(); let width = screenshot.width; let height = screenshot.height; - let sample_points = - Screenshot::get_sample_point(&config, width as usize, height as usize); + let sample_points = Screenshot::get_sample_point(&config, width as usize, height as usize); Ok(sample_points) } else { return Err(format!("display not found: {}", config.display_id)); @@ -132,6 +131,22 @@ async fn get_one_edge_colors( } } +#[tauri::command] +async fn patch_led_strip_len(display_id: u32, border: Border, delta_len: i8) -> Result<(), String> { + info!("patch_led_strip_len: {} {:?} {}", display_id, border, delta_len); + let config_manager = ambient_light::ConfigManager::global().await; + config_manager + .patch_led_strip_len(display_id, border, delta_len) + .await + .map_err(|e| { + error!("can not patch led strip len: {}", e); + e.to_string() + })?; + + info!("patch_led_strip_len: ok"); + Ok(()) +} + #[tokio::main] async fn main() { env_logger::init(); @@ -146,7 +161,8 @@ async fn main() { read_led_strip_configs, write_led_strip_configs, get_led_strips_sample_points, - get_one_edge_colors + get_one_edge_colors, + patch_led_strip_len ]) .register_uri_scheme_protocol("ambient-light", move |_app, request| { let response = ResponseBuilder::new().header("Access-Control-Allow-Origin", "*"); @@ -275,6 +291,28 @@ async fn main() { .status(500) .body(err.to_string().into_bytes()); }) + .setup(move |app| { + let app_handle = app.handle().clone(); + tokio::spawn(async move { + let config_manager = ambient_light::ConfigManager::global().await; + let config_update_receiver = config_manager.clone_config_update_receiver(); + let mut config_update_receiver = config_update_receiver; + loop { + if let Err(err) = config_update_receiver.changed().await { + error!("config update receiver changed error: {}", err); + return; + } + + log::info!("config changed. emit config_changed event."); + + let config = config_update_receiver.borrow().clone(); + + app_handle.emit_all("config_changed", config).unwrap(); + } + }); + + Ok(()) + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/screenshot.rs b/src-tauri/src/screenshot.rs index 78ee7cf..6b9ee5b 100644 --- a/src-tauri/src/screenshot.rs +++ b/src-tauri/src/screenshot.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use tauri::async_runtime::RwLock; use crate::{ - ambient_light::{DisplayConfig, LedStripConfig}, + ambient_light::{LedStripConfigOfDisplays, LedStripConfig}, led_color::LedColor, }; @@ -80,92 +80,92 @@ impl Screenshot { } } - fn get_sample_points(config: DisplayConfig) -> ScreenSamplePoints { - let top = match config.led_strip_of_borders.top { - Some(led_strip_config) => Self::get_one_edge_sample_points( - config.display_height / 8, - config.display_width, - led_strip_config.len, - 1, - ), - None => { - vec![] - } - }; + // fn get_sample_points(config: DisplayConfig) -> ScreenSamplePoints { + // let top = match config.led_strip_of_borders.top { + // Some(led_strip_config) => Self::get_one_edge_sample_points( + // config.display_height / 8, + // config.display_width, + // led_strip_config.len, + // 1, + // ), + // None => { + // vec![] + // } + // }; - let bottom: Vec = match config.led_strip_of_borders.bottom { - Some(led_strip_config) => { - let points = Self::get_one_edge_sample_points( - config.display_height / 9, - config.display_width, - led_strip_config.len, - 5, - ); - points - .into_iter() - .map(|groups| -> Vec { - groups - .into_iter() - .map(|(x, y)| (x, config.display_height - y)) - .collect() - }) - .collect() - } - None => { - vec![] - } - }; + // let bottom: Vec = match config.led_strip_of_borders.bottom { + // Some(led_strip_config) => { + // let points = Self::get_one_edge_sample_points( + // config.display_height / 9, + // config.display_width, + // led_strip_config.len, + // 5, + // ); + // points + // .into_iter() + // .map(|groups| -> Vec { + // groups + // .into_iter() + // .map(|(x, y)| (x, config.display_height - y)) + // .collect() + // }) + // .collect() + // } + // None => { + // vec![] + // } + // }; - let left: Vec = match config.led_strip_of_borders.left { - Some(led_strip_config) => { - let points = Self::get_one_edge_sample_points( - config.display_width / 16, - config.display_height, - led_strip_config.len, - 5, - ); - points - .into_iter() - .map(|groups| -> Vec { - groups.into_iter().map(|(x, y)| (y, x)).collect() - }) - .collect() - } - None => { - vec![] - } - }; + // let left: Vec = match config.led_strip_of_borders.left { + // Some(led_strip_config) => { + // let points = Self::get_one_edge_sample_points( + // config.display_width / 16, + // config.display_height, + // led_strip_config.len, + // 5, + // ); + // points + // .into_iter() + // .map(|groups| -> Vec { + // groups.into_iter().map(|(x, y)| (y, x)).collect() + // }) + // .collect() + // } + // None => { + // vec![] + // } + // }; - let right: Vec = match config.led_strip_of_borders.right { - Some(led_strip_config) => { - let points = Self::get_one_edge_sample_points( - config.display_width / 16, - config.display_height, - led_strip_config.len, - 5, - ); - points - .into_iter() - .map(|groups| -> Vec { - groups - .into_iter() - .map(|(x, y)| (config.display_width - y, x)) - .collect() - }) - .collect() - } - None => { - vec![] - } - }; + // let right: Vec = match config.led_strip_of_borders.right { + // Some(led_strip_config) => { + // let points = Self::get_one_edge_sample_points( + // config.display_width / 16, + // config.display_height, + // led_strip_config.len, + // 5, + // ); + // points + // .into_iter() + // .map(|groups| -> Vec { + // groups + // .into_iter() + // .map(|(x, y)| (config.display_width - y, x)) + // .collect() + // }) + // .collect() + // } + // None => { + // vec![] + // } + // }; - ScreenSamplePoints { - top, - bottom, - left, - right, - } - } + // ScreenSamplePoints { + // top, + // bottom, + // left, + // right, + // } + // } fn get_one_edge_sample_points( width: usize, diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..84f5661 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/App.tsx b/src/App.tsx index d4ce739..3df9a6d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,11 @@ -import { createEffect } from 'solid-js'; -import { convertFileSrc, invoke } from '@tauri-apps/api/tauri'; +import { createEffect, onCleanup } from 'solid-js'; +import { invoke } from '@tauri-apps/api/tauri'; import { DisplayView } from './components/display-view'; import { DisplayListContainer } from './components/display-list-container'; import { displayStore, setDisplayStore } from './stores/display.store'; -import { path } from '@tauri-apps/api'; import { LedStripConfig } from './models/led-strip-config'; import { setLedStripStore } from './stores/led-strip.store'; +import { listen } from '@tauri-apps/api/event'; function App() { createEffect(() => { @@ -15,14 +15,26 @@ function App() { }); }); invoke('read_led_strip_configs').then((strips) => { - console.log(strips); - setLedStripStore({ strips, }); }); }); + // register tauri event listeners + createEffect(() => { + const unlisten = listen('config_changed', (event) => { + const strips = event.payload as LedStripConfig[]; + setLedStripStore({ + strips, + }); + }); + + onCleanup(() => { + unlisten.then((unlisten) => unlisten()); + }); + }); + return (
diff --git a/src/assets/transparent-grid-background.svg b/src/assets/transparent-grid-background.svg new file mode 100644 index 0000000..1360888 --- /dev/null +++ b/src/assets/transparent-grid-background.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/display-info-panel.tsx b/src/components/display-info-panel.tsx index 602271e..9c84850 100644 --- a/src/components/display-info-panel.tsx +++ b/src/components/display-info-panel.tsx @@ -7,7 +7,7 @@ type DisplayInfoItemProps = { export const DisplayInfoItem: ParentComponent = (props) => { return ( -
+
{props.label}
{props.children}
diff --git a/src/components/display-list-container.tsx b/src/components/display-list-container.tsx index 517db2b..81e2c4c 100644 --- a/src/components/display-list-container.tsx +++ b/src/components/display-list-container.tsx @@ -1,11 +1,13 @@ import { createEffect, createSignal, + JSX, onCleanup, onMount, ParentComponent, } from 'solid-js'; import { displayStore, setDisplayStore } from '../stores/display.store'; +import background from '../assets/transparent-grid-background.svg?url'; export const DisplayListContainer: ParentComponent = (props) => { let root: HTMLElement; @@ -13,8 +15,7 @@ export const DisplayListContainer: ParentComponent = (props) => { top: '0px', left: '0px', }); - const [rootStyle, setRootStyle] = createSignal({ - // width: '100%', + const [rootStyle, setRootStyle] = createSignal({ height: '100%', }); const [bound, setBound] = createSignal({ @@ -38,6 +39,7 @@ export const DisplayListContainer: ParentComponent = (props) => { setRootStyle({ height: `${(_bound.bottom - _bound.top) * displayStore.viewScale}px`, + background: `url(${background})`, }); }; @@ -74,7 +76,7 @@ export const DisplayListContainer: ParentComponent = (props) => { return (
-
    +
      {props.children}
diff --git a/src/components/display-view.tsx b/src/components/display-view.tsx index 61fb24d..5083646 100644 --- a/src/components/display-view.tsx +++ b/src/components/display-view.tsx @@ -30,7 +30,7 @@ export const DisplayView: Component = (props) => { return (
= (props) => { /> = (props) => { title={props.color} >
@@ -101,6 +99,20 @@ export const LedStripPart: Component = (props) => { } }); + const onWheel = (e: WheelEvent) => { + if (localProps.config) { + invoke('patch_led_strip_len', { + displayId: localProps.config.display_id, + border: localProps.config.border, + deltaLen: e.deltaY > 0 ? 1 : -1, + }) + .then(() => {}) + .catch((e) => { + console.error(e); + }); + } + }; + const pixels = createMemo(() => { const _colors = colors(); if (_colors) { @@ -118,9 +130,9 @@ export const LedStripPart: Component = (props) => {
{pixels()}