Compare commits
2 Commits
0e29953c0f
...
8008c33017
| Author | SHA1 | Date | |
|---|---|---|---|
| 8008c33017 | |||
| 2b302aece4 |
52
README.md
52
README.md
@@ -4,12 +4,14 @@ A fully automated break timer for wlroots-based desktop environments (SwayWM, et
|
||||
|
||||
## Features
|
||||
|
||||
- **Zero user interaction required** - Automatically tracks work/break cycles
|
||||
- **Dual break timers** - Independent short breaks (20m work → 20s break) and long breaks (45m work → 5m break)
|
||||
- **Automatic idle detection** - Uses swayidle to detect when you're away from the computer
|
||||
- **Self-managing log** - Automatically trims old events to keep log size minimal
|
||||
- **Waybar integration** - Shows countdown timers in your status bar
|
||||
- **No dependencies** - Only requires Python 3 (pre-installed on most Linux systems)
|
||||
- **Zero user interaction required** - Automatically tracks work/break cycles.
|
||||
- **Dual break timers** - Independent short breaks (20m work → 20s break) and long breaks (45m work → 5m break).
|
||||
- **Automatic idle detection** - Uses swayidle to detect when you're away from the computer.
|
||||
- **Pause/Unpause** - Manually pause the timer when needed (paused time doesn't count toward work duration).
|
||||
- **Self-managing log** - Automatically trims old events to keep log size minimal.
|
||||
- **Waybar integration** - Shows countdown timers in your status bar with context menu.
|
||||
- **No dependencies** - Only requires Python 3 (pre-installed on most Linux systems).
|
||||
- **No daemon process** - Swaylock is the only long-running process. Other scripts are executed to modify or read the log file.
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -69,7 +71,8 @@ A fully automated break timer for wlroots-based desktop environments (SwayWM, et
|
||||
"menu": "on-click",
|
||||
"menu-file": "/path/to/menu.xml",
|
||||
"menu-actions": {
|
||||
"skip-long": "break-event skip_long"
|
||||
"skip-long": "break-event skip_long",
|
||||
"toggle-pause": "break-event toggle-pause"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +96,11 @@ A fully automated break timer for wlroots-based desktop environments (SwayWM, et
|
||||
#custom-break-timer.break {
|
||||
color: #89b4fa;
|
||||
}
|
||||
|
||||
#custom-break-timer.paused {
|
||||
color: #cba6f7;
|
||||
font-style: italic;
|
||||
}
|
||||
```
|
||||
|
||||
7. **Reload Sway and Waybar:**
|
||||
@@ -108,14 +116,13 @@ Edit `~/.config/break-timer/config.json`:
|
||||
```json
|
||||
{
|
||||
"short_break": {
|
||||
"interval_minutes": 20, // Work duration before short break
|
||||
"duration_minutes": 2 // Required short break length
|
||||
"interval": "20m", // Work duration before short break
|
||||
"duration": "20s" // Required short break length
|
||||
},
|
||||
"long_break": {
|
||||
"interval_minutes": 60, // Work duration before long break
|
||||
"duration_minutes": 10 // Required long break length
|
||||
"interval": "45", // Work duration before long break
|
||||
"duration": "5m" // Required long break length
|
||||
},
|
||||
"idle_timeout_seconds": 120 // How long before considering user idle
|
||||
}
|
||||
```
|
||||
|
||||
@@ -123,10 +130,11 @@ Edit `~/.config/break-timer/config.json`:
|
||||
|
||||
The Waybar module shows:
|
||||
|
||||
- **Working**: `S:15:30 L:45:20` - Time until short break (15min 30sec) and long break (45min 20sec)
|
||||
- **Short break**: `Short break: 1:45` - Time remaining in short break
|
||||
- **Long break**: `Long break: 8:30` - Time remaining in long break
|
||||
- **Overdue**: `BREAK! S:-2:30 L:5:00` - You've exceeded a break time
|
||||
- **Working**: `15:30 / 45:20` - Time until short break (15min 30sec) and long break (45min 20sec)
|
||||
- **Short break**: `SB 1:45` - Time remaining in short break
|
||||
- **Long break**: `LB 8:30` - Time remaining in long break
|
||||
- **Overdue**: `BREAK! -2:30 / 5:00` - You've exceeded a break time
|
||||
- **Paused**: `⏸ PAUSED` - Timer is paused, no activity is being tracked
|
||||
|
||||
### Color Coding
|
||||
|
||||
@@ -134,6 +142,18 @@ The Waybar module shows:
|
||||
- **Yellow (warning)**: Break due in less than 5 minutes
|
||||
- **Red (overdue)**: Break time has passed, you should take a break!
|
||||
- **Blue (break)**: Currently in a break period
|
||||
- **Purple (paused)**: Timer is paused
|
||||
|
||||
## Using the Context Menu
|
||||
|
||||
Right-click the break timer widget in Waybar to access the context menu:
|
||||
|
||||
- **Skip Long Break**: Reset all timers (useful if you just returned from a break not tracked by the timer)
|
||||
- ** Toggle Pause**: Pause or unpause the timer. While paused:
|
||||
- No activity is tracked
|
||||
- Idle detection is disabled (idle events are not logged)
|
||||
- The widget shows "⏸ PAUSED" in purple
|
||||
- Only the "Unpause Timer" menu option is shown
|
||||
|
||||
## Log Management
|
||||
|
||||
|
||||
31
break-event
31
break-event
@@ -161,9 +161,14 @@ def write_log(log_path: Path, events: Events) -> None:
|
||||
temp_path.unlink()
|
||||
|
||||
|
||||
def is_paused(events: Events) -> bool:
|
||||
"""Check if timer is currently paused (last event is 'paused')"""
|
||||
return bool(events and events[-1][1] == "paused")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2 or sys.argv[1] not in ("idle_start", "idle_end", "skip_long"):
|
||||
print("Usage: break-event <idle_start|idle_end|skip_long>", file=sys.stderr)
|
||||
if len(sys.argv) != 2 or sys.argv[1] not in ("idle_start", "idle_end", "skip_long", "toggle-pause"):
|
||||
print("Usage: break-event <idle_start|idle_end|skip_long|toggle-pause>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
event_type = sys.argv[1]
|
||||
@@ -179,21 +184,25 @@ def main():
|
||||
with open(lock_path, 'w') as lock_file:
|
||||
fcntl.flock(lock_file, fcntl.LOCK_EX)
|
||||
|
||||
# Read current events
|
||||
events = parse_log(log_path)
|
||||
|
||||
try:
|
||||
if event_type == "skip_long":
|
||||
# Erase all events from the log, effectively resetting all timers.
|
||||
write_log(log_path, [])
|
||||
else:
|
||||
# Read current events
|
||||
events = parse_log(log_path)
|
||||
|
||||
# Add new event
|
||||
elif event_type == "toggle-pause":
|
||||
current_time = int(time.time())
|
||||
events.append((current_time, event_type))
|
||||
events = trim_log(events, long_break_duration)
|
||||
|
||||
# Write back to log
|
||||
event_key = "paused" if not is_paused(events) else "unpaused"
|
||||
events.append((current_time, event_key))
|
||||
write_log(log_path, events)
|
||||
else:
|
||||
# Don't log idle events when paused
|
||||
if not is_paused(events):
|
||||
current_time = int(time.time())
|
||||
events.append((current_time, event_type))
|
||||
events = trim_log(events, long_break_duration)
|
||||
write_log(log_path, events)
|
||||
finally:
|
||||
fcntl.flock(lock_file, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
59
break-status
59
break-status
@@ -116,6 +116,11 @@ def format_time(seconds: int) -> str:
|
||||
return f"{minutes}:{secs:02d}"
|
||||
|
||||
|
||||
def is_paused(events: Events) -> bool:
|
||||
"""Check if timer is currently paused (last event is 'paused')"""
|
||||
return bool(events and events[-1][1] == "paused")
|
||||
|
||||
|
||||
def calculate_status(events: Events, config: Config) -> Status:
|
||||
"""
|
||||
Calculate break timer status from events.
|
||||
@@ -123,6 +128,14 @@ def calculate_status(events: Events, config: Config) -> Status:
|
||||
"""
|
||||
current_time = int(time.time())
|
||||
|
||||
# Check if currently paused
|
||||
if is_paused(events):
|
||||
return {
|
||||
"state": "paused",
|
||||
"short_remaining": 0,
|
||||
"long_remaining": 0,
|
||||
}
|
||||
|
||||
# Convert config to seconds
|
||||
short_interval = parse_duration(config["short_break"]["interval"])
|
||||
short_duration = parse_duration(config["short_break"]["duration"])
|
||||
@@ -143,8 +156,36 @@ def calculate_status(events: Events, config: Config) -> Status:
|
||||
current_state = "working"
|
||||
idle_start_time = None
|
||||
|
||||
pause_events = [e for e in events if e[1] in ("paused", "unpaused")]
|
||||
idle_events = [e for e in events if e[1] in ("idle_start", "idle_end")]
|
||||
|
||||
# Helper to calculate paused time within a range
|
||||
def get_paused_time_in_range(start_time: int, end_time: int) -> int:
|
||||
"""Calculate total paused time within a time range"""
|
||||
paused_time = 0
|
||||
pause_start = None
|
||||
|
||||
for timestamp, event_type in pause_events:
|
||||
if timestamp > end_time:
|
||||
break
|
||||
|
||||
if event_type == "paused" and timestamp >= start_time:
|
||||
pause_start = timestamp
|
||||
elif event_type == "unpaused" and pause_start is not None:
|
||||
# Calculate overlap between pause period and our range
|
||||
overlap_start = max(pause_start, start_time)
|
||||
overlap_end = min(timestamp, end_time)
|
||||
if overlap_end > overlap_start:
|
||||
paused_time += overlap_end - overlap_start
|
||||
pause_start = None
|
||||
|
||||
# Handle case where still paused at end of range
|
||||
if pause_start is not None and pause_start < end_time:
|
||||
paused_time += end_time - max(pause_start, start_time)
|
||||
return paused_time
|
||||
|
||||
# Process events to calculate active time and breaks
|
||||
for i, (timestamp, event_type) in enumerate(events):
|
||||
for i, (timestamp, event_type) in enumerate(idle_events):
|
||||
if event_type == "idle_start":
|
||||
current_state = "idle"
|
||||
idle_start_time = timestamp
|
||||
@@ -155,6 +196,10 @@ def calculate_status(events: Events, config: Config) -> Status:
|
||||
active_start = events[i-1][0]
|
||||
duration = active_end - active_start
|
||||
|
||||
# Subtract any paused time within this active period
|
||||
paused_time = get_paused_time_in_range(active_start, active_end)
|
||||
duration -= paused_time
|
||||
|
||||
# Add to both counters, they reset independently
|
||||
active_time_since_short_break += duration
|
||||
active_time_since_long_break += duration
|
||||
@@ -179,6 +224,11 @@ def calculate_status(events: Events, config: Config) -> Status:
|
||||
if events and events[-1][1] == "idle_end":
|
||||
last_active_start = events[-1][0]
|
||||
current_active_duration = current_time - last_active_start
|
||||
|
||||
# Subtract any paused time in current active period
|
||||
paused_time = get_paused_time_in_range(last_active_start, current_time)
|
||||
current_active_duration -= paused_time
|
||||
|
||||
active_time_since_short_break += current_active_duration
|
||||
active_time_since_long_break += current_active_duration
|
||||
|
||||
@@ -232,7 +282,12 @@ def format_output(status: Status):
|
||||
|
||||
state = status["state"]
|
||||
|
||||
if state == "working":
|
||||
if state == "paused":
|
||||
css_class = "paused"
|
||||
text = "⏸ PAUSED"
|
||||
tooltip = "Timer is paused. Click to unpause."
|
||||
|
||||
elif state == "working":
|
||||
short_time = format_time(status["short_remaining"])
|
||||
long_time = format_time(status["long_remaining"])
|
||||
|
||||
|
||||
5
menu.xml
5
menu.xml
@@ -6,5 +6,10 @@
|
||||
<property name="label">Skip Long Break</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuItem" id="toggle-pause">
|
||||
<property name="label">Toggle Pause</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -3,10 +3,16 @@
|
||||
"custom/break-timer"
|
||||
],
|
||||
"custom/break-timer": {
|
||||
"exec": "~/path/to/break-status",
|
||||
"exec": "break-status",
|
||||
"return-type": "json",
|
||||
"interval": 1,
|
||||
"format": "🕐 {}",
|
||||
"tooltip": true
|
||||
"tooltip": true,
|
||||
"menu": "on-click",
|
||||
"menu-file": "$XDG_RUNTIME_DIR/break-timer/menu.xml",
|
||||
"menu-actions": {
|
||||
"skip-long": "break-event skip_long",
|
||||
"toggle-pause": "break-event toggle-pause"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user