<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anicca – Daily Affirmations</title>
    <description>The latest articles on DEV Community by Anicca – Daily Affirmations (@aniccaapp).</description>
    <link>https://dev.to/aniccaapp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4172967%2F1fc7815c-68fd-4442-be18-d9b9d78cb0d9.png</url>
      <title>DEV Community: Anicca – Daily Affirmations</title>
      <link>https://dev.to/aniccaapp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aniccaapp"/>
    <language>en</language>
    <item>
      <title>A 30-Line JavaScript Tone Checker for Text Messages (and Where Regex Breaks)</title>
      <dc:creator>Anicca – Daily Affirmations</dc:creator>
      <pubDate>Sat, 31 Oct 2026 15:23:35 +0000</pubDate>
      <link>https://dev.to/aniccaapp/a-30-line-javascript-tone-checker-for-text-messages-and-where-regex-breaks-gll</link>
      <guid>https://dev.to/aniccaapp/a-30-line-javascript-tone-checker-for-text-messages-and-where-regex-breaks-gll</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I make Honne, an iPhone app mentioned at the end. The code below is a standalone toy and is not how Honne works.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most text-message fights don't start with what someone said. They start with how it &lt;em&gt;read&lt;/em&gt;. "k." is fine in your head and icy on the other person's lock screen.&lt;/p&gt;

&lt;p&gt;Rules of thumb for this are well known (one-word replies, "always/never", "per my last email"), so I turned the common ones into a small JavaScript linter you can run on your own message before you hit send.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tonecheck.js: flag phrases that often read colder than you meant&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RULES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;re&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;ok|k|fine|sure&lt;/span&gt;&lt;span class="se"&gt;)\.?&lt;/span&gt;&lt;span class="sr"&gt;$/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;One-word replies can read as annoyed. Add a reason or a next step.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;re&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(&lt;/span&gt;&lt;span class="sr"&gt;always|never&lt;/span&gt;&lt;span class="se"&gt;)\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;'Always/never' sounds like blame. Describe one specific time instead.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;re&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;you &lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;need to|should|have to&lt;/span&gt;&lt;span class="se"&gt;)\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Commands put people on the defensive. Try 'could we…' or 'would it help if…'.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;re&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;per my last &lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;email|message&lt;/span&gt;&lt;span class="se"&gt;)\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Widely read as passive-aggressive. Just restate the point.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;re&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;whatever&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;'Whatever' usually reads as dismissive.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;re&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/!&lt;/span&gt;&lt;span class="se"&gt;{2,}&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\?{2,}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Stacked punctuation reads as shouting.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;re&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b[&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;]{4,}\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ALL CAPS reads as shouting.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;re&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;no offense,&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt; but&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The 'but' cancels the 'no offense'.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toneCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;RULES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;!?🙂😊&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Very short replies can feel cold over text. A short 'thanks' or emoji softens it.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it flags
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; k.
  - One-word replies can read as annoyed. Add a reason or a next step.
  - Very short replies can feel cold over text. A short 'thanks' or emoji softens it.

&amp;gt; You never answer my calls!!
  - 'Always/never' sounds like blame. Describe one specific time instead.
  - Stacked punctuation reads as shouting.

&amp;gt; Per my last email, the deadline is Friday.
  - Widely read as passive-aggressive. Just restate the point.

&amp;gt; Thanks, that works for me. See you at 7?
  (no flags)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where regex breaks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context.&lt;/strong&gt; "Fine." after "Want pizza?" is neutral. After an argument it isn't. A regex sees one message, not the thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationship.&lt;/strong&gt; A blunt colleague and a blunt partner mean different things. The same words can be normal for one person and a warning sign from another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sarcasm and in-jokes.&lt;/strong&gt; "Great, love that for me" passes every rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language.&lt;/strong&gt; These patterns are English-only. Tone markers differ a lot between languages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good uses: a pre-send check in a chat tool you're building, a Slack bot that nudges before a 1-word reply to a long message, or a writing-assistant demo. Don't use it to judge other people's messages. It will be confidently wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want the AI version
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://apps.apple.com/app/id6759667221?pt=93486075&amp;amp;ct=devto_honne_20261101&amp;amp;mt=8" rel="noopener noreferrer"&gt;Honne&lt;/a&gt; for the other direction: you paste a message someone sent you, and its AI suggests the likely emotion behind it (anger, anxiety, loneliness, hope) and drafts a calm reply. There are modes for partner, friend, family and colleague. The free tier gives 3 analyses a day. It's an AI's read, not mind-reading, so treat it as a second opinion.&lt;/p&gt;

&lt;p&gt;What rules would you add to the list? I'm collecting them in the comments.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Is That Text Passive-Aggressive? How to Tell and What to Reply (Phrase Table + Templates)</title>
      <dc:creator>Anicca – Daily Affirmations</dc:creator>
      <pubDate>Tue, 27 Oct 2026 05:04:51 +0000</pubDate>
      <link>https://dev.to/aniccaapp/is-that-text-passive-aggressive-how-to-tell-and-what-to-reply-phrase-table-templates-24j1</link>
      <guid>https://dev.to/aniccaapp/is-that-text-passive-aggressive-how-to-tell-and-what-to-reply-phrase-table-templates-24j1</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; you usually can't tell from one message alone. A text like "fine." or "k" reads as passive-aggressive when it clashes with what was just asked, with how this person normally writes, or with a pattern of indirect complaints. The most reliable move is to wait a few minutes, then ask one neutral question about the actual decision ("I can't tell if you're okay with Saturday. Are you?") instead of replying to the coldest possible reading.&lt;/p&gt;

&lt;p&gt;Below is a checklist for reading the message, a table of common phrases and their possible meanings, and reply templates you can adapt.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Check three things before you decide what it means
&lt;/h2&gt;

&lt;p&gt;Text strips out voice, face and timing, so your brain fills the gap, often with the worst version. Before you label a message, check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The message it answers.&lt;/strong&gt; "Sure, whatever works" after you suggested two options is different from "sure, whatever works" after you cancelled for the third time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Their normal style.&lt;/strong&gt; Some people always write in short lowercase replies. A one-word answer is only a signal if it's unusual &lt;em&gt;for them&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pattern.&lt;/strong&gt; One curt reply is weak evidence. Repeated jabs, guilt lines, or "I'm fine" followed by cold behavior is stronger.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If none of the three stands out, the most likely explanation is that they were busy.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Common phrases and what they might mean
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Message&lt;/th&gt;
&lt;th&gt;Neutral reading&lt;/th&gt;
&lt;th&gt;Pointed reading&lt;/th&gt;
&lt;th&gt;What to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"k" / "ok."&lt;/td&gt;
&lt;td&gt;Typing fast, agrees&lt;/td&gt;
&lt;td&gt;Annoyed, shutting down&lt;/td&gt;
&lt;td&gt;Do they usually write longer?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Fine."&lt;/td&gt;
&lt;td&gt;Genuinely fine&lt;/td&gt;
&lt;td&gt;Not fine, doesn't want to argue&lt;/td&gt;
&lt;td&gt;Was something just declined or changed?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Whatever works for you"&lt;/td&gt;
&lt;td&gt;Flexible&lt;/td&gt;
&lt;td&gt;Feels ignored in the decision&lt;/td&gt;
&lt;td&gt;Did they get a real choice?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Must be nice"&lt;/td&gt;
&lt;td&gt;Light envy, joking&lt;/td&gt;
&lt;td&gt;Resentment about unfairness&lt;/td&gt;
&lt;td&gt;Your history around that topic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"I guess I'll do it myself"&lt;/td&gt;
&lt;td&gt;Mild self-deprecation&lt;/td&gt;
&lt;td&gt;Feels unsupported, guilt-tripping&lt;/td&gt;
&lt;td&gt;Did they ask for help directly first?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Per my last message" (work)&lt;/td&gt;
&lt;td&gt;Pointing to info&lt;/td&gt;
&lt;td&gt;Irritated you missed it&lt;/td&gt;
&lt;td&gt;Did you actually miss it?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"No worries 🙂"&lt;/td&gt;
&lt;td&gt;No worries&lt;/td&gt;
&lt;td&gt;Worried, being polite&lt;/td&gt;
&lt;td&gt;Tone of the rest of the thread&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these readings is proof. The table is for slowing down, not for deciding.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. How to reply: five steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wait before replying.&lt;/strong&gt; If it stung, give it ten minutes. You don't owe a reply at the speed the message arrived.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide what you want from the exchange.&lt;/strong&gt; A plan settled? Clearing the air? Usually it's one of those, not winning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask about the decision, not their mood.&lt;/strong&gt; "Are you mad at me?" puts them on the defensive. A question about the concrete thing is easier to answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If they say they're fine, take it for now.&lt;/strong&gt; Pushing ("I know you're mad") turns your guess into the fight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If it keeps happening, describe the words, not the person.&lt;/strong&gt; "That message came across as annoyed" is answerable. "You're so passive-aggressive" is an attack on who they are.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  4. Reply templates
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Partner, after "Fine."&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tempting: "Wow. Okay then."&lt;/li&gt;
&lt;li&gt;Better: "I can't tell from text how you feel about it. Do you want to talk tonight, or is it actually fine?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Friend, after "Must be nice."&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tempting: "Not my fault you didn't go."&lt;/li&gt;
&lt;li&gt;Better: "Ha, it was fun. I'd love to do it together next time. Want me to tell you when the next one is?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Coworker or boss, after "Per my last message."&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tempting: "I read it."&lt;/li&gt;
&lt;li&gt;Better: "Thanks, I'd missed that detail. So the deadline is Thursday and I own the draft, right?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Family, after "I guess I'll do it myself."&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tempting: "Fine, do it then."&lt;/li&gt;
&lt;li&gt;Better: "I'm happy to help. Next time just ask me straight out and I'll say yes or no."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Anyone, when it's a repeated pattern&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"If something's bothering you, I'd rather hear it directly. I'll listen."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Getting a second opinion before you hit send
&lt;/h2&gt;

&lt;p&gt;When you've reread a message five times and still can't tell, a second reading helps. Some people ask a friend. Another option is an app like &lt;a href="https://aniccaai.com/honne?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=honne_passive_aggressive" rel="noopener noreferrer"&gt;Honne&lt;/a&gt;, an iPhone app where you pick the relationship (partner, friend, family, or work), paste the message, and get an interpretation of its tone plus a suggested reply you can rewrite in your own voice. Treat any AI reading as a perspective, not proof: you know the person better than it does. Leave out names and personal details you don't need to share.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is "k" always passive-aggressive?&lt;/strong&gt;&lt;br&gt;
No. For many people "k" is just a fast "okay." It's only a signal if it's out of character or follows something they might be upset about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I ask "Are you mad at me?"&lt;/strong&gt;&lt;br&gt;
Usually not. It makes their feelings the topic and invites a defensive "No." Asking about the concrete plan gets a clearer answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if they deny it but keep doing it?&lt;/strong&gt;&lt;br&gt;
Name the pattern once, calmly, and say what you'd prefer: "When I get one-word replies after plans change, I can't tell what you think. Can you tell me straight?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I just ignore a passive-aggressive text?&lt;/strong&gt;&lt;br&gt;
Ignoring one ambiguous message is fine. Ignoring a pattern tends to let resentment build on both sides.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can AI tell if a text is passive-aggressive?&lt;/strong&gt;&lt;br&gt;
It can offer a reasonable reading of tone and suggest wording, but it can't know intent or your history together. Use it to slow down and draft, then decide yourself.&lt;/p&gt;

</description>
      <category>communication</category>
      <category>mentalhealth</category>
      <category>relationships</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Affirmation App With No Streaks: Why Streaks Backfire for Anxious People</title>
      <dc:creator>Anicca – Daily Affirmations</dc:creator>
      <pubDate>Mon, 26 Oct 2026 22:50:10 +0000</pubDate>
      <link>https://dev.to/aniccaapp/affirmation-app-with-no-streaks-why-streaks-backfire-for-anxious-people-m22</link>
      <guid>https://dev.to/aniccaapp/affirmation-app-with-no-streaks-why-streaks-backfire-for-anxious-people-m22</guid>
      <description>&lt;p&gt;Most habit apps assume consistency comes from fear of breaking a chain. For anxious or burned-out people, the chain becomes the problem. Here is why I shipped an affirmation app with zero streaks, and what replaced them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why streaks backfire
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A missed day becomes evidence.&lt;/strong&gt; "I couldn't even keep a 5-day streak" is exactly the self-talk an affirmation is supposed to soften.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The app becomes a chore.&lt;/strong&gt; You open it to protect a number, not because it helps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery feels like starting over.&lt;/strong&gt; Day 1 after day 47 hurts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What works instead
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Streak-based design&lt;/th&gt;
&lt;th&gt;No-streak design&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Daily counter, resets to 0&lt;/td&gt;
&lt;td&gt;No counter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Red badge on missed day&lt;/td&gt;
&lt;td&gt;Nothing happens on missed days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You must open the app&lt;/td&gt;
&lt;td&gt;The line comes to you at a time you choose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Motivation through loss&lt;/td&gt;
&lt;td&gt;Motivation through relief&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How Anicca does it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://aniccaai.com/affirmation-app?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=no-streaks" rel="noopener noreferrer"&gt;Anicca&lt;/a&gt; sends one short, honest line at the time of day you pick, across 13 self-care topics (overthinking, self-worth, burnout and more). There is no streak and no score. Free to download on iPhone, with an optional subscription.&lt;/p&gt;

&lt;p&gt;Example lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Rest is part of the work."&lt;/li&gt;
&lt;li&gt;"I'm allowed to start again without starting over."&lt;/li&gt;
&lt;li&gt;"Today can be a 60% day."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Longer guide: &lt;a href="https://aniccaai.com/affirmation-app/a/affirmation-app-no-streaks?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=no-streaks" rel="noopener noreferrer"&gt;Affirmation app with no streaks&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is there an affirmation app without streaks?&lt;/strong&gt;&lt;br&gt;
Yes. Anicca has no streaks or scores; it sends one line at your chosen time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do streaks help build habits?&lt;/strong&gt;&lt;br&gt;
For some people, yes. For people prone to anxiety or perfectionism, losing a streak often causes quitting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should replace streaks?&lt;/strong&gt;&lt;br&gt;
Timing. A reminder at the moment you usually struggle beats a counter you have to protect.&lt;/p&gt;

</description>
      <category>mentalhealth</category>
      <category>ux</category>
      <category>productivity</category>
      <category>ios</category>
    </item>
    <item>
      <title>Best Affirmation App for Anxiety in 2026: 5 Features That Actually Help (Comparison + FAQ)</title>
      <dc:creator>Anicca – Daily Affirmations</dc:creator>
      <pubDate>Mon, 26 Oct 2026 22:50:08 +0000</pubDate>
      <link>https://dev.to/aniccaapp/best-affirmation-app-for-anxiety-in-2026-5-features-that-actually-help-comparison-faq-2k30</link>
      <guid>https://dev.to/aniccaapp/best-affirmation-app-for-anxiety-in-2026-5-features-that-actually-help-comparison-faq-2k30</guid>
      <description>&lt;p&gt;Anxiety is not a motivation problem, so an affirmation app that shouts "You are unstoppable!" at you often makes it worse. Here is what actually matters when you pick one, plus an honest comparison of the apps people ask about most in 2026.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Affirmation apps are not therapy. If anxiety is disrupting your daily life, talk to a doctor or licensed therapist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The 5 features that matter most for anxiety
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Believable lines, not bold declarations.&lt;/strong&gt; Research (Wood et al., 2009) found that positive self-statements that feel too far from what you believe can backfire for people with low self-esteem. Gentle, plausible lines work better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing you control.&lt;/strong&gt; A line that arrives at 11 p.m. when your mind starts spiraling helps more than one you have to remember to open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No streak guilt.&lt;/strong&gt; Streaks and red "missed day" badges become one more thing to worry about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short.&lt;/strong&gt; Under a minute. Anxiety does not leave room for a 20-minute session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pair with breathing or reframing when needed.&lt;/strong&gt; For panic, a breathing or CBT tool is a better first step than affirmations alone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Streaks&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://aniccaai.com/affirmation-app?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=best-affirmation-app-anxiety" rel="noopener noreferrer"&gt;Anicca&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;One short, gentle line at the time you choose&lt;/td&gt;
&lt;td&gt;Text notification, 13 self-care topics&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Free download, optional subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Innertune&lt;/td&gt;
&lt;td&gt;Listening with eyes closed&lt;/td&gt;
&lt;td&gt;Audio affirmations over music&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Free + paid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gratitude: Self-Care Journal&lt;/td&gt;
&lt;td&gt;Writing the worry down&lt;/td&gt;
&lt;td&gt;Affirmations + journal prompts&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Free + paid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breeze&lt;/td&gt;
&lt;td&gt;Panic moments&lt;/td&gt;
&lt;td&gt;Breathing, CBT thought records&lt;/td&gt;
&lt;td&gt;No streaks (per their site)&lt;/td&gt;
&lt;td&gt;Free, optional Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Say After Me&lt;/td&gt;
&lt;td&gt;Speaking affirmations aloud&lt;/td&gt;
&lt;td&gt;Voice coaching&lt;/td&gt;
&lt;td&gt;No score&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(Check each App Store listing for current pricing.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your mind spirals at a predictable time&lt;/strong&gt; (bedtime, Sunday evening, before meetings): pick an app that pushes a line at that time. That is the reason I built &lt;a href="https://aniccaai.com/affirmation-app?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=best-affirmation-app-anxiety" rel="noopener noreferrer"&gt;Anicca&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You prefer audio:&lt;/strong&gt; Innertune.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need to get it out of your head:&lt;/strong&gt; a journal-style app like Gratitude.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are in a panic attack right now:&lt;/strong&gt; a breathing app first, affirmations later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example lines that are believable
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;"I can feel anxious and still do the next small thing."&lt;/li&gt;
&lt;li&gt;"This feeling is loud, not permanent."&lt;/li&gt;
&lt;li&gt;"I don't have to solve tomorrow tonight."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More: &lt;a href="https://aniccaai.com/affirmation-app/a/affirmations-for-anxiety?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=best-affirmation-app-anxiety" rel="noopener noreferrer"&gt;Affirmations for anxiety&lt;/a&gt; · &lt;a href="https://aniccaai.com/affirmation-app/a/do-affirmations-help-anxiety?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=best-affirmation-app-anxiety" rel="noopener noreferrer"&gt;Do affirmations help anxiety?&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the best affirmation app for anxiety?&lt;/strong&gt;&lt;br&gt;
The one whose lines you actually believe and that reaches you when anxiety starts. For timed, gentle text lines with no streaks, Anicca; for audio, Innertune; for journaling, Gratitude.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do affirmations help anxiety?&lt;/strong&gt;&lt;br&gt;
They can help some people interrupt a thought loop, especially when the lines are believable. They do not treat an anxiety disorder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are streaks bad for anxious people?&lt;/strong&gt;&lt;br&gt;
They can be. Missing a day turns into another source of self-criticism, which is why some apps (including Anicca) have none.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there a free affirmation app for anxiety?&lt;/strong&gt;&lt;br&gt;
Anicca, Innertune, Gratitude and Breeze are all free to download with optional paid tiers.&lt;/p&gt;

</description>
      <category>mentalhealth</category>
      <category>productivity</category>
      <category>ios</category>
      <category>wellness</category>
    </item>
    <item>
      <title>A 3-Entry Gratitude Log in 30 Lines of JavaScript (and Where localStorage Breaks)</title>
      <dc:creator>Anicca – Daily Affirmations</dc:creator>
      <pubDate>Mon, 26 Oct 2026 03:34:21 +0000</pubDate>
      <link>https://dev.to/aniccaapp/a-3-entry-gratitude-log-in-30-lines-of-javascript-and-where-localstorage-breaks-27ig</link>
      <guid>https://dev.to/aniccaapp/a-3-entry-gratitude-log-in-30-lines-of-javascript-and-where-localstorage-breaks-27ig</guid>
      <description>&lt;p&gt;A lot of habit apps fail the same way: the first screen asks for too much. A blank page, a mood score, five questions. On a bad day you close it and never come back.&lt;/p&gt;

&lt;p&gt;I wanted to see how small a gratitude log could get and still be useful, so I wrote one in plain JavaScript. It keeps three short entries per day in &lt;code&gt;localStorage&lt;/code&gt;, shows the last week, and that is it. Here is the whole thing and what I learned building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"f"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"a"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"One small good thing"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"b"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Another"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"c"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"And one more"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Save today&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"log"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gratitude-log&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLocaleDateString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-CA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// YYYY-MM-DD in local time&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;load&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;log&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;li&amp;gt;&amp;lt;b&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/b&amp;gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; · &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/li&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;f&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// one record per day; saving again overwrites&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;About 30 lines. No build step, no account, nothing leaves the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three decisions that mattered more than the code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Only the first field is required.&lt;/strong&gt; On a hard day, one line is a win. Requiring all three turned out to be the fastest way to make me skip a day entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Use the local date, not UTC.&lt;/strong&gt; &lt;code&gt;new Date().toISOString()&lt;/code&gt; gives you a UTC date, so an entry written at 8am in Tokyo lands on "yesterday". &lt;code&gt;toLocaleDateString("en-CA")&lt;/code&gt; returns &lt;code&gt;YYYY-MM-DD&lt;/code&gt; in the user's own time zone, which is what "today" means to a person.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Overwrite instead of append.&lt;/strong&gt; Saving twice on the same day replaces the record. That keeps the data model one key per day and avoids a log full of half-finished duplicates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this breaks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;localStorage&lt;/code&gt; is per browser and per device. Clear site data and the log is gone. For anything you care about, export it (a &lt;code&gt;JSON.stringify(load())&lt;/code&gt; download button is five more lines).&lt;/li&gt;
&lt;li&gt;There is no reminder. In my experience the reminder, not the form, is what keeps the habit alive, and a static page cannot send one reliably.&lt;/li&gt;
&lt;li&gt;I deliberately left out streaks. They motivate some people and make others quit after the first missed day. If you add one, consider showing "days this month" instead of an unbroken count.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you would rather not build it
&lt;/h2&gt;

&lt;p&gt;Disclosure: I make Thankful, a free iPhone gratitude journal built around the same idea: three short entries, about two minutes, plus a daily affirmation. It does have a reminder and a streak count, so it trades a little of the minimalism above for the nudge. &lt;a href="https://apps.apple.com/app/id6759514159?pt=93486075&amp;amp;ct=devto_thankful_20261026&amp;amp;mt=8" rel="noopener noreferrer"&gt;Thankful on the App Store&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Otherwise, the snippet above is free to copy. Three lines a day is enough.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A Bedtime Calculator in 15 Lines of JavaScript (and Where the 90-Minute Rule Breaks)</title>
      <dc:creator>Anicca – Daily Affirmations</dc:creator>
      <pubDate>Sat, 17 Oct 2026 14:08:10 +0000</pubDate>
      <link>https://dev.to/aniccaapp/a-bedtime-calculator-in-15-lines-of-javascript-and-where-the-90-minute-rule-breaks-33f8</link>
      <guid>https://dev.to/aniccaapp/a-bedtime-calculator-in-15-lines-of-javascript-and-where-the-90-minute-rule-breaks-33f8</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I make an iOS sleep app (linked at the end). The code below is free to use and does not need it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A lot of "sleep calculators" boil down to one idea: sleep comes in cycles of roughly 90 minutes, so count backwards from your alarm in whole cycles. That's a rough model, not a medical fact, because real cycles vary between people and across the night. But it is a handy rule of thumb, and it takes about 15 lines of JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bedtime calculator: count back whole 90-minute cycles from a wake time.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CYCLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// minutes, an average, not a constant&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FALL_ASLEEP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// minutes, a typical assumption&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bedtimes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wake&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cycles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wakeMin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cycles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="nx"&gt;wakeMin&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;CYCLE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;FALL_ASLEEP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cycles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sleepHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;CYCLE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bedtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hh&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;mm&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bedtimes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;06:30&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running it for a 06:30 alarm (I ran this with Node before posting):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cycles:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sleepHours:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;bedtime:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cycles:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sleepHours:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;7.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;bedtime:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cycles:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sleepHours:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;bedtime:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two details that bite
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wrap past midnight.&lt;/strong&gt; &lt;code&gt;(x % 1440 + 1440) % 1440&lt;/code&gt; keeps the result in 0 to 1439 even when the subtraction goes negative. Without the double modulo, JavaScript returns negative remainders and you print times like &lt;code&gt;-45:00&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fall-asleep buffer.&lt;/strong&gt; The 15 minutes is an assumption. If you take longer to drop off, the "cycle-aligned" result is already off before you start. Treat the output as a target to head to bed, not as a guarantee of how you will wake.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where this model stops
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The 90-minute figure is an average. Individual cycles run shorter or longer.&lt;/li&gt;
&lt;li&gt;It says nothing about &lt;em&gt;regularity&lt;/em&gt;. Going to bed and waking at the same time most days usually matters more than hitting an exact cycle boundary.&lt;/li&gt;
&lt;li&gt;If you have persistent insomnia or daytime sleepiness, a calculator is not a substitute for talking to a clinician.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to take it
&lt;/h2&gt;

&lt;p&gt;Easy extensions: read the wake time from an &lt;code&gt;&amp;lt;input type="time"&amp;gt;&lt;/code&gt;, show the next three alarms for a given bedtime instead, or let the user set their own cycle length and buffer.&lt;/p&gt;

&lt;p&gt;If you would rather not build it yourself: I make &lt;a href="https://apps.apple.com/us/app/for-better-sleep-sleep-reset/id6762143790?ct=devto_sleep_calc_20261017" rel="noopener noreferrer"&gt;Sleep Reset for iPhone&lt;/a&gt;. It has a free sleep reset score; the personalized plan and bedtime timing need a paid subscription.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>health</category>
    </item>
    <item>
      <title>PDF to Clean Text or Markdown: 3 Free Commands I Tested (and Where Each Breaks)</title>
      <dc:creator>Anicca – Daily Affirmations</dc:creator>
      <pubDate>Fri, 16 Oct 2026 23:31:35 +0000</pubDate>
      <link>https://dev.to/aniccaapp/pdf-to-clean-text-or-markdown-3-free-commands-i-tested-and-where-each-breaks-5fmb</link>
      <guid>https://dev.to/aniccaapp/pdf-to-clean-text-or-markdown-3-free-commands-i-tested-and-where-each-breaks-5fmb</guid>
      <description>&lt;p&gt;Full disclosure: I make PDF Insight, a web app that does PDF → text/summary. This post is mostly about the free command-line options, because for a lot of jobs they're all you need. I'll say where each one breaks, including mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plain text, fast:&lt;/strong&gt; &lt;code&gt;pdftotext -layout file.pdf out.txt&lt;/code&gt; (Poppler). Keep &lt;code&gt;-layout&lt;/code&gt; on or table rows fall apart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Markdown with headings, in Python:&lt;/strong&gt; &lt;code&gt;pymupdf4llm.to_markdown("file.pdf")&lt;/code&gt;. Good headings, but check your tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scanned PDFs (images, no text layer):&lt;/strong&gt; none of the above work. You need OCR first (&lt;code&gt;ocrmypdf in.pdf out.pdf&lt;/code&gt;, then extract).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't want to script it / need a summary:&lt;/strong&gt; a hosted tool (mine is linked at the end, there are others).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The test file
&lt;/h2&gt;

&lt;p&gt;I didn't want to hand-wave, so I made a tiny PDF with a heading, a paragraph, a 3-row table and a second heading, and ran each tool on it. You can rebuild the same file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install pymupdf
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;
&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;h1&amp;gt;Quarterly Report&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;Revenue grew in the second quarter while costs stayed flat.
The table below lists results by region.&amp;lt;/p&amp;gt;
&amp;lt;table border=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Region&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Q1&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Q2&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;North&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;120&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;140&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;South&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;90&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;95&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;West&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;60&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;81&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;
&amp;lt;h2&amp;gt;Notes&amp;lt;/h2&amp;gt;&amp;lt;p&amp;gt;West grew fastest because of a new partner. South was flat.&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="n"&gt;story&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Story&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DocumentWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paper_rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;more&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;more&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;more&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;story&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;place&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;story&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Versions used: Poppler &lt;code&gt;pdftotext&lt;/code&gt; 25.03.0, PyMuPDF / pymupdf4llm 1.28.2, Linux, October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;pdftotext&lt;/code&gt; (Poppler): the default you probably already have
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pdftotext sample.pdf -          &lt;span class="c"&gt;# print to stdout&lt;/span&gt;
pdftotext &lt;span class="nt"&gt;-layout&lt;/span&gt; sample.pdf -  &lt;span class="c"&gt;# keep the visual layout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;-layout&lt;/code&gt;, the table came out like this — every cell on its own line, so you can't tell which number belongs to which region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Region Q1 Q2
North

120 140

South
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;-layout&lt;/code&gt; the rows stay together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Region Q1 Q2
North   120 140
South   90 95
West    60 81
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; ligatures. "flat" came out as "ﬂat" (one &lt;code&gt;ﬂ&lt;/code&gt; character), which breaks &lt;code&gt;grep flat&lt;/code&gt;. Normalise it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NFKC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 'ﬂ' -&amp;gt; 'fl'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; grepping, quick copy-paste, shell pipelines. &lt;strong&gt;What it won't do:&lt;/strong&gt; headings or Markdown structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;pymupdf4llm&lt;/code&gt;: Markdown with real headings
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pymupdf4llm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pymupdf4llm&lt;/span&gt;
&lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymupdf4llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output on the test file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# **Quarterly Report**&lt;/span&gt;

Revenue grew in the second quarter while costs stayed flat. ...

&lt;span class="gs"&gt;**Region Q1 Q2**&lt;/span&gt; North 120 140 South 90 95 West 60 81

&lt;span class="gu"&gt;## **Notes**&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Headings became &lt;code&gt;#&lt;/code&gt; / &lt;code&gt;##&lt;/code&gt; and the ligature was already fixed. But on this file the table was flattened into one line. Results vary by how the table was drawn in the PDF, so check tables by eye before you feed the output to an LLM or a search index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; chunking documents for RAG / LLM prompts, where headings matter. &lt;strong&gt;What it won't do:&lt;/strong&gt; guarantee table structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Scanned PDFs: OCR first
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;pdftotext&lt;/code&gt; prints nothing (or garbage), the PDF is probably images. Add a text layer, then extract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# apt install ocrmypdf   (or brew install ocrmypdf)&lt;/span&gt;
ocrmypdf scanned.pdf searchable.pdf
pdftotext &lt;span class="nt"&gt;-layout&lt;/span&gt; searchable.pdf out.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OCR quality depends on the scan; tables from scans are the hardest case for every tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Hosted tools
&lt;/h2&gt;

&lt;p&gt;If you don't want to script it, or you want a summary rather than raw text, a hosted tool saves time. Options include ChatPDF (chat-first), NotebookLM (free, good across several sources at once) and PDF Insight (mine).&lt;/p&gt;

&lt;p&gt;What PDF Insight does: upload a PDF, get clean Markdown text you can copy or download, plus a short, medium or long AI summary with key points; on Pro you can ask questions and get answers cited from the document. The site says its parser is built for complex layouts, tables and multi-column pages, but I didn't include it in the test above, so try it on your own file. Free is 1 PDF; Pro is $15/mo billed yearly. Link: &lt;a href="https://clear-pdf-converter.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=pdf_insight&amp;amp;ct=devto_pdf_md_20261017" rel="noopener noreferrer"&gt;PDF Insight&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;pdftotext -layout&lt;/code&gt; first. If the output looks right, stop there.&lt;/li&gt;
&lt;li&gt;If you need headings (for chunking/RAG), try &lt;code&gt;pymupdf4llm&lt;/code&gt; and eyeball every table.&lt;/li&gt;
&lt;li&gt;If you get nothing back, OCR with &lt;code&gt;ocrmypdf&lt;/code&gt; and extract again.&lt;/li&gt;
&lt;li&gt;If you need a summary or Q&amp;amp;A rather than raw text, use a hosted tool and spot-check its answers against the page.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why does my extracted table have one cell per line?&lt;/strong&gt; You ran &lt;code&gt;pdftotext&lt;/code&gt; without &lt;code&gt;-layout&lt;/code&gt;. Add it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why can't I grep a word I can see in the PDF?&lt;/strong&gt; Often a ligature (&lt;code&gt;ﬁ&lt;/code&gt;, &lt;code&gt;ﬂ&lt;/code&gt;) or a hyphenated line break. NFKC-normalise and join hyphenated lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which is best for feeding an LLM?&lt;/strong&gt; Markdown with headings (e.g. &lt;code&gt;pymupdf4llm&lt;/code&gt;), checked for tables. Headings make chunking much better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does any of this work on scanned PDFs?&lt;/strong&gt; Not without OCR. Run &lt;code&gt;ocrmypdf&lt;/code&gt; first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it safe to upload confidential PDFs to a hosted tool?&lt;/strong&gt; Read the tool's privacy policy first; for sensitive files the local commands above keep everything on your machine.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I'm the developer of PDF Insight. This post was drafted with an AI assistant; the commands and outputs above were run on a real file.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>pdf</category>
      <category>markdown</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A 3-Minute Reset for When Your Mind Starts to Spiral (and Why Affirmations Fail Without Timing)</title>
      <dc:creator>Anicca – Daily Affirmations</dc:creator>
      <pubDate>Thu, 15 Oct 2026 11:22:19 +0000</pubDate>
      <link>https://dev.to/aniccaapp/a-3-minute-reset-for-when-your-mind-starts-to-spiral-and-why-affirmations-fail-without-timing-o0l</link>
      <guid>https://dev.to/aniccaapp/a-3-minute-reset-for-when-your-mind-starts-to-spiral-and-why-affirmations-fail-without-timing-o0l</guid>
      <description>&lt;p&gt;Disclosure: I'm the indie developer behind Anicca, an iPhone affirmation app. This is a self-care tip, not medical advice.&lt;/p&gt;

&lt;p&gt;Most affirmation advice says "repeat a positive sentence every morning." The trouble is that spirals don't happen in the morning. They happen at 11pm, or right after a tense message. Here is a reset that works because it is tied to the moment, not the clock.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3-minute reset
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Name it (30 seconds).&lt;/strong&gt; Say or write one plain sentence: "I'm overthinking about X." Naming a thought makes it an object you look at instead of a room you live in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow your exhale (60 seconds).&lt;/strong&gt; Breathe in for 4, out for 6. The longer exhale is the whole trick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick one kind, believable line (30 seconds).&lt;/strong&gt; Skip "I am perfect." Try "I can be unsure and still be okay." If you don't believe a line even 20%, it won't land.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do one tiny next action (60 seconds).&lt;/strong&gt; Drink water, close the tab, send the short reply. Spirals feed on inaction.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why timing matters
&lt;/h2&gt;

&lt;p&gt;An affirmation read when you are calm is pleasant. One that arrives when your mind starts racing has a job to do. That is the idea behind Anicca: it sends one line of kindness around the moments you tend to spiral, so you don't have to remember to open it. There are no streaks to protect and no guilt if you miss a day.&lt;/p&gt;

&lt;p&gt;If you'd like to try it, it's a free download with an optional subscription, iPhone only: &lt;a href="https://aniccaai.com/affirmation-app?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=anicca_en_organic&amp;amp;utm_content=devto_reset" rel="noopener noreferrer"&gt;Anicca on the App Store&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Whatever tool you use, the pattern stays the same: name it, slow your exhale, choose a believable line, take one small action.&lt;/p&gt;

</description>
      <category>mentalhealth</category>
      <category>selfcare</category>
      <category>ios</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Reply When You Can't Tell If Someone Is Upset in a Text</title>
      <dc:creator>Anicca – Daily Affirmations</dc:creator>
      <pubDate>Thu, 15 Oct 2026 11:16:03 +0000</pubDate>
      <link>https://dev.to/aniccaapp/how-to-reply-when-you-cant-tell-if-someone-is-upset-in-a-text-1bf8</link>
      <guid>https://dev.to/aniccaapp/how-to-reply-when-you-cant-tell-if-someone-is-upset-in-a-text-1bf8</guid>
      <description>&lt;p&gt;You get a short message: "fine." or "ok, whatever works." And now you're staring at your phone trying to work out whether the person is annoyed, tired, or just typing fast.&lt;/p&gt;

&lt;p&gt;Text strips out tone, so we fill the gap with our own worries. Here is a simple routine for replying without making things worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Separate what they wrote from what you feel about it
&lt;/h2&gt;

&lt;p&gt;Write down the literal words. Then, separately, write what you &lt;em&gt;fear&lt;/em&gt; they mean. Most replies that go badly respond to the fear, not to the words.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Name two or three possible feelings behind the message
&lt;/h2&gt;

&lt;p&gt;"Fine." could be tired, hurt, or relieved the topic is closed. Listing more than one possibility stops you from locking onto the worst one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pick the reply that works for every possibility
&lt;/h2&gt;

&lt;p&gt;A good reply doesn't need you to guess right. "Got it. Want to talk about it tonight, or should I give you some space?" works whether they're upset or just busy.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keep it short and specific
&lt;/h2&gt;

&lt;p&gt;Long defensive replies escalate. One sentence acknowledging, one sentence offering a next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Wait ten minutes if you feel heat
&lt;/h2&gt;

&lt;p&gt;A reply sent at peak irritation is rarely the one you would send an hour later.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tool that does steps 2 and 3
&lt;/h2&gt;

&lt;p&gt;I built Honne, a free iPhone app, for exactly this. You paste the message you received, and it suggests the feeling that may be behind it and drafts a calm reply, with modes for partner, friend, family and colleague. It's an AI suggestion, so treat it as a starting point and edit before you send. The free tier covers 3 analyses a day; Premium is optional. Note that your pasted text is processed by an AI service to produce the suggestion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apps.apple.com/app/id6759667221?pt=93486075&amp;amp;ct=devto_honne&amp;amp;mt=8" rel="noopener noreferrer"&gt;Honne on the App Store&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built Honne.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>communication</category>
      <category>productivity</category>
      <category>ios</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built an affirmation app that shows up when your mind spirals, not when you remember to open it</title>
      <dc:creator>Anicca – Daily Affirmations</dc:creator>
      <pubDate>Fri, 09 Oct 2026 09:27:03 +0000</pubDate>
      <link>https://dev.to/aniccaapp/i-built-an-affirmation-app-that-shows-up-when-your-mind-spirals-not-when-you-remember-to-open-it-13ba</link>
      <guid>https://dev.to/aniccaapp/i-built-an-affirmation-app-that-shows-up-when-your-mind-spirals-not-when-you-remember-to-open-it-13ba</guid>
      <description>&lt;p&gt;Most self-care apps have the same quiet flaw: they only work when you open them. And the moments you most need a kind sentence are the moments you're least likely to open anything. It's 1 a.m. and you're replaying a conversation, or you're halfway through comparing your life to someone's highlight reel.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Anicca&lt;/strong&gt;, an iPhone affirmation app. Its whole job is to send &lt;em&gt;one&lt;/em&gt; line of kindness at the point where your mind tends to start spiraling. You don't have to remember to open it.&lt;/p&gt;

&lt;p&gt;This post covers the decisions behind it, including one that was hurting us, and where the app honestly stands today.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Timing is the product
&lt;/h2&gt;

&lt;p&gt;An affirmation is one sentence, and it's easy to copy. What a sentence can't do on its own is show up at the right moment.&lt;/p&gt;

&lt;p&gt;Anicca learns roughly when self-doubt, late-night overthinking or anxiety tend to hit you. It also learns which lines actually help, and quietly drops the ones that don't. A notification is the whole interaction: you read one line, and you can tap through to the full card if you want more.&lt;/p&gt;

&lt;p&gt;On the iOS side this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;Notification Service Extension&lt;/strong&gt; that processes each incoming card before it is shown, so the notification itself carries the line rather than a generic "Time for your affirmation!"&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;widget&lt;/strong&gt;, so a line can sit on the home screen without the app open&lt;/li&gt;
&lt;li&gt;UI flows tested with &lt;strong&gt;Maestro&lt;/strong&gt;, and releases shipped with &lt;strong&gt;fastlane&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. No streaks, on purpose
&lt;/h2&gt;

&lt;p&gt;Streaks work for language apps. For an app aimed at people who already feel like they're failing, a "You missed a day 🔥" message is the opposite of the point. Anicca has no streaks, no "you missed a day" guilt and no productivity pressure.&lt;/p&gt;

&lt;p&gt;The name comes from the Pali word &lt;em&gt;anicca&lt;/em&gt;, impermanence. The idea behind the app is that every storm passes, and that includes the one you're in right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The mistake: asking for a rating before giving anything
&lt;/h2&gt;

&lt;p&gt;Here's the honest part. When I audited the app, the only place we asked for an App Store review was &lt;strong&gt;during onboarding&lt;/strong&gt;, before a single affirmation had arrived. In the US we have exactly &lt;strong&gt;one&lt;/strong&gt; rating, and that probably isn't a coincidence.&lt;/p&gt;

&lt;p&gt;The fix is merged and goes out in the next build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we only ask people who have used the app for &lt;strong&gt;3+ days&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;we only ask right after a good moment, when they &lt;strong&gt;favourite&lt;/strong&gt; a card or have &lt;strong&gt;read 3+ affirmations&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;we ask &lt;strong&gt;once&lt;/strong&gt;, then wait at least &lt;strong&gt;120 days&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your own app calls &lt;code&gt;requestReview&lt;/code&gt; in onboarding, check the timing. Apple limits how often the prompt can appear anyway, so spending it on someone who hasn't seen any value yet wastes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. What's inside
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Daily affirmation cards for self-love, confidence and calm&lt;/li&gt;
&lt;li&gt;13 self-care topics: self-love, anxiety, overthinking, anger, confidence, self-doubt, forgiveness, letting go, loneliness, procrastination, honest speech, boundaries and sleep&lt;/li&gt;
&lt;li&gt;Short voice sessions (not advice, and not therapy)&lt;/li&gt;
&lt;li&gt;Free to download, with an optional subscription. iPhone only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;It's a self-care app, not medical advice. If you're in crisis, please contact a local emergency number or crisis line.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it / tell me what's wrong with it
&lt;/h2&gt;

&lt;p&gt;If you overthink at night, I'd genuinely like to know whether the timing feels right or just feels like another notification.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://apps.apple.com/app/id6755129214?pt=93486075&amp;amp;ct=devto_article1&amp;amp;mt=8" rel="noopener noreferrer"&gt;Anicca on the App Store&lt;/a&gt;&lt;/strong&gt; · &lt;a href="https://aniccaai.com/affirmation-app?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=anicca_en_organic&amp;amp;utm_content=article1" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Questions about the notification extension or the rating-prompt logic are welcome in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with the help of an AI assistant and reviewed before publishing. The app details are from the live App Store listing and our own code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>mentalhealth</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
