Skip to content

Commit

Permalink
HR can restart BLE Central, allow adding new Activities, sync > flush?
Browse files Browse the repository at this point in the history
  • Loading branch information
nullstalgia committed Oct 3, 2024
1 parent 149e1ef commit cf0b263
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 72 deletions.
189 changes: 167 additions & 22 deletions src/activities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ use serde_with::{serde_as, DisplayFromStr};
use tui_input::Input;

use std::{collections::BTreeMap, path::PathBuf};
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::{fs::File, io::BufWriter};

use crate::app::{App, AppUpdate, SubState};
use crate::broadcast;
use crate::errors::AppError;

const ACTIVITIES_TOML_PATH: &str = "activities.toml";

pub struct Activities {
current_activity: u8,
pub current_activity: u8,
file: ActivitiesFile,
pub input: Input,
pub query: Vec<u8>,
Expand All @@ -24,15 +26,20 @@ pub struct Activities {
#[derive(Debug, Serialize, Deserialize)]
struct ActivitiesFile {
/// Used to set current_activity to the last one used before app close if `remember_last` is true.
#[serde(default)]
last_activity: u8,
#[serde_as(as = "BTreeMap<DisplayFromStr, _>")]
activities: BTreeMap<u8, String>,
/// Items formatted like "Index - Name" to avoid doing it each frame render
#[serde(skip)]
formatted: BTreeMap<u8, String>,
}

fn format_activities(activities: &BTreeMap<u8, String>) -> BTreeMap<u8, String> {
impl ActivitiesFile {
fn format(&mut self) {
self.formatted = formatted_activities(&self.activities);
}
}
fn formatted_activities(activities: &BTreeMap<u8, String>) -> BTreeMap<u8, String> {
let mut formatted = BTreeMap::new();
for (index, name) in activities {
formatted.insert(*index, format!("{index} - {}", name));
Expand All @@ -47,7 +54,7 @@ impl Default for ActivitiesFile {
map.insert(0, no_activity);
Self {
last_activity: 0,
formatted: format_activities(&map),
formatted: formatted_activities(&map),
activities: map,
}
}
Expand All @@ -66,42 +73,72 @@ impl Activities {
pub async fn save(&mut self) -> Result<(), AppError> {
self.file.last_activity = self.current_activity;
let file_path = PathBuf::from(ACTIVITIES_TOML_PATH);
let file = File::create(&file_path).await?;
let mut writer = BufWriter::new(file);
writer
.write_all(toml::to_string(&self.file)?.as_bytes())
let mut file = File::create(&file_path).await?;
file.write_all(toml::to_string(&self.file)?.as_bytes())
.await?;
writer.flush().await?;
file.flush().await?;
file.sync_data().await?;
Ok(())
}
pub async fn load(&mut self, remember_last: bool) -> Result<(), AppError> {
pub async fn load(&mut self, remember_last: bool) -> Result<u8, AppError> {
let file_path = PathBuf::from(ACTIVITIES_TOML_PATH);
if !file_path.exists() {
let file = File::create(&file_path).await?;
let mut writer = BufWriter::new(file);
let mut file = File::create(&file_path).await?;
let default = ActivitiesFile::default();
writer
.write_all(toml::to_string(&default)?.as_bytes())
file.write_all(toml::to_string(&default)?.as_bytes())
.await?;
writer.flush().await?;
file.flush().await?;
file.sync_data().await?;
self.file = default;
} else {
let mut file = File::open(&file_path).await?;
let mut buffer = String::new();
file.read_to_string(&mut buffer).await?;
let new: ActivitiesFile = toml::from_str(&buffer)?;
self.file = new;
// TODO bleh
self.file.formatted = format_activities(&self.file.activities);
self.file.format();
if remember_last && self.file.activities.contains_key(&self.file.last_activity) {
self.current_activity = self.file.last_activity;
} else {
self.current_activity = self.initial_activity();
}
}
self.query_from_input();
if self.selected().is_none() && !self.file.activities.is_empty() {
self.current_activity = self.query[0];

Ok(self.current_activity)
}
fn initial_activity(&self) -> u8 {
if self.file.activities.is_empty() {
0
} else {
*self.file.activities.keys().next().unwrap()
}
Ok(())
}
fn next_activity(&self) -> Option<u8> {
if self.file.activities.is_empty() {
Some(0)
} else if self.file.activities.len() >= u8::MAX as usize {
None
} else {
let mut next = self.initial_activity();
while self.file.activities.contains_key(&next) {
next += 1;
}
Some(next)
}
}
pub fn create_activity(&mut self) -> Option<u8> {
let activity = self.next_activity();
if let Some(new_activity) = activity {
self.file
.activities
.insert(new_activity, self.input.to_string());
self.file.format();
self.reset();
self.current_activity = new_activity;
// Saved by the new activity's broadcast! later
}
activity
}
pub fn selected(&self) -> Option<&String> {
self.file.formatted.get(&self.current_activity)
Expand Down Expand Up @@ -136,6 +173,11 @@ impl Activities {
}
self.current_activity
}
pub fn reset(&mut self) {
self.input.reset();
self.table_state.select(None);
self.query_from_input();
}
}

pub mod tui {
Expand All @@ -149,7 +191,10 @@ pub mod tui {
};
use ratatui_macros::{row, text};

use crate::{app::App, utils::centered_rect};
use crate::{
app::{App, SubState},
utils::centered_rect,
};

pub fn render_activity_selection(app: &mut App, f: &mut Frame) {
let area = centered_rect(20, 70, f.area());
Expand Down Expand Up @@ -183,6 +228,7 @@ pub mod tui {

let options_block = Block::default()
.borders(Borders::TOP | Borders::BOTTOM)
.title_top("Ctrl+N: New")
.title_bottom("Search:")
.title_alignment(Alignment::Center);

Expand All @@ -194,6 +240,11 @@ pub mod tui {
&mut app.activities.table_state,
);

// To hide new activity name from also appearing in the search bar
if app.sub_state != SubState::ActivitySelection {
return;
};

let width = input_area.width.max(1) - 1; // So the cursor doesn't bleed off the edge
let scroll = app.activities.input.visual_scroll(width as usize);
let input = Paragraph::new(app.activities.input.value()).scroll((0, scroll as u16));
Expand Down Expand Up @@ -221,4 +272,98 @@ pub mod tui {

activities_table
}

pub fn render_activity_name_entry(app: &mut App, f: &mut Frame) {
let mut area = centered_rect(30, 25, f.area());
area.height = area.height.min(4);
// let is_renaming = app.sub_state == SubState::ActivityRename;

// Create the outer block with borders and title
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().green())
.title("New Activity")
.title_alignment(Alignment::Center);

// Draw the block
f.render_widget(Clear, area);
f.render_widget(&block, area);

let vertical = Layout::vertical([
// Constraint::Length(2),
Constraint::Max(1),
Constraint::Fill(1),
]);
let inner_area = block.inner(area);
let [table_area, input_area] = vertical.areas(inner_area);

// let activity = app.activities.selected();
// let activity: &str = activity.map(|s| s.as_str()).unwrap_or("???");
// let header = Paragraph::new(text!["Current:", activity]).centered();

// f.render_widget(header, current_area);

let options_block = Block::default()
.borders(Borders::TOP | Borders::BOTTOM)
.title_top("Enter Activity Name:")
.title_alignment(Alignment::Center);

f.render_widget(options_block, table_area);

let width = input_area.width.max(1) - 1; // So the cursor doesn't bleed off the edge
let scroll = app.activities.input.visual_scroll(width as usize);
let input = Paragraph::new(app.activities.input.value()).scroll((0, scroll as u16));
f.render_widget(input, input_area);
f.set_cursor_position((
// Put cursor past the end of the input text
input_area.x + ((app.activities.input.visual_cursor()).max(scroll) - scroll) as u16,
input_area.y,
));
}
}

impl App {
pub fn activities_select_prompt(&mut self) {
if self.settings.activities.enabled {
self.activities.reset();
self.sub_state = SubState::ActivitySelection;
}
}
pub fn activities_new_prompt(&mut self) {
if self.settings.activities.enabled {
self.activities.reset();
self.sub_state = SubState::ActivityCreation;
}
}
pub fn activities_enter_pressed(&mut self) {
match self.sub_state {
SubState::ActivitySelection => {
let new_activity = self.activities.select_from_table();
self.broadcast_activity(new_activity);
self.sub_state = SubState::None;
}
SubState::ActivityCreation => {
if let Some(new_activity) = self.activities.create_activity() {
self.broadcast_activity(new_activity);
}
self.sub_state = SubState::None;
}
_ => {}
}
}
pub fn broadcast_activity(&mut self, activity: u8) {
broadcast!(
self.broadcast_tx,
AppUpdate::ActivitySelected(activity),
"Failed to send activity update!"
);
}
pub fn activities_esc_pressed(&mut self) {
if self.sub_state == SubState::ActivityCreation {
self.activities.reset();
self.sub_state = SubState::ActivitySelection;
} else {
self.sub_state = SubState::None;
}
}
}
37 changes: 26 additions & 11 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ pub enum SubState {
SaveDevicePrompt,
ConnectingForHeartRate,
ActivitySelection,
ActivityCreation,
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand All @@ -116,6 +117,8 @@ pub struct App {
// Devices as found by the BLE thread
pub ble_rx: Receiver<DeviceUpdate>,
pub ble_tx: Sender<DeviceUpdate>,
// A Sender that can be used to trigger the BLE thread to restart it's objects from other threads
pub ble_restart_tx: Option<Sender<()>>,
// (Usually) Status updates from the heart rate monitor
// Can also be errors from other actors
pub broadcast_rx: BReceiver<AppUpdate>,
Expand Down Expand Up @@ -210,6 +213,7 @@ impl App {
Self {
ble_tx,
ble_rx,
ble_restart_tx: None,
broadcast_rx,
broadcast_tx,
ble_scan_paused: Arc::new(AtomicBool::default()),
Expand Down Expand Up @@ -294,6 +298,10 @@ impl App {
} else {
self.start_bluetooth_event_thread();
}

if self.settings.activities.enabled && self.activities.current_activity != 0 {
self.broadcast_activity(self.activities.current_activity);
}
}

async fn try_load_activities(&mut self) -> bool {
Expand Down Expand Up @@ -410,9 +418,17 @@ impl App {
let pause_signal_clone = Arc::clone(&self.ble_scan_paused);
let app_tx_clone = self.ble_tx.clone();
let shutdown_requested_clone = self.cancel_actors.clone();
let (restart_tx, restart_rx) = mpsc::channel(1);
self.ble_restart_tx = Some(restart_tx);
debug!("Spawning Bluetooth CentralEvent thread");
self.ble_thread_handle = Some(tokio::spawn(async move {
bluetooth_event_thread(app_tx_clone, pause_signal_clone, shutdown_requested_clone).await
bluetooth_event_thread(
app_tx_clone,
restart_rx,
pause_signal_clone,
shutdown_requested_clone,
)
.await
}));
}

Expand Down Expand Up @@ -466,6 +482,7 @@ impl App {

let device = selected_device.clone();
let hr_tx_clone = self.broadcast_tx.clone();
let restart_tx_clone = self.ble_restart_tx.clone().expect("BLE Restart TX missing");
let shutdown_requested_clone = self.cancel_actors.clone();
// Not leaving as Duration as it's being used to check an abs difference
let rr_twitch_threshold =
Expand All @@ -475,6 +492,7 @@ impl App {
self.hr_thread_handle = Some(tokio::spawn(async move {
start_notification_thread(
hr_tx_clone,
restart_tx_clone,
device,
rr_ignore_after_empty,
rr_twitch_threshold,
Expand Down Expand Up @@ -874,8 +892,11 @@ impl App {
}
}
pub fn escape_pressed(&mut self) {
if self.sub_state == SubState::ActivitySelection {
self.sub_state = SubState::None;
match self.sub_state {
SubState::ActivitySelection | SubState::ActivityCreation => {
self.activities_esc_pressed();
}
_ => {}
}
}
pub fn enter_pressed(&mut self) {
Expand Down Expand Up @@ -961,14 +982,8 @@ impl App {
}
return;
}
SubState::ActivitySelection => {
let new_activity = self.activities.select_from_table();
broadcast!(
self.broadcast_tx,
AppUpdate::ActivitySelected(new_activity),
"Failed to send activity update!"
);
self.sub_state = SubState::None;
SubState::ActivitySelection | SubState::ActivityCreation => {
self.activities_enter_pressed();
}
_ => {}
}
Expand Down
Loading

0 comments on commit cf0b263

Please sign in to comment.