URI:
       refactor: consolidate repeat toggle and count into single unified repeat setting - timed-remote - Flipper Zero app for sending delayed IR commands
  HTML git clone git://src.adamsgaard.dk/timed-remote
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit 71aab8e1895ee34a0a35c7baab25b322d4be3ce2
   DIR parent 3fc23431d5701c11f9714b6154894808739cf1fc
  HTML Author: Anders Damsgaard <anders@adamsgaard.dk>
       Date:   Fri, 30 Jan 2026 23:17:42 +0100
       
       refactor: consolidate repeat toggle and count into single unified repeat setting
       
       Diffstat:
         M application.fam                     |       2 +-
         M scenes/scene_timer_config.c         |      67 ++++++++++++++++++-------------
         M scenes/scene_timer_running.c        |      10 ++++++----
         M timed_remote.h                      |       3 +--
       
       4 files changed, 47 insertions(+), 35 deletions(-)
       ---
   DIR diff --git a/application.fam b/application.fam
       @@ -2,7 +2,7 @@
        
        App(
            appid="timed_remote",  # Must be unique
       -    name="App timed_remote",  # Displayed in UI
       +    name="Timed Remote",  # Displayed in UI
            apptype=FlipperAppType.EXTERNAL,
            entry_point="timed_remote_app",
            stack_size=4 * 1024,
   DIR diff --git a/scenes/scene_timer_config.c b/scenes/scene_timer_config.c
       @@ -7,7 +7,6 @@ enum {
          TimerConfigIndexMinutes,
          TimerConfigIndexSeconds,
          TimerConfigIndexRepeat,
       -  TimerConfigIndexCount,
          TimerConfigIndexConfirm,
        };
        
       @@ -20,7 +19,7 @@ static void timer_config_mode_change(VariableItem *item) {
        
          /* Disable repeat in scheduled mode */
          if (app->timer_mode == TimerModeScheduled) {
       -    app->repeat_enabled = false;
       +    app->repeat_count = 0;
          }
        
          /* Trigger rebuild to show/hide repeat options */
       @@ -58,20 +57,19 @@ static void timer_config_seconds_change(VariableItem *item) {
        static void timer_config_repeat_change(VariableItem *item) {
          TimedRemoteApp *app = variable_item_get_context(item);
          uint8_t index = variable_item_get_current_value_index(item);
       -  app->repeat_enabled = (index == 1);
       -  variable_item_set_current_value_text(item,
       -                                       app->repeat_enabled ? "On" : "Off");
       -}
       -
       -static void timer_config_count_change(VariableItem *item) {
       -  TimedRemoteApp *app = variable_item_get_context(item);
       -  uint8_t index = variable_item_get_current_value_index(item);
       -  app->repeat_count = index; /* 0 = unlimited, 1-99 = fixed */
        
          char buf[16];
       -  if (app->repeat_count == 0) {
       +  if (index == 0) {
       +    /* Off */
       +    app->repeat_count = 0;
       +    snprintf(buf, sizeof(buf), "Off");
       +  } else if (index == 1) {
       +    /* Unlimited */
       +    app->repeat_count = 255;
            snprintf(buf, sizeof(buf), "Unlimited");
          } else {
       +    /* 1, 2, 3, ... 99 */
       +    app->repeat_count = index - 1;
            snprintf(buf, sizeof(buf), "%d", app->repeat_count);
          }
          variable_item_set_current_value_text(item, buf);
       @@ -79,7 +77,7 @@ static void timer_config_count_change(VariableItem *item) {
        
        static void timer_config_enter_callback(void *context, uint32_t index) {
          TimedRemoteApp *app = context;
       -  /* In countdown mode, confirm is at index 6, in scheduled mode it's at index 4 */
       +  /* In countdown mode, confirm is at index 5, in scheduled mode it's at index 4 */
          uint32_t confirm_index = app->timer_mode == TimerModeCountdown
                                       ? TimerConfigIndexConfirm
                                       : (TimerConfigIndexSeconds + 1);
       @@ -126,18 +124,25 @@ static void build_timer_config_list(TimedRemoteApp *app) {
        
          /* Repeat options - only for countdown mode */
          if (app->timer_mode == TimerModeCountdown) {
       -    /* Repeat toggle: Off/On */
       -    item = variable_item_list_add(app->variable_item_list, "Repeat", 2,
       +    /* Repeat: Off, Unlimited, 1, 2, 3, ... 99 (total 101 values) */
       +    item = variable_item_list_add(app->variable_item_list, "Repeat", 101,
                                          timer_config_repeat_change, app);
       -    variable_item_set_current_value_index(item, app->repeat_enabled ? 1 : 0);
       -    variable_item_set_current_value_text(item,
       -                                         app->repeat_enabled ? "On" : "Off");
       -
       -    /* Repeat count: 0 = unlimited, 1-99 = fixed */
       -    item = variable_item_list_add(app->variable_item_list, "Count", 100,
       -                                  timer_config_count_change, app);
       -    variable_item_set_current_value_index(item, app->repeat_count);
       +
       +    /* Convert repeat_count to index */
       +    uint8_t repeat_index;
       +    if (app->repeat_count == 0) {
       +      repeat_index = 0; /* Off */
       +    } else if (app->repeat_count == 255) {
       +      repeat_index = 1; /* Unlimited */
       +    } else {
       +      repeat_index = app->repeat_count + 1; /* 1-99 */
       +    }
       +    variable_item_set_current_value_index(item, repeat_index);
       +
       +    /* Set display text */
            if (app->repeat_count == 0) {
       +      variable_item_set_current_value_text(item, "Off");
       +    } else if (app->repeat_count == 255) {
              variable_item_set_current_value_text(item, "Unlimited");
            } else {
              snprintf(buf, sizeof(buf), "%d", app->repeat_count);
       @@ -171,11 +176,17 @@ bool timed_remote_scene_timer_config_on_event(void *context,
              build_timer_config_list(app);
              consumed = true;
            } else if (event.event == TimedRemoteEventTimerConfigured) {
       -      /* Initialize repeats remaining */
       -      app->repeats_remaining =
       -          app->repeat_enabled
       -              ? (app->repeat_count == 0 ? 255 : app->repeat_count)
       -              : 1;
       +      /* Initialize repeats remaining based on repeat_count encoding */
       +      if (app->repeat_count == 0) {
       +        /* Off - single execution */
       +        app->repeats_remaining = 1;
       +      } else if (app->repeat_count == 255) {
       +        /* Unlimited */
       +        app->repeats_remaining = 255;
       +      } else {
       +        /* Fixed count (1-99): initial + N repeats = N+1 total executions */
       +        app->repeats_remaining = app->repeat_count + 1;
       +      }
              scene_manager_next_scene(app->scene_manager,
                                       TimedRemoteSceneTimerRunning);
              consumed = true;
   DIR diff --git a/scenes/scene_timer_running.c b/scenes/scene_timer_running.c
       @@ -22,13 +22,15 @@ static void update_display(TimedRemoteApp *app) {
          widget_add_string_element(app->widget, 64, 25, AlignCenter, AlignTop,
                                    FontBigNumbers, time_str);
        
       -  if (app->repeat_enabled && app->repeat_count > 0) {
       +  if (app->repeat_count > 0 && app->repeat_count < 255) {
       +    /* Fixed repeat count (1-99) */
            char repeat_str[24];
            snprintf(repeat_str, sizeof(repeat_str), "Repeat: %d/%d",
                     app->repeat_count - app->repeats_remaining + 1, app->repeat_count);
            widget_add_string_element(app->widget, 64, 52, AlignCenter, AlignTop,
                                      FontSecondary, repeat_str);
       -  } else if (app->repeat_enabled) {
       +  } else if (app->repeat_count == 255) {
       +    /* Unlimited repeat */
            widget_add_string_element(app->widget, 64, 52, AlignCenter, AlignTop,
                                      FontSecondary, "Repeat: Unlimited");
          }
       @@ -54,7 +56,7 @@ void timed_remote_scene_timer_running_on_enter(void *context) {
          }
        
          /* Initialize repeat tracking for non-repeat or scheduled */
       -  if (!app->repeat_enabled) {
       +  if (app->repeat_count == 0) {
            app->repeats_remaining = 1;
          }
        
       @@ -92,7 +94,7 @@ bool timed_remote_scene_timer_running_on_event(void *context,
        
              app->repeats_remaining--;
        
       -      if (app->repeat_enabled && app->repeats_remaining > 0) {
       +      if (app->repeat_count != 0 && app->repeats_remaining > 0) {
                /* Reset countdown for next repeat */
                app->seconds_remaining =
                    time_helper_hms_to_seconds(app->hours, app->minutes, app->seconds);
   DIR diff --git a/timed_remote.h b/timed_remote.h
       @@ -56,8 +56,7 @@ typedef struct {
          uint8_t seconds;
        
          /* Repeat options (Countdown mode only) */
       -  bool repeat_enabled;
       -  uint8_t repeat_count; /* 0 = unlimited */
       +  uint8_t repeat_count; /* 0 = off, 255 = unlimited, 1-99 = count */
          uint8_t repeats_remaining;
        
          /* Timer runtime state */