Folders and files
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Repository files navigation
* org-window-habit [[https://melpa.org/#/org-window-habit][file:https://melpa.org/packages/org-window-habit-badge.svg]] [[https://stable.melpa.org/#/org-window-habit][file:https://stable.melpa.org/packages/org-window-habit-badge.svg]] [[https://github.com/colonelpanic8/org-window-habit/actions/workflows/ci.yml][file:https://github.com/colonelpanic8/org-window-habit/actions/workflows/ci.yml/badge.svg]] ** Why org-window-habit? Standard =org-habit= tracks habits using fixed repeater intervals: complete the habit, and it reschedules for the next interval. This works well for habits with predictable schedules, but many real-world habits don't fit that model: - *Exercise 5 times per week* - You don't care /which/ 5 days, just that you hit 5 within any 7-day window - *Read 4 books per month* - Finishing one book per week is fine, but so is reading two books in the first week and two in the last - *Take medication 3 times per day* - The exact hours don't matter, just the count within each 24-hour period With standard =org-habit=, missing a single day shows as a "failure" in your consistency graph, even if you're exceeding your actual goals. This can be demoralizing and doesn't reflect how habits actually work in practice. =org-window-habit= replaces this binary daily model with *window-based tracking*: instead of asking "did you do it today?", it asks "have you done it enough times within this time window?" ** The Window Habit Concept A *window habit* evaluates your compliance over a rolling time window rather than a single day. You define: 1. *Window Duration* - How far back to look when counting completions (e.g., 7 days, 1 month) 2. *Repetitions Required* - How many completions are needed within that window (e.g., 5 times) 3. *Assessment Interval* - How often to re-evaluate, i.e., the step size for the rolling window (e.g., daily) Note that window duration and assessment interval serve different purposes: the *window duration* determines how much history counts toward your goal, while the *assessment interval* determines how granularly the window rolls forward. For example, with a 7-day window and 1-day assessment interval, you get a fresh evaluation each day that looks back at the previous 7 days. If the assessment interval were also 7 days, the window would jump weekly rather than rolling daily. A practical example where these differ: suppose you want to write 8 blog posts per month, but you don't want daily nagging - just a weekly check-in. You'd set a 1-month window duration with a 1-week assessment interval. Each week, the system evaluates whether you're on track for your monthly goal, and the habit only appears in your agenda at weekly boundaries. *** Assessment Interval Anchoring For fixed-length intervals (days, hours), assessment boundaries are *anchored to your habit's start time*, not arbitrary calendar boundaries. This means: - A habit with a 3-day assessment interval that started on Jan 10 will have periods: Jan 10-13, Jan 13-16, Jan 16-19, etc. - Another habit with the same 3-day interval but starting Jan 11 will have different periods: Jan 11-14, Jan 14-17, Jan 17-20, etc. This ensures consistent, predictable evaluation periods for each habit. The library tracks your completions and calculates a *conforming ratio* - are you on track to meet your goal? The agenda graph shows this visually, with colors indicating your conformity status (by default, green when conforming and red when falling behind). When you complete a habit, org-window-habit automatically reschedules it to the next date when a completion will be needed to maintain conformity, rather than using a fixed repeater interval. *** Example: Weekly Exercise You want to exercise 5 times per week. With org-window-habit: #+begin_src org ,* TODO Exercise SCHEDULED: <2024-01-15 Mon .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: (:window-specs ((:duration (:days 7) :repetitions 5))) :END: #+end_src If you exercise Monday, Tuesday, Wednesday, Thursday, and Friday, you've met your goal for the week. Saturday and Sunday will show as "optional" - you don't need to exercise, but you can if you want to build buffer. If you only exercised 3 times so far this week and it's Friday, the graph shows you're behind and the habit appears as requiring completion. ** Installation org-window-habit is available on MELPA. With =use-package=: #+begin_src emacs-lisp (use-package org-window-habit :ensure t :demand t :config (org-window-habit-mode +1)) #+end_src Or install from git using =straight.el=: #+begin_src emacs-lisp (use-package org-window-habit :demand t :straight (org-window-habit :repo "colonelpanic8/org-window-habit" :host github :files ("org-window-habit.el")) :config (org-window-habit-mode +1)) #+end_src ** Basic Usage *** Creating a Window Habit 1. Create a TODO heading with =SCHEDULED= or =DEADLINE= that has any repeater (the repeater value doesn't matter - org-window-habit uses its own rescheduling logic, but a repeater must be present) 2. Add =:STYLE: habit= under =:PROPERTIES:= 3. Add the =OWH_CONFIG= property with your habit configuration 4. Ensure Org is logging TODO state changes for the entry. =org-window-habit= reads completion history from standard Org state-change logs whether they are stored inline or inside a =LOGBOOK= drawer. Setting =org-log-into-drawer= is optional. *** Recommended: Unified CONFIG Property The =OWH_CONFIG= property is the recommended way to configure window habits. It uses a single property containing all habit parameters: #+begin_src org ,* TODO Exercise SCHEDULED: <2024-01-15 Mon .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: (:window-specs ((:duration (:days 7) :repetitions 5))) :END: #+end_src This defines a habit requiring 5 completions per 7-day window. *With additional options:* #+begin_src org ,* TODO Exercise SCHEDULED: <2024-01-15 Mon .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: (:window-specs ((:duration (:days 7) :repetitions 5)) :assessment-interval (:days 1) :only-days (:monday :wednesday :friday)) :END: #+end_src *Available CONFIG keys:* | Key | Required | Default | Description | |-----------------------------------+----------+--------------------------+-------------------------------------------------------| | =:window-specs= | Yes | - | List of window specifications (see below) | | =:assessment-interval= | No | =(:days 1)= | How often to re-evaluate streaks and graphs | | =:aggregation-fn= | No | min ratio | Function used to combine multiple window scores | | =:reschedule-assessment-interval= | No | =(:days 1)= | How often to check whether another completion is due | | =:reschedule-interval= | No | =(:days 1)= | Minimum time after completion before rescheduling | | =:reschedule-threshold= | No | =1.0= | Conforming ratio threshold for rescheduling | | =:reschedule-days= | No | =:only-days= or all days | Restrict reminder dates without filtering completions | | =:max-reps-per-interval= | No | =1= | Max completions counted per assessment interval | | =:only-days= | No | (all days) | Restrict specific completion days | | =:from= | No | (unbounded) | Start requiring the habit at this inclusive date | | =:until= | No | (unbounded) | Stop requiring the habit at this exclusive date | *Window spec format:* #+begin_src elisp (:duration (:days 7) :repetitions 5) (:duration (:days 7) :repetitions 5 :value 1.0) ; with explicit weight #+end_src *** Versioned Configurations (Evolving Habits) Habits often evolve over time - you might start with 3 workouts per week and gradually increase to 5. The CONFIG property supports *versioned configurations* that let your habit requirements change while preserving accurate historical tracking. *Example: Increasing workout frequency* #+begin_src org ,* TODO Exercise SCHEDULED: <2024-01-15 Mon .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: ((:window-specs ((:duration (:days 7) :repetitions 5))) (:until "2024-06-01" :window-specs ((:duration (:days 7) :repetitions 3)))) :END: #+end_src This defines: - Current requirement: 5 times per week (active from June 1, 2024 onward) - Previous requirement: 3 times per week (active until June 1, 2024) *Versioned config rules:* 1. Configs are listed in *reverse temporal order* (most recent first) 2. Each config is active over the half-open range =[from, until)= 3. =:from= is inclusive; =:until= is exclusive; omitted bounds are unbounded 4. Matching adjacent bounds produce a continuous rule change; a gap between bounds makes the habit inactive during that period *With a gap (habit pause):* #+begin_src org :OWH_CONFIG: ((:from "2024-09-01" :window-specs ((:duration (:days 7) :repetitions 5))) (:until "2024-06-01" :window-specs ((:duration (:days 7) :repetitions 3)))) #+end_src This habit was inactive from June 1 through August 31, then resumed on September 1. A single config with =:until= and no later version represents a habit that remains inactive after that date. At runtime, =org-window-habit-create-instance-from-heading-at-point= and =org-window-habit-create-instance-from-config= accept an optional reference time and return nil when no config is active then. =org-window-habit-active-at-time-p= tests an existing instance, while =org-window-habit-for-time= returns an instance materialized with the version active at the requested time or nil for a gap. Status and scheduling APIs use the same behavior and return nil rather than a conformity or requirement for an inactive habit. *** Legacy: Scattered Properties For backwards compatibility, you can use individual properties instead of =OWH_CONFIG=. See "Configuration Properties Reference" below for the full list of legacy properties and defaults. *** Additional Examples *Monthly reading example:* #+begin_src org ,* TODO Read a book SCHEDULED: <2024-01-15 Mon .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: (:window-specs ((:duration (:months 1) :repetitions 4))) :END: #+end_src *Calendar-aligned monthly task (e.g., paying rent):* For tasks that must happen exactly once per calendar month (not a rolling 30-day window), set both the window duration and assessment interval to =(:months 1)=. The library aligns monthly intervals to the 1st of each month: #+begin_src org ,* TODO Pay rent SCHEDULED: <2024-01-01 Mon .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: (:window-specs ((:duration (:months 1) :repetitions 1)) :assessment-interval (:months 1)) :END: #+end_src This evaluates once per calendar month: did you pay rent in January? In February? The window doesn't roll daily - it jumps from month to month, aligned to calendar boundaries. *** Traditional Org-Habit Style (Fixed Interval from Completion) If you want a habit that simply repeats a fixed number of days after each completion - like traditional =org-habit= with a =.+3d= repeater - you can achieve this with org-window-habit: #+begin_src org ,* TODO Water plants SCHEDULED: <2024-01-15 Mon .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: (:window-specs ((:duration (:days 3) :repetitions 1))) :END: #+end_src This says "I need to water plants once every 3 days." After completing, the habit automatically reschedules to the next date when a completion will be needed - approximately 3 days out. The key insight is that org-window-habit's rescheduling is based on *when you'll fall out of conformity*, not a fixed offset. With a 3-day window requiring 1 completion, you'll need to complete again within 3 days of your last completion to stay conforming. *Taking medication every 8 hours:* #+begin_src org ,* TODO Take medication SCHEDULED: <2024-01-15 Mon .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: (:window-specs ((:duration (:hours 8) :repetitions 1)) :assessment-interval (:hours 1)) :END: #+end_src *Weekly task (once per week):* #+begin_src org ,* TODO Review finances SCHEDULED: <2024-01-15 Mon .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: (:window-specs ((:duration (:days 7) :repetitions 1))) :END: #+end_src The advantage over traditional =org-habit= is that you still get the visual conformity graph showing your history, and the flexibility to complete early without penalty. *** Advanced Configuration: Multiple Windows For more complex requirements, use =:window-specs= in =OWH_CONFIG= to define multiple evaluation windows. The habit must satisfy *all* windows simultaneously. #+begin_src org :PROPERTIES: :OWH_CONFIG: (:window-specs ((:duration (:days 4) :repetitions 1) (:duration (:days 6) :repetitions 2))) :END: #+end_src This requires: - At least 1 completion in any 4-day window, AND - At least 2 completions in any 6-day window This prevents both long gaps and insufficient frequency. *Example: Averaging multiple windows* #+begin_src org :PROPERTIES: :OWH_CONFIG: (:window-specs ((:duration (:days 2) :repetitions 1 :value 1.0) (:duration (:days 4) :repetitions 2 :value 1.0) (:duration (:days 8) :repetitions 4 :value 1.0)) :assessment-interval (:days 1) :aggregation-fn org-window-habit-weighted-average-aggregation-fn) :END: #+end_src This averages the completion ratios for the 2, 4, and 8 day windows instead of taking the minimum across them. ** Configuration Properties Reference All properties use the =OWH_= prefix by default (configurable via =org-window-habit-property-prefix=). *** Recommended: CONFIG Property The =OWH_CONFIG= property is the recommended approach. It contains all habit parameters in a single plist and supports versioned configurations. See "Unified CONFIG Property" above for details. *** Legacy: Scattered Properties For backwards compatibility, you can use individual properties instead of =OWH_CONFIG=: | Property | Default | Description | |--------------------------------------+------------------------+---------------------------------------------------------| | =OWH_WINDOW_DURATION= | (required) | Single window length (e.g., =1w=, =1m=, =(:days 7)=) | | =OWH_REPETITIONS_REQUIRED= | (required) | Target completions for simple window | | =OWH_OKAY_REPETITIONS_REQUIRED= | same as above | Minimum acceptable completions | | =OWH_WINDOW_SPECS= | (alternative) | List of window specs for complex requirements | | =OWH_ASSESSMENT_INTERVAL= | =(:days 1)= | How often to re-evaluate streaks and graphs | | =OWH_RESCHEDULE_ASSESSMENT_INTERVAL= | =(:days 1)= | How often to check whether another completion is due | | =OWH_RESCHEDULE_INTERVAL= | =(:days 1)= | Minimum time after completion before rescheduling | | =OWH_RESCHEDULE_THRESHOLD= | =1.0= | Conforming ratio threshold for rescheduling | | =OWH_MAX_REPETITIONS_PER_INTERVAL= | =1= | Cap on completions counted per assessment interval | | =OWH_RESET_TIME= | (none) | Inactive date to ignore completions before | | =OWH_ONLY_DAYS= | (none) | Restrict habit to specific days of week (e.g., M/W/F) | | =OWH_RESCHEDULE_DAYS= | =OWH_ONLY_DAYS= or all | Restrict reminder dates without filtering completions | *Note:* When =OWH_CONFIG= is present, scattered properties are ignored. You can migrate existing habits using =M-x org-window-habit-migrate-to-config=. ** Reading the Agenda Graph Like =org-habit=, org-window-habit displays a consistency graph in the agenda. Each character represents one assessment interval (typically one day): - *Past intervals*: Show whether you were conforming (green) or not (red) at that time - *Current interval*: Shows whether you need to complete today: - =☐= (box) - Completion needed today to stay on track - =✓= (checkmark) - Already completed today (or not needed) - *Future intervals*: Project expected conformity based on current trajectory The colors indicate your conforming ratio: - Green shades: On track or ahead - Red shades: Falling behind on requirements When =org-window-habit-show-streak= is non-nil, the graph also appends a compact =S<n>= indicator. This is the current streak: the number of consecutive assessment intervals, ending with the current interval, whose aggregate conforming ratio is at least =org-window-habit-streak-threshold=. For a weekly 5-of-7 habit with a weekly assessment interval, =S3= means three consecutive conforming weeks. For a rolling daily assessment, it means consecutive conforming days. ** Customization Key customizable variables (=M-x customize-group RET org-window-habit=): | Variable | Default | Description | |--------------------------------------------------+-------------+------------------------------------| | =org-window-habit-property-prefix= | ="OWH"= | Prefix for org properties | | =org-window-habit-preceding-intervals= | =21= | Days of history shown in graph | | =org-window-habit-following-days= | =4= | Days of future projection in graph | | =org-window-habit-show-streak= | =t= | Show current streak after graph | | =org-window-habit-streak-format= | =" S%d"= | Format for the streak indicator | | =org-window-habit-streak-threshold= | =1.0= | Ratio required to extend streak | | =org-window-habit-conforming-color= | ="#4d7085"= | Color for conforming intervals | | =org-window-habit-not-conforming-color= | ="#d40d0d"= | Color for non-conforming intervals | | =org-window-habit-completion-needed-today-glyph= | =?☐= | Character for "needed today" | | =org-window-habit-completed-glyph= | =?✓= | Character for "completed" | ** Duration Format Window durations can be specified as: - Shorthand strings: =1d= (1 day), =1w= (1 week), =1m= (1 month), =2w= (2 weeks) - Property lists: =(:days 7)=, =(:weeks 1)=, =(:months 1)=, or combinations like =(:days 1 :hours 12)= *** Week Alignment Note that =1w= shorthand converts to =(:days 7)=, which does *not* align to a specific day of the week. For true week-aligned habits, use the =:weeks= duration type with an optional =:start= day: - =(:weeks 1)= - Aligns to Monday (default) - =(:weeks 1 :start :sunday)= - Aligns to Sunday - =(:weeks 1 :start :saturday)= - Aligns to Saturday (for habits aligned to the weekend) Available start days: =:sunday=, =:monday=, =:tuesday=, =:wednesday=, =:thursday=, =:friday=, =:saturday= *Example: Weekly review every Monday* #+begin_src org ,* TODO Weekly review SCHEDULED: <2024-01-15 Mon .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: (:window-specs ((:duration (:weeks 1 :start :monday) :repetitions 1)) :assessment-interval (:weeks 1 :start :monday)) :END: #+end_src *Example: Weekly streak with daily catch-up reminders* #+begin_src org ,* TODO Exercise SCHEDULED: <2024-01-22 Mon .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: (:window-specs ((:duration (:weeks 1 :start :monday) :repetitions 5)) :assessment-interval (:weeks 1 :start :monday) :max-reps-per-interval 5) :END: #+end_src This counts streaks by completed Monday-to-Monday weeks. Rescheduling still checks daily by default, so after each completion the habit appears again the next day until the week has five completions. Once the goal is met, the next reminder is the following Monday. *Example: Sabbath observance (Friday sundown)* #+begin_src org ,* TODO Shabbat preparation SCHEDULED: <2024-01-19 Fri .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: (:window-specs ((:duration (:weeks 1 :start :friday) :repetitions 1)) :assessment-interval (:weeks 1 :start :friday)) :END: #+end_src *** Day-of-Week Restrictions Some habits only make sense on specific days of the week. For example, you might go to the gym on Monday, Wednesday, and Friday, or do meal prep only on weekends. Use =:only-days= in =OWH_CONFIG= to restrict which days count toward your habit (legacy =OWH_ONLY_DAYS= still works). When =:only-days= is set: - Completions on non-allowed days are *ignored* when counting toward your goal - Rescheduling automatically targets the *next allowed day* The format is a list of day symbols: =(:monday :wednesday :friday)= Available days: =:sunday=, =:monday=, =:tuesday=, =:wednesday=, =:thursday=, =:friday=, =:saturday= *Example: M/W/F gym habit* #+begin_src org ,* TODO Go to gym SCHEDULED: <2024-01-15 Mon .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: (:window-specs ((:duration (:days 7) :repetitions 3)) :only-days (:monday :wednesday :friday)) :END: #+end_src This requires 3 gym visits per week, but only Monday, Wednesday, and Friday completions count. If you complete on Tuesday by mistake, it won't count toward your goal. After completing on Monday, the habit reschedules to Wednesday (skipping Tuesday). *Example: Weekend meal prep* #+begin_src org ,* TODO Meal prep for the week SCHEDULED: <2024-01-20 Sat .+1d> :PROPERTIES: :STYLE: habit :OWH_CONFIG: (:window-specs ((:duration (:days 7) :repetitions 1)) :only-days (:saturday :sunday)) :END: #+end_src This requires meal prep once per week, but only on weekends. Completing on a weekday won't count.