Compare commits
10 Commits
c5fff116e6
...
0e29953c0f
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e29953c0f | |||
| 2efa36bfeb | |||
| d32c03164f | |||
| 8933a6cced | |||
| b475718a99 | |||
| 8cc44fdc90 | |||
| 7275c0e0e6 | |||
| fdf782a858 | |||
| f4df17e9bd | |||
| 4e32d6179a |
49
README.md
49
README.md
@@ -7,8 +7,9 @@ A fully automated break timer for wlroots-based desktop environments (SwayWM, et
|
|||||||
- **Zero user interaction required** - Automatically tracks work/break cycles
|
- **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)
|
- **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
|
- **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
|
- **Self-managing log** - Automatically trims old events to keep log size minimal
|
||||||
- **Waybar integration** - Shows countdown timers in your status bar
|
- **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 dependencies** - Only requires Python 3 (pre-installed on most Linux systems)
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
@@ -50,7 +51,9 @@ A fully automated break timer for wlroots-based desktop environments (SwayWM, et
|
|||||||
```
|
```
|
||||||
exec swayidle -w \
|
exec swayidle -w \
|
||||||
timeout 5 'break-event idle_start' \
|
timeout 5 'break-event idle_start' \
|
||||||
resume 'break-event idle_end'
|
resume 'break-event idle_end' \
|
||||||
|
before-sleep 'break-event idle_start' \
|
||||||
|
after-resume 'break-event idle_end'
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Add to Waybar config** (`~/.config/waybar/config`):
|
5. **Add to Waybar config** (`~/.config/waybar/config`):
|
||||||
@@ -63,7 +66,12 @@ A fully automated break timer for wlroots-based desktop environments (SwayWM, et
|
|||||||
"return-type": "json",
|
"return-type": "json",
|
||||||
"interval": 1,
|
"interval": 1,
|
||||||
"format": "🕐 {}",
|
"format": "🕐 {}",
|
||||||
"tooltip": true
|
"tooltip": true,
|
||||||
|
"menu": "on-click",
|
||||||
|
"menu-file": "/path/to/menu.xml",
|
||||||
|
"menu-actions": {
|
||||||
|
"skip-long": "break-event skip_long"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -86,6 +94,11 @@ A fully automated break timer for wlroots-based desktop environments (SwayWM, et
|
|||||||
#custom-break-timer.break {
|
#custom-break-timer.break {
|
||||||
color: #89b4fa;
|
color: #89b4fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#custom-break-timer.paused {
|
||||||
|
color: #cba6f7;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
7. **Reload Sway and Waybar:**
|
7. **Reload Sway and Waybar:**
|
||||||
@@ -101,14 +114,13 @@ Edit `~/.config/break-timer/config.json`:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"short_break": {
|
"short_break": {
|
||||||
"interval_minutes": 20, // Work duration before short break
|
"interval": "20m", // Work duration before short break
|
||||||
"duration_minutes": 2 // Required short break length
|
"duration": "20s" // Required short break length
|
||||||
},
|
},
|
||||||
"long_break": {
|
"long_break": {
|
||||||
"interval_minutes": 60, // Work duration before long break
|
"interval": "45", // Work duration before long break
|
||||||
"duration_minutes": 10 // Required long break length
|
"duration": "5m" // Required long break length
|
||||||
},
|
},
|
||||||
"idle_timeout_seconds": 120 // How long before considering user idle
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -116,10 +128,11 @@ Edit `~/.config/break-timer/config.json`:
|
|||||||
|
|
||||||
The Waybar module shows:
|
The Waybar module shows:
|
||||||
|
|
||||||
- **Working**: `S:15:30 L:45:20` - Time until short break (15min 30sec) and long break (45min 20sec)
|
- **Working**: `15:30 / 45:20` - Time until short break (15min 30sec) and long break (45min 20sec)
|
||||||
- **Short break**: `Short break: 1:45` - Time remaining in short break
|
- **Short break**: `SB 1:45` - Time remaining in short break
|
||||||
- **Long break**: `Long break: 8:30` - Time remaining in long break
|
- **Long break**: `LB 8:30` - Time remaining in long break
|
||||||
- **Overdue**: `BREAK! S:-2:30 L:5:00` - You've exceeded a break time
|
- **Overdue**: `BREAK! -2:30 / 5:00` - You've exceeded a break time
|
||||||
|
- **Paused**: `⏸ PAUSED` - Timer is paused, no activity is being tracked
|
||||||
|
|
||||||
### Color Coding
|
### Color Coding
|
||||||
|
|
||||||
@@ -127,6 +140,18 @@ The Waybar module shows:
|
|||||||
- **Yellow (warning)**: Break due in less than 5 minutes
|
- **Yellow (warning)**: Break due in less than 5 minutes
|
||||||
- **Red (overdue)**: Break time has passed, you should take a break!
|
- **Red (overdue)**: Break time has passed, you should take a break!
|
||||||
- **Blue (break)**: Currently in a break period
|
- **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
|
## Log Management
|
||||||
|
|
||||||
|
|||||||
76
break-event
76
break-event
@@ -12,26 +12,39 @@ import time
|
|||||||
import fcntl
|
import fcntl
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
Event = tuple[int, str]
|
||||||
|
Events = list[Event]
|
||||||
|
|
||||||
|
|
||||||
def parse_duration(duration_str):
|
class BreakConfig(TypedDict):
|
||||||
|
interval: str
|
||||||
|
duration: str
|
||||||
|
|
||||||
|
|
||||||
|
class Config(TypedDict):
|
||||||
|
short_break: BreakConfig
|
||||||
|
long_break: BreakConfig
|
||||||
|
|
||||||
|
def parse_duration(duration: str) -> int:
|
||||||
"""Parse duration string like '5m' or '30s' to seconds"""
|
"""Parse duration string like '5m' or '30s' to seconds"""
|
||||||
if isinstance(duration_str, (int, float)):
|
if isinstance(duration, (int, float)):
|
||||||
return int(duration_str)
|
return int(duration)
|
||||||
|
|
||||||
duration_str = duration_str.strip()
|
duration = duration.strip()
|
||||||
if duration_str.endswith('m'):
|
if duration.endswith('m'):
|
||||||
return int(duration_str[:-1]) * 60
|
return int(duration[:-1]) * 60
|
||||||
elif duration_str.endswith('s'):
|
elif duration.endswith('s'):
|
||||||
return int(duration_str[:-1])
|
return int(duration[:-1])
|
||||||
elif duration_str.endswith('h'):
|
elif duration.endswith('h'):
|
||||||
return int(duration_str[:-1]) * 3600
|
return int(duration[:-1]) * 3600
|
||||||
else:
|
else:
|
||||||
# Assume seconds if no unit
|
# Assume seconds if no unit
|
||||||
return int(duration_str)
|
return int(duration)
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
def load_config() -> Config:
|
||||||
"""Load configuration from ~/.config/break-timer/config.json"""
|
"""Load configuration from ~/.config/break-timer/config.json"""
|
||||||
config_path = Path.home() / ".config" / "break-timer" / "config.json"
|
config_path = Path.home() / ".config" / "break-timer" / "config.json"
|
||||||
|
|
||||||
@@ -51,7 +64,7 @@ def load_config():
|
|||||||
return default_config
|
return default_config
|
||||||
|
|
||||||
|
|
||||||
def get_log_path():
|
def get_log_path() -> Path:
|
||||||
"""Get the path to the activity log file"""
|
"""Get the path to the activity log file"""
|
||||||
runtime_dir = os.environ.get('XDG_RUNTIME_DIR')
|
runtime_dir = os.environ.get('XDG_RUNTIME_DIR')
|
||||||
if runtime_dir:
|
if runtime_dir:
|
||||||
@@ -64,7 +77,7 @@ def get_log_path():
|
|||||||
return log_dir / "activity.log"
|
return log_dir / "activity.log"
|
||||||
|
|
||||||
|
|
||||||
def parse_log(log_path):
|
def parse_log(log_path: Path) -> Events:
|
||||||
"""Parse the log file and return list of (timestamp, event_type) tuples"""
|
"""Parse the log file and return list of (timestamp, event_type) tuples"""
|
||||||
events = []
|
events = []
|
||||||
if not log_path.exists():
|
if not log_path.exists():
|
||||||
@@ -90,7 +103,7 @@ def parse_log(log_path):
|
|||||||
return events
|
return events
|
||||||
|
|
||||||
|
|
||||||
def trim_log(events, long_break_duration):
|
def trim_log(events: Events, long_break_duration: int) -> Events:
|
||||||
"""
|
"""
|
||||||
Trim events before qualifying long breaks.
|
Trim events before qualifying long breaks.
|
||||||
Returns filtered list of events.
|
Returns filtered list of events.
|
||||||
@@ -111,7 +124,7 @@ def trim_log(events, long_break_duration):
|
|||||||
if next_event_type == "idle_end":
|
if next_event_type == "idle_end":
|
||||||
idle_duration = next_timestamp - timestamp
|
idle_duration = next_timestamp - timestamp
|
||||||
if idle_duration >= long_break_duration:
|
if idle_duration >= long_break_duration:
|
||||||
last_qualifying_idle_index = i
|
last_qualifying_idle_index = j
|
||||||
break
|
break
|
||||||
elif next_event_type == "idle_start":
|
elif next_event_type == "idle_start":
|
||||||
# No idle_end found, check if still idle
|
# No idle_end found, check if still idle
|
||||||
@@ -131,7 +144,7 @@ def trim_log(events, long_break_duration):
|
|||||||
return events
|
return events
|
||||||
|
|
||||||
|
|
||||||
def write_log(log_path, events):
|
def write_log(log_path: Path, events: Events) -> None:
|
||||||
"""Write events to log file atomically"""
|
"""Write events to log file atomically"""
|
||||||
temp_path = log_path.with_suffix('.tmp')
|
temp_path = log_path.with_suffix('.tmp')
|
||||||
|
|
||||||
@@ -148,9 +161,14 @@ def write_log(log_path, events):
|
|||||||
temp_path.unlink()
|
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():
|
def main():
|
||||||
if len(sys.argv) != 2 or sys.argv[1] not in ("idle_start", "idle_end"):
|
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>", file=sys.stderr)
|
print("Usage: break-event <idle_start|idle_end|skip_long|toggle-pause>", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
event_type = sys.argv[1]
|
event_type = sys.argv[1]
|
||||||
@@ -166,21 +184,25 @@ def main():
|
|||||||
with open(lock_path, 'w') as lock_file:
|
with open(lock_path, 'w') as lock_file:
|
||||||
fcntl.flock(lock_file, fcntl.LOCK_EX)
|
fcntl.flock(lock_file, fcntl.LOCK_EX)
|
||||||
|
|
||||||
try:
|
|
||||||
# Read current events
|
# Read current events
|
||||||
events = parse_log(log_path)
|
events = parse_log(log_path)
|
||||||
|
|
||||||
# Add new event
|
try:
|
||||||
|
if event_type == "skip_long":
|
||||||
|
# Erase all events from the log, effectively resetting all timers.
|
||||||
|
write_log(log_path, [])
|
||||||
|
elif event_type == "toggle-pause":
|
||||||
|
current_time = int(time.time())
|
||||||
|
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())
|
current_time = int(time.time())
|
||||||
events.append((current_time, event_type))
|
events.append((current_time, event_type))
|
||||||
|
|
||||||
# Trim old events if this is an idle_start
|
|
||||||
if event_type == "idle_start":
|
|
||||||
events = trim_log(events, long_break_duration)
|
events = trim_log(events, long_break_duration)
|
||||||
|
|
||||||
# Write back to log
|
|
||||||
write_log(log_path, events)
|
write_log(log_path, events)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
fcntl.flock(lock_file, fcntl.LOCK_UN)
|
fcntl.flock(lock_file, fcntl.LOCK_UN)
|
||||||
|
|
||||||
|
|||||||
228
break-status
Normal file → Executable file
228
break-status
Normal file → Executable file
@@ -11,26 +11,46 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import TypedDict, NotRequired
|
||||||
|
|
||||||
|
|
||||||
def parse_duration(duration_str):
|
Event = tuple[int, str]
|
||||||
|
Events = list[Event]
|
||||||
|
|
||||||
|
class BreakConfig(TypedDict):
|
||||||
|
interval: str
|
||||||
|
duration: str
|
||||||
|
|
||||||
|
|
||||||
|
class Config(TypedDict):
|
||||||
|
short_break: BreakConfig
|
||||||
|
long_break: BreakConfig
|
||||||
|
|
||||||
|
class Status(TypedDict):
|
||||||
|
state: str
|
||||||
|
short_remaining: int
|
||||||
|
long_remaining: int
|
||||||
|
break_remaining: NotRequired[int]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_duration(duration: str) -> int:
|
||||||
"""Parse duration string like '5m' or '30s' to seconds"""
|
"""Parse duration string like '5m' or '30s' to seconds"""
|
||||||
if isinstance(duration_str, (int, float)):
|
if isinstance(duration, (int, float)):
|
||||||
return int(duration_str)
|
return int(duration)
|
||||||
|
|
||||||
duration_str = duration_str.strip()
|
duration = duration.strip()
|
||||||
if duration_str.endswith('m'):
|
if duration.endswith('m'):
|
||||||
return int(duration_str[:-1]) * 60
|
return int(duration[:-1]) * 60
|
||||||
elif duration_str.endswith('s'):
|
elif duration.endswith('s'):
|
||||||
return int(duration_str[:-1])
|
return int(duration[:-1])
|
||||||
elif duration_str.endswith('h'):
|
elif duration.endswith('h'):
|
||||||
return int(duration_str[:-1]) * 3600
|
return int(duration[:-1]) * 3600
|
||||||
else:
|
else:
|
||||||
# Assume seconds if no unit
|
# Assume seconds if no unit
|
||||||
return int(duration_str)
|
return int(duration)
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
def load_config() -> Config:
|
||||||
"""Load configuration from ~/.config/break-timer/config.json"""
|
"""Load configuration from ~/.config/break-timer/config.json"""
|
||||||
config_path = Path.home() / ".config" / "break-timer" / "config.json"
|
config_path = Path.home() / ".config" / "break-timer" / "config.json"
|
||||||
|
|
||||||
@@ -50,7 +70,7 @@ def load_config():
|
|||||||
return default_config
|
return default_config
|
||||||
|
|
||||||
|
|
||||||
def get_log_path():
|
def get_log_path() -> Path:
|
||||||
"""Get the path to the activity log file"""
|
"""Get the path to the activity log file"""
|
||||||
runtime_dir = os.environ.get('XDG_RUNTIME_DIR')
|
runtime_dir = os.environ.get('XDG_RUNTIME_DIR')
|
||||||
if runtime_dir:
|
if runtime_dir:
|
||||||
@@ -61,7 +81,7 @@ def get_log_path():
|
|||||||
return log_dir / "activity.log"
|
return log_dir / "activity.log"
|
||||||
|
|
||||||
|
|
||||||
def parse_log(log_path):
|
def parse_log(log_path: Path) -> Events:
|
||||||
"""Parse the log file and return list of (timestamp, event_type) tuples"""
|
"""Parse the log file and return list of (timestamp, event_type) tuples"""
|
||||||
events = []
|
events = []
|
||||||
if not log_path.exists():
|
if not log_path.exists():
|
||||||
@@ -87,7 +107,7 @@ def parse_log(log_path):
|
|||||||
return events
|
return events
|
||||||
|
|
||||||
|
|
||||||
def format_time(seconds):
|
def format_time(seconds: int) -> str:
|
||||||
"""Format seconds as MM:SS"""
|
"""Format seconds as MM:SS"""
|
||||||
if seconds < 0:
|
if seconds < 0:
|
||||||
seconds = 0
|
seconds = 0
|
||||||
@@ -96,13 +116,26 @@ def format_time(seconds):
|
|||||||
return f"{minutes}:{secs:02d}"
|
return f"{minutes}:{secs:02d}"
|
||||||
|
|
||||||
|
|
||||||
def calculate_status(events, config):
|
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.
|
Calculate break timer status from events.
|
||||||
Returns dict with status information.
|
Returns dict with status information.
|
||||||
"""
|
"""
|
||||||
current_time = int(time.time())
|
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
|
# Convert config to seconds
|
||||||
short_interval = parse_duration(config["short_break"]["interval"])
|
short_interval = parse_duration(config["short_break"]["interval"])
|
||||||
short_duration = parse_duration(config["short_break"]["duration"])
|
short_duration = parse_duration(config["short_break"]["duration"])
|
||||||
@@ -115,48 +148,93 @@ def calculate_status(events, config):
|
|||||||
"state": "working",
|
"state": "working",
|
||||||
"short_remaining": short_interval,
|
"short_remaining": short_interval,
|
||||||
"long_remaining": long_interval,
|
"long_remaining": long_interval,
|
||||||
"active_time": 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Track active time and last break times
|
# Track active time since each type of break
|
||||||
total_active_time = 0
|
active_time_since_short_break = 0
|
||||||
last_short_break = None
|
active_time_since_long_break = 0
|
||||||
last_long_break = None
|
|
||||||
current_state = "working"
|
current_state = "working"
|
||||||
idle_start_time = None
|
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
|
# 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_end":
|
if event_type == "idle_start":
|
||||||
# Start of active period
|
|
||||||
current_state = "working"
|
|
||||||
|
|
||||||
# Calculate previous idle duration
|
|
||||||
if i > 0 and events[i-1][1] == "idle_start":
|
|
||||||
idle_duration = timestamp - events[i-1][0]
|
|
||||||
|
|
||||||
# Check if idle period satisfied breaks
|
|
||||||
if idle_duration >= long_duration:
|
|
||||||
last_long_break = events[i-1][0]
|
|
||||||
last_short_break = events[i-1][0] # Long break also resets short
|
|
||||||
elif idle_duration >= short_duration:
|
|
||||||
last_short_break = events[i-1][0]
|
|
||||||
|
|
||||||
# This becomes the start of the next active period
|
|
||||||
active_start = timestamp
|
|
||||||
|
|
||||||
# Calculate active time until next idle_start or now
|
|
||||||
if i + 1 < len(events) and events[i+1][1] == "idle_start":
|
|
||||||
active_end = events[i+1][0]
|
|
||||||
total_active_time += (active_end - active_start)
|
|
||||||
else:
|
|
||||||
# Still active or log ends
|
|
||||||
total_active_time += (current_time - active_start)
|
|
||||||
|
|
||||||
elif event_type == "idle_start":
|
|
||||||
current_state = "idle"
|
current_state = "idle"
|
||||||
idle_start_time = timestamp
|
idle_start_time = timestamp
|
||||||
|
|
||||||
|
if i > 0 and events[i-1][1] == "idle_end":
|
||||||
|
# Active period just ended
|
||||||
|
active_end = timestamp
|
||||||
|
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
|
||||||
|
|
||||||
|
elif event_type == "idle_end":
|
||||||
|
current_state = "working"
|
||||||
|
|
||||||
|
if i > 0 and events[i-1][1] == "idle_start":
|
||||||
|
# Idle period just ended
|
||||||
|
idle_end = timestamp
|
||||||
|
idle_start = events[i-1][0]
|
||||||
|
idle_duration = idle_end - idle_start
|
||||||
|
|
||||||
|
# If the idle duration qualifies as a break, reset timers
|
||||||
|
if idle_duration >= long_duration:
|
||||||
|
active_time_since_long_break = 0
|
||||||
|
if idle_duration >= short_duration:
|
||||||
|
active_time_since_short_break = 0
|
||||||
|
|
||||||
|
if current_state == "working":
|
||||||
|
# If currently working, add time since last idle_end
|
||||||
|
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
|
||||||
|
|
||||||
|
short_remaining = short_interval - active_time_since_short_break
|
||||||
|
long_remaining = long_interval - active_time_since_long_break
|
||||||
|
|
||||||
# Determine current state and calculate remaining times
|
# Determine current state and calculate remaining times
|
||||||
if current_state == "idle" and idle_start_time:
|
if current_state == "idle" and idle_start_time:
|
||||||
# Currently idle - determine if in break
|
# Currently idle - determine if in break
|
||||||
@@ -166,9 +244,8 @@ def calculate_status(events, config):
|
|||||||
# Long break completed, show time until next breaks
|
# Long break completed, show time until next breaks
|
||||||
return {
|
return {
|
||||||
"state": "break_complete",
|
"state": "break_complete",
|
||||||
"short_remaining": short_interval,
|
"short_remaining": short_interval, # Reset
|
||||||
"long_remaining": long_interval,
|
"long_remaining": long_interval, # Reset
|
||||||
"active_time": 0
|
|
||||||
}
|
}
|
||||||
elif idle_elapsed >= short_duration:
|
elif idle_elapsed >= short_duration:
|
||||||
# Short break completed, long break in progress
|
# Short break completed, long break in progress
|
||||||
@@ -176,9 +253,8 @@ def calculate_status(events, config):
|
|||||||
return {
|
return {
|
||||||
"state": "long_break",
|
"state": "long_break",
|
||||||
"break_remaining": long_break_remaining,
|
"break_remaining": long_break_remaining,
|
||||||
"short_remaining": short_interval, # Will reset when back to work
|
"short_remaining": short_interval, # Already reset
|
||||||
"long_remaining": long_interval, # Will reset when long break done
|
"long_remaining": long_remaining, # Will reset when break completed
|
||||||
"active_time": 0
|
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# Short break in progress
|
# Short break in progress
|
||||||
@@ -186,61 +262,65 @@ def calculate_status(events, config):
|
|||||||
return {
|
return {
|
||||||
"state": "short_break",
|
"state": "short_break",
|
||||||
"break_remaining": short_break_remaining,
|
"break_remaining": short_break_remaining,
|
||||||
"short_remaining": short_interval, # Will reset when break done
|
"short_remaining": short_remaining, # Will reset when break completed
|
||||||
"long_remaining": long_interval - total_active_time,
|
"long_remaining": long_remaining,
|
||||||
"active_time": total_active_time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Currently working - calculate time until breaks
|
# Currently working
|
||||||
short_remaining = short_interval - total_active_time
|
|
||||||
long_remaining = long_interval - total_active_time
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"state": "working",
|
"state": "working",
|
||||||
"short_remaining": short_remaining,
|
"short_remaining": short_remaining,
|
||||||
"long_remaining": long_remaining,
|
"long_remaining": long_remaining,
|
||||||
"active_time": total_active_time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def format_output(status):
|
def format_output(status: Status):
|
||||||
"""Format status as Waybar JSON output"""
|
"""
|
||||||
|
Format status as Waybar JSON output.
|
||||||
|
https://github.com/Alexays/Waybar/wiki/Module:-Custom
|
||||||
|
"""
|
||||||
|
|
||||||
state = status["state"]
|
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"])
|
short_time = format_time(status["short_remaining"])
|
||||||
long_time = format_time(status["long_remaining"])
|
long_time = format_time(status["long_remaining"])
|
||||||
|
|
||||||
# Determine urgency class
|
# Determine urgency class
|
||||||
if status["short_remaining"] <= 0 or status["long_remaining"] <= 0:
|
if status["short_remaining"] <= 0 or status["long_remaining"] <= 0:
|
||||||
css_class = "overdue"
|
css_class = "overdue"
|
||||||
text = f"BREAK! S:{short_time} L:{long_time}"
|
text = f"BREAK! {short_time} / {long_time}"
|
||||||
elif status["short_remaining"] < 300: # Less than 5 minutes
|
elif status["long_remaining"] < 300: # Less than 5 minutes
|
||||||
css_class = "warning"
|
css_class = "warning"
|
||||||
text = f"S:{short_time} L:{long_time}"
|
text = f"{short_time} / {long_time}"
|
||||||
else:
|
else:
|
||||||
css_class = "normal"
|
css_class = "normal"
|
||||||
text = f"S:{short_time} L:{long_time}"
|
text = f"{short_time} / {long_time}"
|
||||||
|
|
||||||
tooltip = f"Short break in {short_time}\\nLong break in {long_time}"
|
tooltip = f"Short break in {short_time}\nLong break in {long_time}"
|
||||||
|
|
||||||
elif state == "short_break":
|
elif state == "short_break":
|
||||||
break_time = format_time(status["break_remaining"])
|
break_time = format_time(status["break_remaining"])
|
||||||
css_class = "break"
|
css_class = "break"
|
||||||
text = f"Short break: {break_time}"
|
text = f"SB {break_time}"
|
||||||
tooltip = f"Short break ends in {break_time}"
|
tooltip = f"Short break ends in {break_time}"
|
||||||
|
|
||||||
elif state == "long_break":
|
elif state == "long_break":
|
||||||
break_time = format_time(status["break_remaining"])
|
break_time = format_time(status["break_remaining"])
|
||||||
css_class = "break"
|
css_class = "break"
|
||||||
text = f"Long break: {break_time}"
|
text = f"LB {break_time}"
|
||||||
tooltip = f"Long break ends in {break_time}"
|
tooltip = f"Long break ends in {break_time}"
|
||||||
|
|
||||||
elif state == "break_complete":
|
elif state == "break_complete":
|
||||||
short_time = format_time(status["short_remaining"])
|
short_time = format_time(status["short_remaining"])
|
||||||
long_time = format_time(status["long_remaining"])
|
long_time = format_time(status["long_remaining"])
|
||||||
css_class = "break"
|
css_class = "break"
|
||||||
text = f"Break done! S:{short_time} L:{long_time}"
|
text = f"{short_time} / {long_time}"
|
||||||
tooltip = "Break complete. Timers reset."
|
tooltip = "Break complete. Timers reset."
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
15
menu.xml
Normal file
15
menu.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<object class="GtkMenu" id="menu">
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuItem" id="skip-long">
|
||||||
|
<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,4 +3,6 @@
|
|||||||
# Break timer integration
|
# Break timer integration
|
||||||
exec swayidle -w \
|
exec swayidle -w \
|
||||||
timeout 5 '~/path/to/break-event idle_start' \
|
timeout 5 '~/path/to/break-event idle_start' \
|
||||||
resume '~/path/to/break-event idle_end'
|
resume '~/path/to/break-event idle_end' \
|
||||||
|
before-sleep '~/path/to/break-event idle_start' \
|
||||||
|
after-resume '~/path/to/break-event idle_end'
|
||||||
|
|||||||
@@ -3,10 +3,16 @@
|
|||||||
"custom/break-timer"
|
"custom/break-timer"
|
||||||
],
|
],
|
||||||
"custom/break-timer": {
|
"custom/break-timer": {
|
||||||
"exec": "~/path/to/break-status",
|
"exec": "break-status",
|
||||||
"return-type": "json",
|
"return-type": "json",
|
||||||
"interval": 1,
|
"interval": 1,
|
||||||
"format": "🕐 {}",
|
"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