@khemsok/tunl 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -38
  2. package/dist/cli.js +625 -361
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -55096,129 +55096,97 @@ var useTerminalDimensions = () => {
55096
55096
  };
55097
55097
 
55098
55098
  // src/cli.tsx
55099
- import { execSync as execSync2 } from "child_process";
55099
+ import { execSync as execSync3 } from "child_process";
55100
55100
 
55101
55101
  // src/app.tsx
55102
- var import_react17 = __toESM(require_react(), 1);
55102
+ var import_react21 = __toESM(require_react(), 1);
55103
+
55104
+ // src/utils/time.ts
55105
+ function formatMinutes(totalMinutes) {
55106
+ const hours = Math.floor(totalMinutes / 60);
55107
+ const mins = totalMinutes % 60;
55108
+ if (hours > 0) {
55109
+ return `${hours}h ${mins}m`;
55110
+ }
55111
+ return `${mins}m`;
55112
+ }
55113
+ function formatTime(seconds) {
55114
+ const m2 = Math.floor(seconds / 60);
55115
+ const s = seconds % 60;
55116
+ return `${String(m2).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
55117
+ }
55118
+
55119
+ // src/theme.ts
55120
+ var COLORS = {
55121
+ text: "#CDD6F4",
55122
+ textBody: "#B8C0E0",
55123
+ textMuted: "#9399B2",
55124
+ textDim: "#7F849C",
55125
+ textDimmer: "#585B70",
55126
+ textLabel: "#6E738D",
55127
+ accent: "#7FDBCA",
55128
+ highlight: "#E0F0FF",
55129
+ white: "#FFFFFF",
55130
+ success: "#A6DA95",
55131
+ warning: "#EED49F",
55132
+ error: "#ED8796",
55133
+ orange: "#F5A97F",
55134
+ purple: "#C6A0F6",
55135
+ border: "#6C7086"
55136
+ };
55137
+ var CELEBRATION_COLORS = [
55138
+ COLORS.purple,
55139
+ COLORS.accent,
55140
+ COLORS.orange,
55141
+ COLORS.highlight,
55142
+ COLORS.warning
55143
+ ];
55103
55144
 
55104
55145
  // node_modules/@opentui/react/jsx-dev-runtime.js
55105
55146
  var import_jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
55106
55147
 
55107
55148
  // src/components/timer.tsx
55108
55149
  var DIGITS = {
55109
- "0": [
55110
- "\u2588\u2580\u2580\u2588",
55111
- "\u2588 \u2588",
55112
- "\u2588 \u2588",
55113
- "\u2588 \u2588",
55114
- "\u2588\u2584\u2584\u2588"
55115
- ],
55116
- "1": [
55117
- " \u2584\u2588 ",
55118
- " \u2588 ",
55119
- " \u2588 ",
55120
- " \u2588 ",
55121
- " \u2584\u2588\u2584"
55122
- ],
55123
- "2": [
55124
- "\u2588\u2580\u2580\u2588",
55125
- " \u2588",
55126
- "\u2588\u2580\u2580\u2580",
55127
- "\u2588 ",
55128
- "\u2588\u2584\u2584\u2584"
55129
- ],
55130
- "3": [
55131
- "\u2588\u2580\u2580\u2588",
55132
- " \u2588",
55133
- " \u2580\u2580\u2588",
55134
- " \u2588",
55135
- "\u2588\u2584\u2584\u2588"
55136
- ],
55137
- "4": [
55138
- "\u2588 \u2588",
55139
- "\u2588 \u2588",
55140
- "\u2580\u2580\u2580\u2588",
55141
- " \u2588",
55142
- " \u2588"
55143
- ],
55144
- "5": [
55145
- "\u2588\u2580\u2580\u2580",
55146
- "\u2588 ",
55147
- "\u2580\u2580\u2580\u2588",
55148
- " \u2588",
55149
- "\u2588\u2584\u2584\u2588"
55150
- ],
55151
- "6": [
55152
- "\u2588\u2580\u2580\u2580",
55153
- "\u2588 ",
55154
- "\u2588\u2580\u2580\u2588",
55155
- "\u2588 \u2588",
55156
- "\u2588\u2584\u2584\u2588"
55157
- ],
55158
- "7": [
55159
- "\u2588\u2580\u2580\u2588",
55160
- " \u2588",
55161
- " \u2588",
55162
- " \u2588 ",
55163
- " \u2588 "
55164
- ],
55165
- "8": [
55166
- "\u2588\u2580\u2580\u2588",
55167
- "\u2588 \u2588",
55168
- "\u2588\u2580\u2580\u2588",
55169
- "\u2588 \u2588",
55170
- "\u2588\u2584\u2584\u2588"
55171
- ],
55172
- "9": [
55173
- "\u2588\u2580\u2580\u2588",
55174
- "\u2588 \u2588",
55175
- "\u2580\u2580\u2580\u2588",
55176
- " \u2588",
55177
- "\u2588\u2584\u2584\u2588"
55178
- ],
55179
- ":": [
55180
- " ",
55181
- " \u2580\u2580 ",
55182
- " ",
55183
- " \u2580\u2580 ",
55184
- " "
55185
- ]
55150
+ "0": ["\u250C\u2500\u2510", "\u2502 \u2502", "\u2514\u2500\u2518"],
55151
+ "1": [" \u2500\u2510", " \u2502", " \u2500\u2518"],
55152
+ "2": ["\u250C\u2500\u2510", "\u250C\u2500\u2518", "\u2514\u2500 "],
55153
+ "3": ["\u250C\u2500\u2510", " \u2500\u2524", "\u2514\u2500\u2518"],
55154
+ "4": ["\u2502 \u2502", "\u2514\u2500\u2524", " \u2502"],
55155
+ "5": ["\u250C\u2500 ", "\u2514\u2500\u2510", "\u2500\u2500\u2518"],
55156
+ "6": ["\u250C\u2500 ", "\u251C\u2500\u2510", "\u2514\u2500\u2518"],
55157
+ "7": ["\u2500\u2500\u2510", " \u2502", " \u2502"],
55158
+ "8": ["\u250C\u2500\u2510", "\u251C\u2500\u2524", "\u2514\u2500\u2518"],
55159
+ "9": ["\u250C\u2500\u2510", "\u2514\u2500\u2524", " \u2502"],
55160
+ ":": [" ", " \xB7 ", " \xB7 "]
55186
55161
  };
55187
- function formatTime(seconds) {
55188
- const m2 = Math.floor(seconds / 60);
55189
- const s = seconds % 60;
55190
- return `${String(m2).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
55191
- }
55192
- function TimerDisplay({
55193
- remaining,
55194
- color,
55195
- animTick
55196
- }) {
55197
- const timeStr = formatTime(remaining);
55198
- const fg2 = color || "#E0F0FF";
55199
- const colonVisible = animTick === undefined || animTick % 2 === 0;
55200
- const colonLines = colonVisible ? DIGITS[":"] : [" ", " ", " ", " ", " "];
55201
- const lines = ["", "", "", "", ""];
55162
+ function getTimerLines(seconds) {
55163
+ const timeStr = formatTime(seconds);
55164
+ const lines = ["", "", ""];
55202
55165
  for (let i = 0;i < timeStr.length; i++) {
55203
55166
  const char = timeStr[i];
55204
- const digitLines = char === ":" ? colonLines : DIGITS[char] || DIGITS["0"];
55205
- for (let row = 0;row < 5; row++) {
55167
+ const digitLines = DIGITS[char] || DIGITS["0"];
55168
+ for (let row = 0;row < 3; row++) {
55206
55169
  lines[row] += digitLines[row] + " ";
55207
55170
  }
55208
55171
  }
55172
+ return lines;
55173
+ }
55174
+ function TimerDisplay({ remaining, color }) {
55175
+ const timerLines = getTimerLines(remaining);
55176
+ const fg2 = color || COLORS.text;
55209
55177
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
55210
55178
  justifyContent: "center",
55211
55179
  alignItems: "center",
55212
55180
  width: "100%",
55213
55181
  children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55214
55182
  fg: fg2,
55215
- children: lines.join(`
55183
+ children: timerLines.join(`
55216
55184
  `)
55217
55185
  }, undefined, false, undefined, this)
55218
55186
  }, undefined, false, undefined, this);
55219
55187
  }
55220
55188
 
55221
- // src/components/progress-bar.tsx
55189
+ // src/utils/colors.ts
55222
55190
  function interpolateColor(color1, color2, t2) {
55223
55191
  const r = Math.round(color1[0] + (color2[0] - color1[0]) * t2);
55224
55192
  const g2 = Math.round(color1[1] + (color2[1] - color1[1]) * t2);
@@ -55241,10 +55209,9 @@ function getGradientColor(progress) {
55241
55209
  }
55242
55210
  return interpolateColor(STOPS[STOPS.length - 2].color, STOPS[STOPS.length - 1].color, 1);
55243
55211
  }
55244
- function ProgressBar({
55245
- progress,
55246
- width
55247
- }) {
55212
+
55213
+ // src/components/progress-bar.tsx
55214
+ function ProgressBar({ progress, width }) {
55248
55215
  const barWidth = Math.max(width - 10, 20);
55249
55216
  const filledCount = Math.round(progress * barWidth);
55250
55217
  const emptyCount = barWidth - filledCount;
@@ -55263,11 +55230,11 @@ function ProgressBar({
55263
55230
  children: filled
55264
55231
  }, undefined, false, undefined, this),
55265
55232
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55266
- fg: "#585B70",
55233
+ fg: COLORS.textDimmer,
55267
55234
  children: empty
55268
55235
  }, undefined, false, undefined, this),
55269
55236
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55270
- fg: "#9399B2",
55237
+ fg: COLORS.textMuted,
55271
55238
  children: ` ${String(percent).padStart(3)}%`
55272
55239
  }, undefined, false, undefined, this)
55273
55240
  ]
@@ -55561,13 +55528,14 @@ var cityTheme = {
55561
55528
  };
55562
55529
 
55563
55530
  // src/components/art-canvas.tsx
55531
+ var FULL_CYCLE_TICKS = 375;
55564
55532
  function ArtCanvas({
55565
55533
  stage,
55566
55534
  theme,
55567
55535
  animTick
55568
55536
  }) {
55569
- const progress = stage / Math.max(NUM_STAGES - 1, 1);
55570
- const artData = theme.generate ? theme.generate(progress, animTick, 70) : theme.stages[Math.min(stage, theme.stages.length - 1)];
55537
+ const tickProgress = Math.min(animTick / FULL_CYCLE_TICKS, 1);
55538
+ const artData = theme.generate ? theme.generate(tickProgress, animTick, 70) : theme.stages[Math.min(Math.floor(tickProgress * (NUM_STAGES - 1)), theme.stages.length - 1)];
55571
55539
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
55572
55540
  flexGrow: 1,
55573
55541
  width: "100%",
@@ -55603,7 +55571,7 @@ function StatusBar({
55603
55571
  width: "100%",
55604
55572
  borderStyle: "single",
55605
55573
  border: ["top"],
55606
- borderColor: "#6C7086",
55574
+ borderColor: COLORS.border,
55607
55575
  children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
55608
55576
  justifyContent: "center",
55609
55577
  alignItems: "center",
@@ -55611,77 +55579,89 @@ function StatusBar({
55611
55579
  children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55612
55580
  children: [
55613
55581
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55614
- fg: "#9399B2",
55582
+ fg: COLORS.textMuted,
55615
55583
  children: "q"
55616
55584
  }, undefined, false, undefined, this),
55617
55585
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55618
- fg: "#7F849C",
55586
+ fg: COLORS.textDim,
55619
55587
  children: " quit"
55620
55588
  }, undefined, false, undefined, this),
55621
55589
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55622
- fg: "#6C7086",
55590
+ fg: COLORS.border,
55623
55591
  children: " \xB7 "
55624
55592
  }, undefined, false, undefined, this),
55625
55593
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55626
- fg: "#9399B2",
55594
+ fg: COLORS.textMuted,
55627
55595
  children: "space"
55628
55596
  }, undefined, false, undefined, this),
55629
55597
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55630
- fg: "#7F849C",
55598
+ fg: COLORS.textDim,
55631
55599
  children: timerStatus === "idle" ? " start" : timerStatus === "running" ? " pause" : timerStatus === "paused" ? " resume" : " restart"
55632
55600
  }, undefined, false, undefined, this),
55633
55601
  timerStatus === "idle" && /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(import_jsx_dev_runtime2.Fragment, {
55634
55602
  children: [
55635
55603
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55636
- fg: "#6C7086",
55604
+ fg: COLORS.border,
55637
55605
  children: " \xB7 "
55638
55606
  }, undefined, false, undefined, this),
55639
55607
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55640
- fg: "#9399B2",
55608
+ fg: COLORS.textMuted,
55641
55609
  children: "+/-"
55642
55610
  }, undefined, false, undefined, this),
55643
55611
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55644
- fg: "#7F849C",
55612
+ fg: COLORS.textDim,
55645
55613
  children: " time"
55646
55614
  }, undefined, false, undefined, this),
55647
55615
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55648
- fg: "#6C7086",
55616
+ fg: COLORS.border,
55649
55617
  children: " \xB7 "
55650
55618
  }, undefined, false, undefined, this),
55651
55619
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55652
- fg: "#9399B2",
55620
+ fg: COLORS.textMuted,
55653
55621
  children: "s"
55654
55622
  }, undefined, false, undefined, this),
55655
55623
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55656
- fg: "#7F849C",
55624
+ fg: COLORS.textDim,
55657
55625
  children: " sites"
55658
55626
  }, undefined, false, undefined, this),
55659
55627
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55660
- fg: "#6C7086",
55628
+ fg: COLORS.border,
55661
55629
  children: " \xB7 "
55662
55630
  }, undefined, false, undefined, this),
55663
55631
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55664
- fg: "#9399B2",
55632
+ fg: COLORS.textMuted,
55665
55633
  children: "t"
55666
55634
  }, undefined, false, undefined, this),
55667
55635
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55668
- fg: "#7F849C",
55636
+ fg: COLORS.textDim,
55669
55637
  children: " theme"
55638
+ }, undefined, false, undefined, this),
55639
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55640
+ fg: COLORS.border,
55641
+ children: " \xB7 "
55642
+ }, undefined, false, undefined, this),
55643
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55644
+ fg: COLORS.textMuted,
55645
+ children: "i"
55646
+ }, undefined, false, undefined, this),
55647
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55648
+ fg: COLORS.textDim,
55649
+ children: " stats"
55670
55650
  }, undefined, false, undefined, this)
55671
55651
  ]
55672
55652
  }, undefined, true, undefined, this),
55673
55653
  (timerStatus === "running" || timerStatus === "paused") && /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(import_jsx_dev_runtime2.Fragment, {
55674
55654
  children: [
55675
55655
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55676
- fg: "#6C7086",
55656
+ fg: COLORS.border,
55677
55657
  children: " \xB7 "
55678
55658
  }, undefined, false, undefined, this),
55679
55659
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55680
- fg: "#9399B2",
55660
+ fg: COLORS.textMuted,
55681
55661
  children: "r"
55682
55662
  }, undefined, false, undefined, this),
55683
55663
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55684
- fg: "#7F849C",
55664
+ fg: COLORS.textDim,
55685
55665
  children: " stop"
55686
55666
  }, undefined, false, undefined, this)
55687
55667
  ]
@@ -55689,11 +55669,11 @@ function StatusBar({
55689
55669
  timerStatus === "running" && blockedCount > 0 && /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(import_jsx_dev_runtime2.Fragment, {
55690
55670
  children: [
55691
55671
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55692
- fg: "#6C7086",
55672
+ fg: COLORS.border,
55693
55673
  children: " \xB7 "
55694
55674
  }, undefined, false, undefined, this),
55695
55675
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55696
- fg: "#ED8796",
55676
+ fg: COLORS.error,
55697
55677
  children: [
55698
55678
  "\u2715 ",
55699
55679
  blockedCount,
@@ -55708,6 +55688,50 @@ function StatusBar({
55708
55688
  }, undefined, false, undefined, this);
55709
55689
  }
55710
55690
 
55691
+ // src/components/stats-screen.tsx
55692
+ function StatsScreen({ config }) {
55693
+ const timeStr = formatMinutes(config.totalMinutesFocused);
55694
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
55695
+ flexDirection: "column",
55696
+ width: "100%",
55697
+ height: "100%",
55698
+ alignItems: "center",
55699
+ justifyContent: "center",
55700
+ children: [
55701
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55702
+ fg: COLORS.accent,
55703
+ children: "focus stats"
55704
+ }, undefined, false, undefined, this),
55705
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
55706
+ height: 2
55707
+ }, undefined, false, undefined, this),
55708
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55709
+ fg: COLORS.text,
55710
+ children: " sessions " + config.totalSessions
55711
+ }, undefined, false, undefined, this),
55712
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55713
+ fg: COLORS.text,
55714
+ children: " total focused " + timeStr
55715
+ }, undefined, false, undefined, this),
55716
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55717
+ fg: COLORS.text,
55718
+ children: " day streak " + config.currentStreak
55719
+ }, undefined, false, undefined, this),
55720
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55721
+ fg: COLORS.text,
55722
+ children: " last session " + (config.lastSessionDate || "never")
55723
+ }, undefined, false, undefined, this),
55724
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
55725
+ height: 2
55726
+ }, undefined, false, undefined, this),
55727
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55728
+ fg: COLORS.textMuted,
55729
+ children: "esc to go back"
55730
+ }, undefined, false, undefined, this)
55731
+ ]
55732
+ }, undefined, true, undefined, this);
55733
+ }
55734
+
55711
55735
  // src/components/onboarding.tsx
55712
55736
  var import_react11 = __toESM(require_react(), 1);
55713
55737
 
@@ -55741,7 +55765,11 @@ var DEFAULTS = {
55741
55765
  duration: 25,
55742
55766
  blockedSites: DEFAULT_SITES,
55743
55767
  theme: "city",
55744
- noblock: false
55768
+ noblock: false,
55769
+ totalSessions: 0,
55770
+ totalMinutesFocused: 0,
55771
+ lastSessionDate: "",
55772
+ currentStreak: 0
55745
55773
  };
55746
55774
  function loadConfig() {
55747
55775
  if (!existsSync3(CONFIG_PATH)) {
@@ -55796,7 +55824,7 @@ function Onboarding({
55796
55824
  height: "100%",
55797
55825
  children: [
55798
55826
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55799
- fg: "#7FDBCA",
55827
+ fg: COLORS.accent,
55800
55828
  children: LOGO.join(`
55801
55829
  `)
55802
55830
  }, undefined, false, undefined, this),
@@ -55804,14 +55832,14 @@ function Onboarding({
55804
55832
  height: 1
55805
55833
  }, undefined, false, undefined, this),
55806
55834
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55807
- fg: "#6E738D",
55835
+ fg: COLORS.textBody,
55808
55836
  children: TAGLINE
55809
55837
  }, undefined, false, undefined, this),
55810
55838
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
55811
55839
  height: 2
55812
55840
  }, undefined, false, undefined, this),
55813
55841
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55814
- fg: "#494D64",
55842
+ fg: COLORS.textMuted,
55815
55843
  children: "press space to begin setup"
55816
55844
  }, undefined, false, undefined, this)
55817
55845
  ]
@@ -55826,21 +55854,21 @@ function Onboarding({
55826
55854
  height: "100%",
55827
55855
  children: [
55828
55856
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55829
- fg: "#7FDBCA",
55857
+ fg: COLORS.accent,
55830
55858
  children: "sites to block during focus:"
55831
55859
  }, undefined, false, undefined, this),
55832
55860
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
55833
55861
  height: 1
55834
55862
  }, undefined, false, undefined, this),
55835
55863
  DEFAULT_SITES.map((site, i) => /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55836
- fg: "#EED49F",
55864
+ fg: COLORS.warning,
55837
55865
  children: " \u2713 " + site
55838
55866
  }, i, false, undefined, this)),
55839
55867
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
55840
55868
  height: 2
55841
55869
  }, undefined, false, undefined, this),
55842
55870
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55843
- fg: "#494D64",
55871
+ fg: COLORS.textMuted,
55844
55872
  children: "press space to confirm \xB7 use --block to add more"
55845
55873
  }, undefined, false, undefined, this)
55846
55874
  ]
@@ -55854,7 +55882,7 @@ function Onboarding({
55854
55882
  height: "100%",
55855
55883
  children: [
55856
55884
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55857
- fg: "#7FDBCA",
55885
+ fg: COLORS.accent,
55858
55886
  children: "focus duration:"
55859
55887
  }, undefined, false, undefined, this),
55860
55888
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
@@ -55863,17 +55891,17 @@ function Onboarding({
55863
55891
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("ascii-font", {
55864
55892
  text: String(duration),
55865
55893
  font: "block",
55866
- color: "#E0F0FF"
55894
+ color: COLORS.highlight
55867
55895
  }, undefined, false, undefined, this),
55868
55896
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55869
- fg: "#6E738D",
55897
+ fg: COLORS.textLabel,
55870
55898
  children: "minutes"
55871
55899
  }, undefined, false, undefined, this),
55872
55900
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
55873
55901
  height: 2
55874
55902
  }, undefined, false, undefined, this),
55875
55903
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55876
- fg: "#494D64",
55904
+ fg: COLORS.textMuted,
55877
55905
  children: "\u2191/\u2193 to adjust \xB7 space to start"
55878
55906
  }, undefined, false, undefined, this)
55879
55907
  ]
@@ -55956,14 +55984,14 @@ function SiteEditor({
55956
55984
  justifyContent: "center",
55957
55985
  children: [
55958
55986
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55959
- fg: "#7FDBCA",
55987
+ fg: COLORS.accent,
55960
55988
  children: "edit blocked sites"
55961
55989
  }, undefined, false, undefined, this),
55962
55990
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
55963
55991
  height: 1
55964
55992
  }, undefined, false, undefined, this),
55965
55993
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55966
- fg: "#B8C0E0",
55994
+ fg: COLORS.textBody,
55967
55995
  children: "\u2191/\u2193 navigate \xB7 space toggle \xB7 enter save \xB7 esc cancel"
55968
55996
  }, undefined, false, undefined, this),
55969
55997
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
@@ -55973,8 +56001,8 @@ function SiteEditor({
55973
56001
  const isEnabled = enabled.has(site);
55974
56002
  const isCursor = i === cursor;
55975
56003
  const prefix = isEnabled ? "\u2713" : "\u25CB";
55976
- const prefixColor = isEnabled ? "#A6DA95" : "#9399B2";
55977
- const textColor = isCursor ? "#FFFFFF" : "#B8C0E0";
56004
+ const prefixColor = isEnabled ? COLORS.success : COLORS.textMuted;
56005
+ const textColor = isCursor ? COLORS.white : COLORS.textBody;
55978
56006
  const indicator = isCursor ? " \u25C0" : "";
55979
56007
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55980
56008
  children: [
@@ -55987,7 +56015,7 @@ function SiteEditor({
55987
56015
  children: site
55988
56016
  }, undefined, false, undefined, this),
55989
56017
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55990
- fg: "#7FDBCA",
56018
+ fg: COLORS.accent,
55991
56019
  children: indicator
55992
56020
  }, undefined, false, undefined, this)
55993
56021
  ]
@@ -55996,11 +56024,11 @@ function SiteEditor({
55996
56024
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
55997
56025
  children: [
55998
56026
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
55999
- fg: cursor === allDisplaySites.length ? "#FFFFFF" : "#B8C0E0",
56027
+ fg: cursor === allDisplaySites.length ? COLORS.white : COLORS.textBody,
56000
56028
  children: " + add new site"
56001
56029
  }, undefined, false, undefined, this),
56002
56030
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
56003
- fg: "#7FDBCA",
56031
+ fg: COLORS.accent,
56004
56032
  children: cursor === allDisplaySites.length ? " \u25C0" : ""
56005
56033
  }, undefined, false, undefined, this)
56006
56034
  ]
@@ -56013,15 +56041,15 @@ function SiteEditor({
56013
56041
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
56014
56042
  children: [
56015
56043
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
56016
- fg: "#B8C0E0",
56044
+ fg: COLORS.textBody,
56017
56045
  children: " site: "
56018
56046
  }, undefined, false, undefined, this),
56019
56047
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
56020
- fg: "#FFFFFF",
56048
+ fg: COLORS.white,
56021
56049
  children: inputBuffer
56022
56050
  }, undefined, false, undefined, this),
56023
56051
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
56024
- fg: "#7FDBCA",
56052
+ fg: COLORS.accent,
56025
56053
  children: "_"
56026
56054
  }, undefined, false, undefined, this)
56027
56055
  ]
@@ -56032,7 +56060,7 @@ function SiteEditor({
56032
56060
  height: 1
56033
56061
  }, undefined, false, undefined, this),
56034
56062
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
56035
- fg: "#9399B2",
56063
+ fg: COLORS.textMuted,
56036
56064
  children: `${enabled.size} site${enabled.size !== 1 ? "s" : ""} selected`
56037
56065
  }, undefined, false, undefined, this)
56038
56066
  ]
@@ -56073,14 +56101,14 @@ function ThemePicker({
56073
56101
  justifyContent: "center",
56074
56102
  children: [
56075
56103
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
56076
- fg: "#7FDBCA",
56104
+ fg: COLORS.accent,
56077
56105
  children: "choose art theme"
56078
56106
  }, undefined, false, undefined, this),
56079
56107
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
56080
56108
  height: 1
56081
56109
  }, undefined, false, undefined, this),
56082
56110
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
56083
- fg: "#B8C0E0",
56111
+ fg: COLORS.textBody,
56084
56112
  children: "\u2191/\u2193 navigate \xB7 space select \xB7 esc cancel"
56085
56113
  }, undefined, false, undefined, this),
56086
56114
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
@@ -56090,8 +56118,8 @@ function ThemePicker({
56090
56118
  const isCurrent = theme.name === currentTheme;
56091
56119
  const isCursor = i === cursor;
56092
56120
  const prefix = isCurrent ? "\u25C9" : "\u25CB";
56093
- const prefixColor = isCurrent ? "#7FDBCA" : "#9399B2";
56094
- const textColor = isCursor ? "#FFFFFF" : "#B8C0E0";
56121
+ const prefixColor = isCurrent ? COLORS.accent : COLORS.textMuted;
56122
+ const textColor = isCursor ? COLORS.white : COLORS.textBody;
56095
56123
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
56096
56124
  flexDirection: "column",
56097
56125
  alignItems: "center",
@@ -56107,13 +56135,13 @@ function ThemePicker({
56107
56135
  children: theme.name
56108
56136
  }, undefined, false, undefined, this),
56109
56137
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
56110
- fg: "#7FDBCA",
56138
+ fg: COLORS.accent,
56111
56139
  children: isCursor ? " \u25C0" : ""
56112
56140
  }, undefined, false, undefined, this)
56113
56141
  ]
56114
56142
  }, undefined, true, undefined, this),
56115
56143
  isCursor && /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
56116
- fg: "#7F849C",
56144
+ fg: COLORS.textDim,
56117
56145
  children: " " + (DESCRIPTIONS[theme.name] || "")
56118
56146
  }, undefined, false, undefined, this)
56119
56147
  ]
@@ -56644,7 +56672,26 @@ var spaceTheme = {
56644
56672
  generate: generateSpace
56645
56673
  };
56646
56674
 
56647
- // src/blocker.ts
56675
+ // src/lib/session.ts
56676
+ function recordSession(durationMinutes) {
56677
+ const config = loadConfig();
56678
+ const today = new Date().toISOString().split("T")[0];
56679
+ const yesterday = new Date(Date.now() - 86400000).toISOString().split("T")[0];
56680
+ let streak = config.currentStreak;
56681
+ if (config.lastSessionDate === today) {} else if (config.lastSessionDate === yesterday) {
56682
+ streak += 1;
56683
+ } else {
56684
+ streak = 1;
56685
+ }
56686
+ saveConfig({
56687
+ totalSessions: config.totalSessions + 1,
56688
+ totalMinutesFocused: config.totalMinutesFocused + durationMinutes,
56689
+ lastSessionDate: today,
56690
+ currentStreak: streak
56691
+ });
56692
+ }
56693
+
56694
+ // src/lib/blocker.ts
56648
56695
  import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync } from "fs";
56649
56696
  import { execSync } from "child_process";
56650
56697
  import { homedir as homedir2 } from "os";
@@ -56656,49 +56703,64 @@ var PID_PATH = join3(homedir2(), ".tunl.pid");
56656
56703
  function validateSite(site) {
56657
56704
  return /^[a-zA-Z0-9.-]+$/.test(site);
56658
56705
  }
56659
- function blockSites(sites) {
56660
- cleanupStaleBlocks();
56661
- const validSites = sites.filter(validateSite);
56662
- if (validSites.length === 0)
56663
- return;
56664
- writeFileSync2(PID_PATH, String(process.pid));
56665
- const entries = validSites.flatMap((s) => [`127.0.0.1 ${s}`, `127.0.0.1 www.${s}`]).join(`
56706
+ function blockHosts(sites) {
56707
+ unblockHosts();
56708
+ const entries = sites.flatMap((s) => [
56709
+ `0.0.0.0 ${s}`,
56710
+ `0.0.0.0 www.${s}`,
56711
+ `:: ${s}`,
56712
+ `:: www.${s}`
56713
+ ]).join(`
56666
56714
  `);
56667
- const block = `
56715
+ const currentHosts = readFileSync2(HOSTS_PATH, "utf-8").trimEnd();
56716
+ const block = `${currentHosts}
56668
56717
  ${START_MARKER}
56669
56718
  ${entries}
56670
56719
  ${END_MARKER}
56671
56720
  `;
56672
56721
  const tmpPath = join3(homedir2(), ".tunl-block.tmp");
56673
56722
  writeFileSync2(tmpPath, block);
56674
- execSync(`sudo sh -c 'cat "${tmpPath}" >> ${HOSTS_PATH}'`);
56723
+ execSync(`sudo cp "${tmpPath}" ${HOSTS_PATH}`);
56675
56724
  unlinkSync(tmpPath);
56676
- try {
56677
- execSync("sudo dscacheutil -flushcache 2>/dev/null");
56678
- } catch {}
56679
- try {
56680
- execSync("sudo killall -HUP mDNSResponder 2>/dev/null");
56681
- } catch {}
56682
56725
  }
56683
- function unblockSites() {
56726
+ function unblockHosts() {
56684
56727
  try {
56685
56728
  const hosts = readFileSync2(HOSTS_PATH, "utf-8");
56686
56729
  const startIdx = hosts.indexOf(START_MARKER);
56687
56730
  const endIdx = hosts.indexOf(END_MARKER);
56688
56731
  if (startIdx === -1 || endIdx === -1)
56689
56732
  return;
56690
- const cleaned = hosts.slice(0, startIdx) + hosts.slice(endIdx + END_MARKER.length);
56733
+ const cleaned = (hosts.slice(0, startIdx) + hosts.slice(endIdx + END_MARKER.length)).trimEnd() + `
56734
+ `;
56691
56735
  const tmpPath = join3(homedir2(), ".tunl-hosts.tmp");
56692
56736
  writeFileSync2(tmpPath, cleaned);
56693
56737
  execSync(`sudo cp "${tmpPath}" ${HOSTS_PATH}`);
56694
56738
  unlinkSync(tmpPath);
56695
- try {
56696
- execSync("sudo dscacheutil -flushcache 2>/dev/null");
56697
- } catch {}
56698
- try {
56699
- execSync("sudo killall -HUP mDNSResponder 2>/dev/null");
56700
- } catch {}
56739
+ } catch {}
56740
+ }
56741
+ function flushDNS() {
56742
+ try {
56743
+ execSync("sudo dscacheutil -flushcache 2>/dev/null");
56744
+ } catch {}
56745
+ try {
56746
+ execSync("sudo killall -HUP mDNSResponder 2>/dev/null");
56701
56747
  } catch {}
56748
+ try {
56749
+ execSync("sudo killall mDNSResponderHelper 2>/dev/null");
56750
+ } catch {}
56751
+ }
56752
+ function blockSites(sites) {
56753
+ cleanupStaleBlocks();
56754
+ const validSites = sites.filter(validateSite);
56755
+ if (validSites.length === 0)
56756
+ return;
56757
+ writeFileSync2(PID_PATH, String(process.pid));
56758
+ blockHosts(validSites);
56759
+ flushDNS();
56760
+ }
56761
+ function unblockSites() {
56762
+ unblockHosts();
56763
+ flushDNS();
56702
56764
  try {
56703
56765
  if (existsSync5(PID_PATH))
56704
56766
  unlinkSync(PID_PATH);
@@ -56714,8 +56776,21 @@ function cleanupStaleBlocks() {
56714
56776
  unblockSites();
56715
56777
  }
56716
56778
  }
56779
+
56780
+ // src/lib/terminal.ts
56781
+ function destroyRenderer() {
56782
+ const renderer = globalThis.__tunl_renderer;
56783
+ if (renderer) {
56784
+ try {
56785
+ renderer.destroy();
56786
+ } catch {}
56787
+ globalThis.__tunl_renderer = null;
56788
+ }
56789
+ process.stdout.write("\x1B[?25h\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?1049l");
56790
+ }
56717
56791
  function registerCleanup() {
56718
56792
  const cleanup = () => {
56793
+ destroyRenderer();
56719
56794
  try {
56720
56795
  unblockSites();
56721
56796
  } catch {}
@@ -56723,7 +56798,9 @@ function registerCleanup() {
56723
56798
  };
56724
56799
  process.on("SIGINT", cleanup);
56725
56800
  process.on("SIGTERM", cleanup);
56801
+ process.on("exit", destroyRenderer);
56726
56802
  process.on("uncaughtException", (err) => {
56803
+ destroyRenderer();
56727
56804
  try {
56728
56805
  unblockSites();
56729
56806
  } catch {}
@@ -56732,37 +56809,12 @@ function registerCleanup() {
56732
56809
  });
56733
56810
  }
56734
56811
 
56735
- // src/app.tsx
56736
- var ALL_THEMES = [cityTheme, forestTheme, spaceTheme];
56737
- function App({
56738
- initialDuration,
56739
- noblock,
56740
- extraBlocks
56741
- }) {
56742
- const config = loadConfig();
56743
- const [screen, setScreen] = import_react17.useState(config.isFirstRun ? "onboarding" : "timer");
56812
+ // src/hooks/use-timer.ts
56813
+ var import_react17 = __toESM(require_react(), 1);
56814
+ function useTimer(initialSeconds, onFinish) {
56744
56815
  const [timerStatus, setTimerStatus] = import_react17.useState("idle");
56745
- const [totalSeconds, setTotalSeconds] = import_react17.useState((initialDuration || config.duration) * 60);
56746
- const [remainingSeconds, setRemainingSeconds] = import_react17.useState((initialDuration || config.duration) * 60);
56747
- const [artStage, setArtStage] = import_react17.useState(0);
56748
- const [blockedSites, setBlockedSites] = import_react17.useState(config.blockedSites);
56749
- const [isBlocking, setIsBlocking] = import_react17.useState(false);
56750
- const [celebrationColor, setCelebrationColor] = import_react17.useState("#E0F0FF");
56751
- const [blockError, setBlockError] = import_react17.useState(null);
56752
- const [animTick, setAnimTick] = import_react17.useState(0);
56753
- const [currentTheme, setCurrentTheme] = import_react17.useState(ALL_THEMES.find((t2) => t2.name === config.theme) || cityTheme);
56754
- const { width } = useTerminalDimensions();
56755
- import_react17.useEffect(() => {
56756
- registerCleanup();
56757
- }, []);
56758
- import_react17.useEffect(() => {
56759
- if (screen === "onboarding" || screen === "sites" || screen === "themes")
56760
- return;
56761
- const interval = setInterval(() => {
56762
- setAnimTick((t2) => t2 + 1);
56763
- }, 800);
56764
- return () => clearInterval(interval);
56765
- }, [screen]);
56816
+ const [totalSeconds, setTotalSeconds] = import_react17.useState(initialSeconds);
56817
+ const [remainingSeconds, setRemainingSeconds] = import_react17.useState(initialSeconds);
56766
56818
  import_react17.useEffect(() => {
56767
56819
  if (timerStatus !== "running")
56768
56820
  return;
@@ -56771,131 +56823,219 @@ function App({
56771
56823
  if (prev <= 1) {
56772
56824
  clearInterval(interval);
56773
56825
  setTimerStatus("finished");
56774
- setArtStage(NUM_STAGES - 1);
56775
- setScreen("completed");
56776
- if (isBlocking) {
56777
- try {
56778
- unblockSites();
56779
- } catch {}
56780
- setIsBlocking(false);
56781
- }
56826
+ onFinish();
56782
56827
  return 0;
56783
56828
  }
56784
- const next = prev - 1;
56785
- const elapsed = totalSeconds - next;
56786
- const progress2 = elapsed / totalSeconds;
56787
- const easedProgress = Math.sqrt(progress2);
56788
- const newStage = Math.min(Math.floor(easedProgress * NUM_STAGES), NUM_STAGES - 1);
56789
- setArtStage(newStage);
56790
- return next;
56829
+ return prev - 1;
56791
56830
  });
56792
56831
  }, 1000);
56793
56832
  return () => clearInterval(interval);
56794
- }, [timerStatus, totalSeconds]);
56795
- import_react17.useEffect(() => {
56796
- if (screen !== "completed")
56833
+ }, [timerStatus]);
56834
+ function adjustDuration(deltaSeconds) {
56835
+ const newTotal = Math.max(totalSeconds + deltaSeconds, 60);
56836
+ setTotalSeconds(newTotal);
56837
+ setRemainingSeconds(newTotal);
56838
+ }
56839
+ function resetTimer() {
56840
+ setTimerStatus("idle");
56841
+ setRemainingSeconds(totalSeconds);
56842
+ }
56843
+ function setDuration(seconds) {
56844
+ setTotalSeconds(seconds);
56845
+ setRemainingSeconds(seconds);
56846
+ }
56847
+ return {
56848
+ timerStatus,
56849
+ totalSeconds,
56850
+ remainingSeconds,
56851
+ setTimerStatus,
56852
+ adjustDuration,
56853
+ resetTimer,
56854
+ setDuration
56855
+ };
56856
+ }
56857
+
56858
+ // src/hooks/use-animation.ts
56859
+ var import_react18 = __toESM(require_react(), 1);
56860
+ function useAnimation(timerStatus) {
56861
+ const [animTick, setAnimTick] = import_react18.useState(0);
56862
+ import_react18.useEffect(() => {
56863
+ if (timerStatus !== "running" && timerStatus !== "paused")
56864
+ return;
56865
+ const interval = setInterval(() => {
56866
+ setAnimTick((t2) => t2 + 1);
56867
+ }, 800);
56868
+ return () => clearInterval(interval);
56869
+ }, [timerStatus]);
56870
+ function resetAnimation() {
56871
+ setAnimTick(0);
56872
+ }
56873
+ return { animTick, resetAnimation };
56874
+ }
56875
+
56876
+ // src/hooks/use-celebration.ts
56877
+ var import_react19 = __toESM(require_react(), 1);
56878
+ function useCelebration(active) {
56879
+ const [celebrationColor, setCelebrationColor] = import_react19.useState(COLORS.highlight);
56880
+ import_react19.useEffect(() => {
56881
+ if (!active)
56797
56882
  return;
56798
- const colors = ["#C6A0F6", "#7FDBCA", "#F5A97F", "#E0F0FF", "#EED49F"];
56799
56883
  let idx = 0;
56800
56884
  const interval = setInterval(() => {
56801
- idx = (idx + 1) % colors.length;
56802
- setCelebrationColor(colors[idx]);
56885
+ idx = (idx + 1) % CELEBRATION_COLORS.length;
56886
+ setCelebrationColor(CELEBRATION_COLORS[idx]);
56803
56887
  }, 400);
56804
56888
  return () => clearInterval(interval);
56805
- }, [screen]);
56806
- const startSession = () => {
56889
+ }, [active]);
56890
+ return celebrationColor;
56891
+ }
56892
+
56893
+ // src/hooks/use-blocker.ts
56894
+ var import_react20 = __toESM(require_react(), 1);
56895
+ function useBlocker() {
56896
+ const [isBlocking, setIsBlocking] = import_react20.useState(false);
56897
+ const [blockError, setBlockError] = import_react20.useState(null);
56898
+ function startBlocking(sites) {
56899
+ try {
56900
+ blockSites(sites);
56901
+ setIsBlocking(true);
56902
+ setBlockError(null);
56903
+ } catch (err) {
56904
+ setBlockError(err?.message || "Failed to block sites");
56905
+ }
56906
+ }
56907
+ function stopBlocking() {
56908
+ if (!isBlocking)
56909
+ return;
56910
+ try {
56911
+ unblockSites();
56912
+ } catch {}
56913
+ setIsBlocking(false);
56914
+ }
56915
+ return { isBlocking, blockError, startBlocking, stopBlocking };
56916
+ }
56917
+
56918
+ // src/app.tsx
56919
+ var ALL_THEMES = [cityTheme, forestTheme, spaceTheme];
56920
+ function App({ initialDuration, noblock, extraBlocks }) {
56921
+ const [config, setConfig] = import_react21.useState(() => loadConfig());
56922
+ const [screen, setScreen] = import_react21.useState(config.isFirstRun ? "onboarding" : "timer");
56923
+ const [blockedSites, setBlockedSites] = import_react21.useState(config.blockedSites);
56924
+ const [currentTheme, setCurrentTheme] = import_react21.useState(ALL_THEMES.find((t2) => t2.name === config.theme) || cityTheme);
56925
+ const initialSeconds = (initialDuration || config.duration) * 60;
56926
+ const { isBlocking, blockError, startBlocking, stopBlocking } = useBlocker();
56927
+ const timer = useTimer(initialSeconds, () => {
56928
+ setScreen("completed");
56929
+ recordSession(Math.round(timer.totalSeconds / 60));
56930
+ setConfig(loadConfig());
56931
+ stopBlocking();
56932
+ });
56933
+ const { animTick, resetAnimation } = useAnimation(timer.timerStatus);
56934
+ const celebrationColor = useCelebration(screen === "completed");
56935
+ const { width } = useTerminalDimensions();
56936
+ import_react21.useEffect(() => {
56937
+ registerCleanup();
56938
+ }, []);
56939
+ function startSession() {
56807
56940
  if (!noblock && blockedSites.length > 0) {
56808
- try {
56809
- const sites = [...blockedSites, ...extraBlocks || []];
56810
- blockSites(sites);
56811
- setIsBlocking(true);
56812
- setBlockError(null);
56813
- } catch (err) {
56814
- setBlockError(err?.message || "Failed to block sites");
56815
- }
56941
+ const sites = [...blockedSites, ...extraBlocks || []];
56942
+ startBlocking(sites);
56816
56943
  }
56817
- setTimerStatus("running");
56818
- };
56944
+ timer.setTimerStatus("running");
56945
+ }
56946
+ function resetSession() {
56947
+ stopBlocking();
56948
+ timer.resetTimer();
56949
+ resetAnimation();
56950
+ }
56819
56951
  useKeyboard((key) => {
56820
56952
  if (key.ctrl && key.name === "c") {
56821
- if (isBlocking) {
56822
- try {
56823
- unblockSites();
56824
- } catch {}
56825
- }
56953
+ stopBlocking();
56826
56954
  process.exit(0);
56827
56955
  }
56828
- if (screen === "onboarding" || screen === "sites" || screen === "themes")
56956
+ if (screen === "stats") {
56957
+ if (key.name === "escape" || key.name === "q" || key.name === "i") {
56958
+ setScreen("timer");
56959
+ }
56829
56960
  return;
56961
+ }
56962
+ if (screen === "onboarding" || screen === "sites" || screen === "themes") {
56963
+ return;
56964
+ }
56830
56965
  if (key.name === "q") {
56831
- if (isBlocking) {
56832
- try {
56833
- unblockSites();
56834
- } catch {}
56835
- }
56966
+ stopBlocking();
56836
56967
  process.exit(0);
56837
56968
  }
56838
56969
  if (screen === "timer") {
56839
- if (key.name === "s" && timerStatus === "idle") {
56970
+ handleTimerKeys(key);
56971
+ }
56972
+ if (screen === "completed" && key.name === "space") {
56973
+ timer.resetTimer();
56974
+ resetAnimation();
56975
+ setScreen("timer");
56976
+ }
56977
+ });
56978
+ function handleTimerKeys(key) {
56979
+ if (timer.timerStatus === "idle") {
56980
+ if (key.name === "s") {
56840
56981
  setScreen("sites");
56841
56982
  return;
56842
56983
  }
56843
- if (key.name === "t" && timerStatus === "idle") {
56984
+ if (key.name === "t") {
56844
56985
  setScreen("themes");
56845
56986
  return;
56846
56987
  }
56988
+ if (key.name === "i") {
56989
+ setScreen("stats");
56990
+ return;
56991
+ }
56847
56992
  if (key.name === "space") {
56848
- if (timerStatus === "idle") {
56849
- startSession();
56850
- } else if (timerStatus === "running") {
56851
- setTimerStatus("paused");
56852
- } else if (timerStatus === "paused") {
56853
- setTimerStatus("running");
56854
- }
56993
+ startSession();
56994
+ return;
56855
56995
  }
56856
- if (key.name === "r" && (timerStatus === "running" || timerStatus === "paused")) {
56857
- if (isBlocking) {
56858
- try {
56859
- unblockSites();
56860
- } catch {}
56861
- setIsBlocking(false);
56862
- }
56863
- setTimerStatus("idle");
56864
- setRemainingSeconds(totalSeconds);
56865
- setArtStage(0);
56996
+ if (key.name === "=" || key.name === "+") {
56997
+ const newTotal = timer.totalSeconds + 300;
56998
+ timer.adjustDuration(300);
56999
+ saveConfig({ duration: newTotal / 60 });
57000
+ return;
56866
57001
  }
56867
- if (timerStatus === "idle") {
56868
- if (key.name === "=" || key.name === "+") {
56869
- const newTotal = totalSeconds + 300;
56870
- setTotalSeconds(newTotal);
56871
- setRemainingSeconds(newTotal);
56872
- }
56873
- if (key.name === "-") {
56874
- const newTotal = Math.max(totalSeconds - 300, 60);
56875
- setTotalSeconds(newTotal);
56876
- setRemainingSeconds(newTotal);
56877
- }
57002
+ if (key.name === "-") {
57003
+ const newTotal = Math.max(timer.totalSeconds - 300, 60);
57004
+ timer.adjustDuration(-300);
57005
+ saveConfig({ duration: newTotal / 60 });
57006
+ return;
56878
57007
  }
56879
57008
  }
56880
- if (screen === "completed") {
56881
- if (key.name === "space") {
56882
- setRemainingSeconds(totalSeconds);
56883
- setArtStage(0);
56884
- setTimerStatus("idle");
56885
- setScreen("timer");
57009
+ if (key.name === "space") {
57010
+ if (timer.timerStatus === "running") {
57011
+ timer.setTimerStatus("paused");
57012
+ } else if (timer.timerStatus === "paused") {
57013
+ timer.setTimerStatus("running");
56886
57014
  }
56887
57015
  }
56888
- });
56889
- const handleOnboardingComplete = (result) => {
57016
+ if (key.name === "r" && (timer.timerStatus === "running" || timer.timerStatus === "paused")) {
57017
+ resetSession();
57018
+ }
57019
+ }
57020
+ function handleOnboardingComplete(result) {
56890
57021
  setBlockedSites(result.blockedSites);
56891
- setTotalSeconds(result.duration * 60);
56892
- setRemainingSeconds(result.duration * 60);
57022
+ timer.setDuration(result.duration * 60);
56893
57023
  saveConfig({
56894
57024
  duration: result.duration,
56895
57025
  blockedSites: result.blockedSites
56896
57026
  });
56897
57027
  setScreen("timer");
56898
- };
57028
+ }
57029
+ function handleSitesSave(sites) {
57030
+ setBlockedSites(sites);
57031
+ saveConfig({ blockedSites: sites });
57032
+ setScreen("timer");
57033
+ }
57034
+ function handleThemeSelect(theme) {
57035
+ setCurrentTheme(theme);
57036
+ saveConfig({ theme: theme.name });
57037
+ setScreen("timer");
57038
+ }
56899
57039
  if (screen === "onboarding") {
56900
57040
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Onboarding, {
56901
57041
  onComplete: handleOnboardingComplete
@@ -56904,11 +57044,7 @@ function App({
56904
57044
  if (screen === "sites") {
56905
57045
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(SiteEditor, {
56906
57046
  currentSites: blockedSites,
56907
- onSave: (sites) => {
56908
- setBlockedSites(sites);
56909
- saveConfig({ blockedSites: sites });
56910
- setScreen("timer");
56911
- },
57047
+ onSave: handleSitesSave,
56912
57048
  onCancel: () => setScreen("timer")
56913
57049
  }, undefined, false, undefined, this);
56914
57050
  }
@@ -56916,32 +57052,28 @@ function App({
56916
57052
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(ThemePicker, {
56917
57053
  themes: ALL_THEMES,
56918
57054
  currentTheme: currentTheme.name,
56919
- onSelect: (theme) => {
56920
- setCurrentTheme(theme);
56921
- saveConfig({ theme: theme.name });
56922
- setScreen("timer");
56923
- },
57055
+ onSelect: handleThemeSelect,
56924
57056
  onCancel: () => setScreen("timer")
56925
57057
  }, undefined, false, undefined, this);
56926
57058
  }
56927
- const progress = totalSeconds > 0 ? 1 - remainingSeconds / totalSeconds : 0;
56928
- const timerColor = screen === "completed" ? celebrationColor : "#E0F0FF";
57059
+ if (screen === "stats") {
57060
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(StatsScreen, {
57061
+ config
57062
+ }, undefined, false, undefined, this);
57063
+ }
57064
+ const progress = timer.totalSeconds > 0 ? 1 - timer.remainingSeconds / timer.totalSeconds : 0;
57065
+ const timerColor = screen === "completed" ? celebrationColor : COLORS.text;
57066
+ const displayStatus = screen === "completed" ? "finished" : timer.timerStatus;
56929
57067
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
56930
57068
  flexDirection: "column",
56931
57069
  width: "100%",
56932
57070
  height: "100%",
56933
57071
  children: [
56934
57072
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
56935
- justifyContent: "center",
56936
- alignItems: "center",
56937
- width: "100%",
56938
- children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
56939
- fg: "#7FDBCA",
56940
- children: screen === "completed" ? "\u2726 tunl \u2726" : "\u25C9 tunl"
56941
- }, undefined, false, undefined, this)
57073
+ height: 1
56942
57074
  }, undefined, false, undefined, this),
56943
57075
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(TimerDisplay, {
56944
- remaining: remainingSeconds,
57076
+ remaining: timer.remainingSeconds,
56945
57077
  color: timerColor
56946
57078
  }, undefined, false, undefined, this),
56947
57079
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(ProgressBar, {
@@ -56952,38 +57084,117 @@ function App({
56952
57084
  justifyContent: "center",
56953
57085
  alignItems: "center",
56954
57086
  width: "100%",
56955
- children: screen === "completed" ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
56956
- fg: celebrationColor,
56957
- children: "\u2726 Session complete! " + Math.round(totalSeconds / 60) + " minutes of pure focus. \u2726"
56958
- }, undefined, false, undefined, this) : blockError ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
56959
- fg: "#ED8796",
56960
- children: "\u26A0 " + blockError
56961
- }, undefined, false, undefined, this) : timerStatus === "idle" ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
56962
- fg: "#9399B2",
56963
- children: "press space to start focusing"
56964
- }, undefined, false, undefined, this) : timerStatus === "paused" ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
56965
- fg: "#EED49F",
56966
- children: "paused \u2014 press space to resume"
56967
- }, undefined, false, undefined, this) : /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
56968
- fg: "#9399B2",
56969
- children: "stay focused..."
57087
+ children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(TimerStatusMessage, {
57088
+ screen,
57089
+ timerStatus: timer.timerStatus,
57090
+ blockError,
57091
+ config,
57092
+ totalSeconds: timer.totalSeconds,
57093
+ celebrationColor
56970
57094
  }, undefined, false, undefined, this)
56971
57095
  }, undefined, false, undefined, this),
56972
57096
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(ArtCanvas, {
56973
- stage: artStage,
57097
+ stage: 0,
56974
57098
  theme: currentTheme,
56975
57099
  animTick
56976
57100
  }, undefined, false, undefined, this),
56977
57101
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(StatusBar, {
56978
57102
  blockedCount: isBlocking ? blockedSites.length : 0,
56979
57103
  blockedSites: isBlocking ? blockedSites : [],
56980
- timerStatus: screen === "completed" ? "finished" : timerStatus
57104
+ timerStatus: displayStatus
56981
57105
  }, undefined, false, undefined, this)
56982
57106
  ]
56983
57107
  }, undefined, true, undefined, this);
56984
57108
  }
57109
+ function TimerStatusMessage({
57110
+ screen,
57111
+ timerStatus,
57112
+ blockError,
57113
+ config,
57114
+ totalSeconds
57115
+ }) {
57116
+ if (screen === "completed") {
57117
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
57118
+ children: [
57119
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
57120
+ fg: COLORS.accent,
57121
+ children: "done. "
57122
+ }, undefined, false, undefined, this),
57123
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
57124
+ fg: COLORS.textMuted,
57125
+ children: [
57126
+ config.totalSessions,
57127
+ " sessions \xB7",
57128
+ " ",
57129
+ config.totalMinutesFocused,
57130
+ " min total",
57131
+ config.currentStreak > 0 ? ` \xB7 ${config.currentStreak} day streak` : ""
57132
+ ]
57133
+ }, undefined, true, undefined, this)
57134
+ ]
57135
+ }, undefined, true, undefined, this);
57136
+ }
57137
+ if (blockError) {
57138
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
57139
+ fg: COLORS.error,
57140
+ children: blockError
57141
+ }, undefined, false, undefined, this);
57142
+ }
57143
+ if (timerStatus === "idle") {
57144
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
57145
+ children: [
57146
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
57147
+ fg: COLORS.border,
57148
+ children: "space to begin"
57149
+ }, undefined, false, undefined, this),
57150
+ config.totalSessions > 0 && /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
57151
+ fg: COLORS.textDimmer,
57152
+ children: " \xB7 " + config.totalSessions + " sessions" + (config.currentStreak > 1 ? " \xB7 " + config.currentStreak + " day streak" : "")
57153
+ }, undefined, false, undefined, this)
57154
+ ]
57155
+ }, undefined, true, undefined, this);
57156
+ }
57157
+ if (timerStatus === "paused") {
57158
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
57159
+ fg: COLORS.warning,
57160
+ children: "paused"
57161
+ }, undefined, false, undefined, this);
57162
+ }
57163
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
57164
+ fg: COLORS.border,
57165
+ children: " "
57166
+ }, undefined, false, undefined, this);
57167
+ }
56985
57168
 
56986
- // src/cli.tsx
57169
+ // src/lib/browser-dns.ts
57170
+ import { existsSync as existsSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
57171
+ import { execSync as execSync2 } from "child_process";
57172
+ import { homedir as homedir3 } from "os";
57173
+ import { join as join4 } from "path";
57174
+ var BROWSERS = [
57175
+ "com.google.Chrome",
57176
+ "com.google.Chrome.canary",
57177
+ "company.thebrowser.Browser",
57178
+ "com.microsoft.Edge",
57179
+ "com.brave.Browser",
57180
+ "com.vivaldi.Vivaldi",
57181
+ "com.operasoftware.Opera"
57182
+ ];
57183
+ var DNS_SETUP_FLAG = join4(homedir3(), ".tunl-dns-setup");
57184
+ function setupBrowserDNS() {
57185
+ if (existsSync6(DNS_SETUP_FLAG))
57186
+ return false;
57187
+ for (const bundle of BROWSERS) {
57188
+ try {
57189
+ execSync2(`defaults write ${bundle} BuiltInDnsClientEnabled -bool false 2>/dev/null`);
57190
+ execSync2(`defaults write ${bundle} DnsOverHttpsMode -string "off" 2>/dev/null`);
57191
+ } catch {}
57192
+ }
57193
+ writeFileSync3(DNS_SETUP_FLAG, new Date().toISOString());
57194
+ return true;
57195
+ }
57196
+
57197
+ // src/utils/args.ts
56987
57198
  function parseArgs(argv) {
56988
57199
  const opts = {
56989
57200
  duration: undefined,
@@ -56991,6 +57202,7 @@ function parseArgs(argv) {
56991
57202
  extraBlocks: [],
56992
57203
  sites: undefined,
56993
57204
  showConfig: false,
57205
+ showStats: false,
56994
57206
  resetConfig: false
56995
57207
  };
56996
57208
  for (let i = 0;i < argv.length; i++) {
@@ -57008,10 +57220,19 @@ function parseArgs(argv) {
57008
57220
  i++;
57009
57221
  } else if (arg === "--config") {
57010
57222
  opts.showConfig = true;
57223
+ } else if (arg === "--stats") {
57224
+ opts.showStats = true;
57011
57225
  } else if (arg === "--reset") {
57012
57226
  opts.resetConfig = true;
57013
57227
  } else if (arg === "--help" || arg === "-h") {
57014
- console.log(`
57228
+ printHelp();
57229
+ process.exit(0);
57230
+ }
57231
+ }
57232
+ return opts;
57233
+ }
57234
+ function printHelp() {
57235
+ console.log(`
57015
57236
  tunl \u2014 Terminal Focus Timer with Art Reveal
57016
57237
 
57017
57238
  Usage:
@@ -57020,6 +57241,7 @@ function parseArgs(argv) {
57020
57241
  tunl --block "site.com" Add extra sites to block
57021
57242
  tunl --sites "site1,site2" Set the full blocklist (saves to config)
57022
57243
  tunl --noblock Timer + art only, no site blocking
57244
+ tunl --stats Show focus stats and streaks
57023
57245
  tunl --config Show current saved config
57024
57246
  tunl --reset Reset config (re-run onboarding)
57025
57247
  tunl --help Show this help
@@ -57029,41 +57251,21 @@ function parseArgs(argv) {
57029
57251
  q Quit
57030
57252
  +/- Adjust time by 5 minutes (before starting)
57031
57253
  `);
57032
- process.exit(0);
57033
- }
57034
- }
57035
- return opts;
57036
57254
  }
57255
+
57256
+ // src/cli.tsx
57037
57257
  async function main2() {
57038
57258
  const args = parseArgs(process.argv.slice(2));
57259
+ if (args.showStats) {
57260
+ printStats();
57261
+ process.exit(0);
57262
+ }
57039
57263
  if (args.showConfig) {
57040
- const config = loadConfig();
57041
- console.log(`
57042
- \u25C9 tunl config (~/.tunl.json)
57043
- `);
57044
- console.log(` Duration: ${config.duration} minutes`);
57045
- console.log(` Theme: ${config.theme}`);
57046
- console.log(` No-block: ${config.noblock}`);
57047
- console.log(` Sites: ${config.blockedSites.join(", ")}`);
57048
- console.log(` First run: ${config.isFirstRun}
57049
- `);
57264
+ printConfig();
57050
57265
  process.exit(0);
57051
57266
  }
57052
57267
  if (args.resetConfig) {
57053
- const { unlinkSync: unlinkSync2, existsSync: existsSync4 } = await import("fs");
57054
- const { homedir: homedir3 } = await import("os");
57055
- const { join: join4 } = await import("path");
57056
- const configPath = join4(homedir3(), ".tunl.json");
57057
- if (existsSync4(configPath)) {
57058
- unlinkSync2(configPath);
57059
- console.log(`
57060
- \u2713 Config reset. Next run will show onboarding.
57061
- `);
57062
- } else {
57063
- console.log(`
57064
- No config found. Already fresh.
57065
- `);
57066
- }
57268
+ await resetConfig();
57067
57269
  process.exit(0);
57068
57270
  }
57069
57271
  if (args.sites) {
@@ -57072,6 +57274,18 @@ async function main2() {
57072
57274
  \u2713 Blocklist updated: ${args.sites.join(", ")}
57073
57275
  `);
57074
57276
  }
57277
+ if (!args.noblock) {
57278
+ const needsRestart = setupBrowserDNS();
57279
+ if (needsRestart) {
57280
+ console.log(`
57281
+ \u25C9 tunl \u2014 first-time setup
57282
+ `);
57283
+ console.log(" Disabled browser built-in DNS so site blocking works.");
57284
+ console.log(" Please restart your browser (Chrome/Arc/Edge) for this to take effect.");
57285
+ console.log(` This only needs to happen once.
57286
+ `);
57287
+ }
57288
+ }
57075
57289
  if (!args.noblock) {
57076
57290
  console.log(`
57077
57291
  \u25C9 tunl \u2014 Terminal Focus Timer
@@ -57080,7 +57294,7 @@ async function main2() {
57080
57294
  console.log(` You'll be prompted for your password.
57081
57295
  `);
57082
57296
  try {
57083
- execSync2("sudo -v", { stdio: "inherit" });
57297
+ execSync3("sudo -v", { stdio: "inherit" });
57084
57298
  console.log(`
57085
57299
  \u2713 Ready! Entering focus mode...
57086
57300
  `);
@@ -57097,6 +57311,7 @@ async function main2() {
57097
57311
  useMouse: false,
57098
57312
  enableMouseMovement: false
57099
57313
  });
57314
+ globalThis.__tunl_renderer = renderer;
57100
57315
  const root = createRoot(renderer);
57101
57316
  root.render(/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(App, {
57102
57317
  initialDuration: args.duration,
@@ -57104,7 +57319,56 @@ async function main2() {
57104
57319
  extraBlocks: args.extraBlocks
57105
57320
  }, undefined, false, undefined, this));
57106
57321
  }
57322
+ function printStats() {
57323
+ const config = loadConfig();
57324
+ const timeStr = formatMinutes(config.totalMinutesFocused);
57325
+ console.log(`
57326
+ \u25C9 tunl stats
57327
+ `);
57328
+ console.log(` Sessions: ${config.totalSessions}`);
57329
+ console.log(` Total focused: ${timeStr}`);
57330
+ console.log(` Day streak: ${config.currentStreak}`);
57331
+ console.log(` Last session: ${config.lastSessionDate || "never"}`);
57332
+ console.log(` Timer: ${config.duration} min`);
57333
+ console.log(` Theme: ${config.theme}
57334
+ `);
57335
+ }
57336
+ function printConfig() {
57337
+ const config = loadConfig();
57338
+ console.log(`
57339
+ \u25C9 tunl config (~/.tunl.json)
57340
+ `);
57341
+ console.log(` Duration: ${config.duration} minutes`);
57342
+ console.log(` Theme: ${config.theme}`);
57343
+ console.log(` No-block: ${config.noblock}`);
57344
+ console.log(` Sites: ${config.blockedSites.join(", ")}`);
57345
+ console.log(` First run: ${config.isFirstRun}
57346
+ `);
57347
+ }
57348
+ async function resetConfig() {
57349
+ const { unlinkSync: unlinkSync3, existsSync: existsSync4 } = await import("fs");
57350
+ const { homedir: homedir4 } = await import("os");
57351
+ const { join: join5 } = await import("path");
57352
+ const configPath = join5(homedir4(), ".tunl.json");
57353
+ if (existsSync4(configPath)) {
57354
+ unlinkSync3(configPath);
57355
+ console.log(`
57356
+ \u2713 Config reset. Next run will show onboarding.
57357
+ `);
57358
+ } else {
57359
+ console.log(`
57360
+ No config found. Already fresh.
57361
+ `);
57362
+ }
57363
+ }
57107
57364
  main2().catch((err) => {
57365
+ const renderer = globalThis.__tunl_renderer;
57366
+ if (renderer) {
57367
+ try {
57368
+ renderer.destroy();
57369
+ } catch {}
57370
+ }
57371
+ process.stdout.write("\x1B[?25h\x1B[?1049l");
57108
57372
  console.error("tunl error:", err);
57109
57373
  process.exit(1);
57110
57374
  });