Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ When building a package for distribution, please ensure the following:
```
The resulting binary will be located at `target/release/linux-enable-ir-emitter`. You can also use `cargo install --path <...>` to your convenience.
4. The v7 is incompatible with the v6. If applicable, please make sure to use the provided [migration script]() on the saved configuration.
> [!Important]
> [!IMPORTANT]
> This script is not yet available. It will be provided when the v7 will be officially released (currently in beta).

## Contributing Code
Expand All @@ -24,10 +24,8 @@ This project is using the usual Rust conventions. Here are some additional expla
cargo build
```
The resulting binary will be located at `target/debug/linux-enable-ir-emitter`

> [!NOTE]
> With a debug build, any camera can be used, even not infrared ones. This is useful for development and testing.

2. Add the pre-commit hooks to make sure the linting checks and tests are passing before each commit:
```
git config core.hooksPath .githooks
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ auth optional pam_exec.so /home/<USER>/.local/bin/linux-enable-ir-emitter run --
```

> [!TIP]
> The installation paths may vary depending on your installation method. You can determine the correct binary absolute paths by running `which linux-enable-ir-emitter` and use that path instead. For the configuration path, it will be written when you can execute `linux-enable-ir-emitter --config`.
> The installation path may vary depending on your installation method. You can determine the correct binary absolute path by running `which linux-enable-ir-emitter` and use it instead. For the configuration path, it will be written when you execute `linux-enable-ir-emitter --config`.

### Integration with other program
You will need to execute the `linux-enable-ir-emitter run` command before the program that uses the infrared camera.

Alternatively, if you can and/or want to integrate better with the program that uses the camera, you can pass an opened file descriptor for the camera to the command: `linux-enable-ir-emitter run --device <DEVICE> --fd <FD>`.

> [!Important]
> You will need to pass the config path as argument to `linux-enable-ir-emitter run --config <CONFIG_PATH>` **when executed as root** if `linux-enable-ir-emitter configure` was executed as a normal user.
> [!IMPORTANT]
> You will need to pass the config path as argument to `linux-enable-ir-emitter run --config <CONFIG_PATH>` **when executed as root**, if `linux-enable-ir-emitter configure` was executed as a normal user.

## How do I enable my infrared emitter?
0. For a better experience, use a large terminal window.
Expand Down
4 changes: 3 additions & 1 deletion src/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ mod ui;
pub async fn configure() -> anyhow::Result<()> {
let res = app::run(&mut ratatui::init()).await;
ratatui::restore();

// Print any successful message to the user once the TUI is closed
if let Ok(msg) = &res
&& !msg.is_empty()
{
println!("{}", msg);
}
res.map(|_| ())
res.map(|_| ()) // Delete the success message
}
1 change: 1 addition & 0 deletions src/configure/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod helper;
pub mod ir_enabler;
pub mod tool_menu;
pub mod tweaker;

pub async fn run(terminal: &mut ratatui::DefaultTerminal) -> anyhow::Result<&'static str> {
tool_menu::App::new().run(terminal).await
Expand Down
113 changes: 84 additions & 29 deletions src/configure/app/ir_enabler.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::helper::*;
use crate::configure::ui::ir_enabler::{IrEnablerCtx, View, ui};
use crate::configure::ui::keys::*;
use crate::configure::ui::{DeviceSettingsCtx, SearchSettingsCtx};
use crate::video::ir::analyzer::{
self, IsIrWorking as AnalyzerResponse, Message as AnalyzerRequest, StreamAnalyzer,
};
Expand All @@ -24,6 +24,14 @@ use tokio::{
task,
};

const KEY_YES: KeyCode = KeyCode::Char('y');
const KEY_NO: KeyCode = KeyCode::Char('n');
const KEY_EXIT: KeyCode = KeyCode::Esc;
const KEY_NAVIGATE_UP: KeyCode = KeyCode::Up;
const KEY_NAVIGATE_DOWN: KeyCode = KeyCode::Down;
const KEY_CONTINUE: KeyCode = KeyCode::Enter;
const KEY_DELETE: KeyCode = KeyCode::Backspace;

#[derive(Debug)]
pub struct Config {
/// Path to the video device.
Expand Down Expand Up @@ -334,7 +342,7 @@ impl App {
async fn handle_term_event(&mut self, event: Event) -> Result<()> {
match event {
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
self.handle_key_press(key_event.code.into()).await?;
self.handle_key_press(key_event.code).await?;
}
_ => {}
};
Expand Down Expand Up @@ -422,13 +430,14 @@ impl App {
}

/// Handles a key event based on the current application state.
async fn handle_key_press(&mut self, key: Key) -> Result<()> {
async fn handle_key_press(&mut self, key: KeyCode) -> Result<()> {
match self.state() {
State::Menu => match key {
KEY_EXIT => self.set_state(State::Failure),
KEY_NAVIGATE => self.next_setting(),
KEY_NAVIGATE_UP => self.prev_setting(),
KEY_NAVIGATE_DOWN => self.next_setting(),
KEY_DELETE => self.edit_setting(None),
Key(KeyCode::Char(c)) => self.edit_setting(Some(c)),
KeyCode::Char(c) => self.edit_setting(Some(c)),
KEY_CONTINUE => self.set_state(State::ConfirmStart),
_ => {}
},
Expand Down Expand Up @@ -458,7 +467,7 @@ impl App {
/// In both of the two case, also changes the state to [`State::Running`].
///
/// Otherwise, does nothing.
async fn confirm_working(&mut self, k: Key) -> Result<()> {
async fn confirm_working(&mut self, k: KeyCode) -> Result<()> {
let mut response = IREnablerResponse::No;
if k == KEY_YES {
response = IREnablerResponse::Yes;
Expand All @@ -477,7 +486,7 @@ impl App {
/// and sends [`IREnablerResponse::Abort`] to the configurator task.
///
/// If the key is [`KEY_NO`], change the state back to [`State::Running`].
async fn abort_or_continue(&mut self, k: Key) -> Result<()> {
async fn abort_or_continue(&mut self, k: KeyCode) -> Result<()> {
match k {
KEY_NO | KEY_EXIT => self.set_state(self.prev_state()),
KEY_YES => {
Expand All @@ -498,7 +507,7 @@ impl App {
/// If the key is [`KEY_NO`], change the state back to the previous state.
///
/// Returns directly an error if the video stream is already started.
fn start_or_back(&mut self, k: Key) -> Result<()> {
fn start_or_back(&mut self, k: KeyCode) -> Result<()> {
// check that the path exists
if !self.is_device_valid() {
self.set_state(State::Menu);
Expand Down Expand Up @@ -534,15 +543,22 @@ impl App {
self.device_settings_list_state.select(None);
self.search_settings_list_state.select_first();
}
} else if let Some(i) = self.search_settings_list_state.selected() {
if i < 3 {
self.search_settings_list_state.select_next();
} else {
self.search_settings_list_state.select_next();
}
}

/// Moves the selection to the previous setting in the settings lists.
fn prev_setting(&mut self) {
if let Some(i) = self.search_settings_list_state.selected() {
if i > 0 {
self.search_settings_list_state.select_previous();
} else {
self.search_settings_list_state.select(None);
self.device_settings_list_state.select_first();
self.device_settings_list_state.select_last();
}
} else {
self.device_settings_list_state.select_first();
self.device_settings_list_state.select_previous();
}
}

Expand Down Expand Up @@ -594,6 +610,18 @@ impl IrEnablerCtx for App {
fn show_menu_start_prompt(&self) -> bool {
self.state() == State::ConfirmStart
}
fn controls_list_state(&mut self) -> &mut ListState {
&mut self.controls_list_state
}
fn controls(&self) -> &[XuControl] {
&self.controls
}
fn image(&self) -> Option<&Image> {
self.image.as_ref()
}
}

impl DeviceSettingsCtx for App {
fn device_settings_list_state(&mut self) -> &mut ListState {
&mut self.device_settings_list_state
}
Expand All @@ -615,6 +643,9 @@ impl IrEnablerCtx for App {
fn fps(&self) -> Option<u32> {
self.config.fps
}
}

impl SearchSettingsCtx for App {
fn search_settings_list_state(&mut self) -> &mut ListState {
&mut self.search_settings_list_state
}
Expand All @@ -633,15 +664,6 @@ impl IrEnablerCtx for App {
fn inc_step(&self) -> u8 {
self.config.inc_step
}
fn controls_list_state(&mut self) -> &mut ListState {
&mut self.controls_list_state
}
fn controls(&self) -> &[XuControl] {
&self.controls
}
fn image(&self) -> Option<&Image> {
self.image.as_ref()
}
}

#[cfg(test)]
Expand All @@ -655,16 +677,16 @@ mod tests {
App::new()
}

fn make_key_event(keycode: Key) -> KeyEvent {
fn make_key_event(keycode: KeyCode) -> KeyEvent {
KeyEvent {
code: keycode.into(),
code: keycode,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE,
}
}

fn make_term_key_event(keycode: Key) -> Event {
fn make_term_key_event(keycode: KeyCode) -> Event {
Event::Key(make_key_event(keycode))
}

Expand Down Expand Up @@ -713,7 +735,7 @@ mod tests {
}

#[test]
fn test_next_setting_device_to_search_and_back() {
fn test_next_setting_device_to_search() {
let mut app = make_app();
// Move through device settings (0..4)
for i in 0..4 {
Expand All @@ -724,14 +746,47 @@ mod tests {
app.next_setting();
assert!(app.device_settings_list_state.selected().is_none());
assert_eq!(app.search_settings_list_state.selected(), Some(0));
// Move through search settings (0..2)
// Move through search settings (0..3)
for i in 0..3 {
app.next_setting();
assert_eq!(app.search_settings_list_state.selected(), Some(i + 1));
}
// After 2, should wrap to device settings first
// After 3 we should stay at the last search setting
app.next_setting();
assert_eq!(app.search_settings_list_state.selected(), Some(4));
}

#[test]
fn test_prev_setting_device_to_search() {
let mut app = make_app();
// Start at the end of search settings
app.device_settings_list_state.select(None);
app.search_settings_list_state.select(Some(4));

// Move through search settings (4..0)
for i in (1..=4).rev() {
assert_eq!(app.search_settings_list_state.selected(), Some(i));
app.prev_setting();
}

// At index 0 of search settings, prev_setting should move to device settings last
assert_eq!(app.search_settings_list_state.selected(), Some(0));
app.prev_setting();
assert!(app.search_settings_list_state.selected().is_none());

// NOTE: select_last() sets to usize::MAX until screen is rendered to know that it is actually 4
assert_eq!(app.device_settings_list_state.selected(), Some(usize::MAX));
// so let's cheat
app.device_settings_list_state.select(Some(4));

// Move through device settings (4..0)
for i in (1..=4).rev() {
app.prev_setting();
assert_eq!(app.device_settings_list_state.selected(), Some(i - 1));
}

// At the first device setting, we should stay there
app.prev_setting();
assert_eq!(app.device_settings_list_state.selected(), Some(0));
}

Expand Down Expand Up @@ -880,7 +935,7 @@ mod tests {
let mut app = make_app();
app.set_state(State::Menu);
app.device_settings_list_state.select(Some(0));
let key_event = make_term_key_event(KEY_NAVIGATE);
let key_event = make_term_key_event(KEY_NAVIGATE_DOWN);
let res = app.handle_term_event(key_event).await;
assert!(res.is_ok(), "{:?}", res.err());
assert_eq!(app.device_settings_list_state.selected(), Some(1));
Expand Down
14 changes: 7 additions & 7 deletions src/configure/app/tool_menu.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::configure::app::ir_enabler::App as IREnablerApp;
use crate::configure::ui::keys::{KEY_CONTINUE, KEY_EXIT, KEY_NAVIGATE};
use crate::configure::app::tweaker::App as TweakerApp;
use crate::configure::ui::tool_menu::ui;

use anyhow::Result;
use crossterm::event;
use crossterm::event::{self, KeyCode};
use crossterm::event::{Event, KeyEventKind};

/// Application state for the tool menu.
Expand Down Expand Up @@ -31,10 +31,10 @@ impl App {
terminal.draw(|f| ui(f, self))?;
match event::read()? {
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
match key_event.code.into() {
KEY_NAVIGATE => self.next_tool(),
KEY_CONTINUE => return self.start_tool(terminal).await,
KEY_EXIT => return Ok(""),
match key_event.code {
KeyCode::Tab => self.next_tool(),
KeyCode::Enter => return self.start_tool(terminal).await,
KeyCode::Esc => return Ok(""),
_ => {}
}
}
Expand All @@ -57,7 +57,7 @@ impl App {
) -> Result<&'static str> {
match self.state {
State::IREnablerSelected => IREnablerApp::new().run(terminal).await,
State::UVCTweakerSelected => anyhow::bail!("UVC Tweaker is not yet implemented"),
State::UVCTweakerSelected => TweakerApp::new().run(terminal).await,
}
}
}
Expand Down
Loading
Loading