├── .gitignore ├── LICENSE ├── README.md ├── screenshot.png ├── scrntime.py ├── setup.py └── xprintidle_scrntime.sh /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | scrntime.egg* 3 | scrntime-git-* 4 | pkg 5 | src 6 | dist 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2025, Sahaj Bhatt 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scrntime 2 | 3 | **scrntime** is a command-line tool that displays your daily screen time usage over n days. By parsing system reboot logs (using the `last reboot` command) and **subtracting idle/afk time** 4 | 5 | ![screenshot](./screenshot.png) 6 | 7 | ## Features 8 | 9 | - **Daily Screen Time Tracking:** Calculates screen time per day based on system reboot logs. 10 | - **Idle Time Integration:** Subtract idle times from the total screen time(optional). 11 | - **Customizable Visualization:** Choose multiple styles and adjust max length 12 | - **Idle Time Management:** Add idle time management using `xprintidle`(for Xorg), `hypridle` or `swayidle`. 13 | 14 | ## Prerequisites 15 | - `python` Ensure that Python 3 is installed on your system. 16 | 17 | ## Installation 18 | 19 | ### PyPi 20 | ```bash 21 | pip install scrntime 22 | #OR 23 | pipx install scrntime 24 | ``` 25 | 26 | ### Through AUR 27 | Install the [scrntime-git AUR Package](https://aur.archlinux.org/packages/scrntime-git) using an AUR helper like `yay` or install manually: 28 | 29 | ### From Source 30 | ```bash 31 | git clone https://github.com/sahaj-b/scrntime.git 32 | cd scrntime 33 | # Usage: 34 | python3 scrntime.py --help 35 | ``` 36 | 37 | #### Add scrntime to path for global usage: 38 | ```bash 39 | chmod +x scrntime.py 40 | sudo cp scrntime.py /usr/local/bin/scrntime 41 | # Usage: 42 | scrntime --help 43 | 44 | ``` 45 | 46 | ## Usage 47 | ``` 48 | usage: scrntime [-h] [-d [DAYS]] [-i] [-s NUMBER(1-9)] [-m LENGTH] [-f FILE] [-a SECONDS] 49 | 50 | Show screen time for the last n days 51 | 52 | options: 53 | -h, --help show this help message and exit 54 | -d, --days [DAYS] Number of days to show screen time for (default: 7) 55 | -i, --with-idletimes Also show idle times 56 | -s, --style NUMBER(1-9) 57 | Style for bar character (1-9) (default: 1) 58 | -m, --max-length LENGTH 59 | Specify length of longest bar to auto adjust other bars relatively (default: 40) 60 | -f, --idletime-file FILE 61 | Specify the file to read/write idletimes (default: ~/.idletimes) 62 | -a, --add-idletime SECONDS 63 | Add idle time for today (in seconds) 64 | ``` 65 | 66 | ## Setup Idle Time Management 67 | `scrntime` doesn't have a built-in idle time tracker. However, you can use the following tools with `scrntime` to track idle time by storing idle times in a file (default: `~/.idletimes`): 68 | - ### Xorg - `xprintidle` 69 | - Install [xprintidle](https://github.com/g0hl1n/xprintidle) using your package manager 70 | - Run and Autostart `xprintidle_scrntime.sh`(script in this repo) on startup 71 | 72 | - ### Hyprland - `hypridle` 73 | - Install [hypridle](https://wiki.hyprland.org/Hypr-Ecosystem/hypridle/) 74 | - Add this snippet to `~/.config/hypr/hypridle.conf`: 75 | ```conf 76 | # for sleep/suspend support 77 | general { 78 | before_sleep_cmd = echo $(date +%s) > /tmp/idle 79 | after_sleep_cmd = scrntime -a $(( $(date +%s) - $(cat /tmp/idle) )) 80 | } 81 | listener { 82 | timeout = 150 # 2.5 minutes 83 | on-timeout = echo $(( $(date +%s) - 150 )) > /tmp/idle 84 | on-resume = scrntime -a $(( $(date +%s) - $(cat /tmp/idle) )) 85 | } 86 | ``` 87 | - start `hypridle` in background by running `hypridle & disown` command 88 | - Autostart `hypridle` on startup by adding `exec-once = hypridle` to `~/.config/hypr/hyprland.conf` 89 | 90 | - ### Sway - `swayidle` 91 | - Install [swayidle](https://github.com/swaywm/swayidle) 92 | - Run this command 93 | ```bash 94 | swayidle -w \ 95 | timeout 150 'echo $(( $(date +%s) - 150 )) > /tmp/idle' \ 96 | resume 'scrntime -a $(( $(date +%s) - $(cat /tmp/idle) ))' 97 | ``` 98 | - Add this command to your sway config for autostart 99 | 100 | ## Note: 101 | This only shows the uptime of the system subtracted by the idle/afk time(which may not be fully accurate), and it doesn't have the capability to track the time spent on a particular application/window. 102 | 103 | For that purpose, you can use tools like [ActivityWatch](https://activitywatch.net/) (with [aw-watcher-window-wayland](https://github.com/ActivityWatch/aw-watcher-window-wayland) if on wayland), but I couldn't find a good cli for displaying the data on the terminal, guis are available though. 104 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahaj-b/scrntime/bb923586e43c306026efee18c1a7c52d9af131e4/screenshot.png -------------------------------------------------------------------------------- /scrntime.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | import os 3 | import sys 4 | from datetime import datetime, timedelta 5 | import argparse 6 | 7 | # defaults 8 | IDLETIME_FILE = os.path.expanduser("~") + "/.idletimes" 9 | MAX_BAR_LENGTH = 40 10 | DAYS_TO_SHOW = 7 11 | BAR_CHARACTER = "❙" # 🬋❙ 12 | COLOR_DATES = "green" 13 | COLOR_TIMES = "blue" 14 | COLOR_BARS = "blue" 15 | COLOR_IDLE_BARS = "lightgray" 16 | DATE_FORMAT = "%b %d" 17 | TIME_FORMAT = "%H:%M" 18 | SECONDS_PER_BAR = "auto" 19 | WITH_IDLETIMES = False 20 | CURRENT_TIME = datetime.now() 21 | 22 | 23 | def printTime(dayStr, durationWithIdletime, idletimeSeconds): 24 | netDuration = durationWithIdletime - timedelta(seconds=idletimeSeconds) 25 | if WITH_IDLETIMES: 26 | if durationWithIdletime.days == 1: 27 | formattedTimeStr = "24h 00m" 28 | else: 29 | formattedTimeStr = f"{f'{durationWithIdletime.total_seconds() // 3600:.0f}': >2}h {f'{durationWithIdletime.total_seconds() //60 % 60:.0f}': >2}m" 30 | else: 31 | if netDuration.days == 1: 32 | formattedTimeStr = "24h 00m" 33 | else: 34 | formattedTimeStr = f"{f'{netDuration.total_seconds() // 3600:.0f}': >2}h {f'{netDuration.total_seconds() //60 % 60:.0f}': >2}m" 35 | 36 | print( 37 | colored(dayStr, COLOR_DATES) 38 | + " " 39 | + bold(colored(formattedTimeStr, COLOR_TIMES)) 40 | + " " 41 | + ( 42 | colored( 43 | BAR_CHARACTER * int(netDuration.total_seconds() // SECONDS_PER_BAR), 44 | COLOR_BARS, 45 | ) 46 | if SECONDS_PER_BAR 47 | else "" 48 | ) 49 | + ( 50 | colored( 51 | BAR_CHARACTER * int(idletimeSeconds // SECONDS_PER_BAR), 52 | COLOR_IDLE_BARS, 53 | ) 54 | if SECONDS_PER_BAR and WITH_IDLETIMES 55 | else "" 56 | ) 57 | ) 58 | 59 | 60 | def bold(text): 61 | return f"\033[1m{text}\033[00m" 62 | 63 | 64 | def colored(text, color): 65 | if color == "red": 66 | return f"\033[91m{text}\033[00m" 67 | elif color == "green": 68 | return f"\033[92m{text}\033[00m" 69 | elif color == "yellow": 70 | return f"\033[93m{text}\033[00m" 71 | elif color == "blue": 72 | return f"\033[94m{text}\033[00m" 73 | elif color == "purple": 74 | return f"\033[95m{text}\033[00m" 75 | elif color == "cyan": 76 | return f"\033[96m{text}\033[00m" 77 | elif color == "lightgray": 78 | return f"\033[97m{text}\033[00m" 79 | else: 80 | return text 81 | 82 | 83 | def updateIdleTime(file, idleTime, idleDate): 84 | idleTimesList = [] 85 | file.seek(0) 86 | idleTimeToUpdate_str = file.readline() 87 | idleTimesList.append(idleTimeToUpdate_str) 88 | idleTimeToUpdate_split = idleTimeToUpdate_str.split(" - ") 89 | idleDateToUpdate_obj = datetime.strptime( 90 | idleTimeToUpdate_split[0] + CURRENT_TIME.strftime("%Y"), DATE_FORMAT + "%Y" 91 | ).date() 92 | while idleDateToUpdate_obj > idleDate: 93 | idleTimeToUpdate_str = file.readline() 94 | if not idleTimeToUpdate_str: 95 | idleDateToUpdate_obj = None 96 | break 97 | idleTimesList.append(idleTimeToUpdate_str) 98 | idleTimeToUpdate_split = idleTimeToUpdate_str.split(" - ") 99 | idleDateToUpdate_obj = datetime.strptime( 100 | idleTimeToUpdate_split[0] + CURRENT_TIME.strftime("%Y"), DATE_FORMAT + "%Y" 101 | ).date() 102 | 103 | if idleDateToUpdate_obj != idleDate: 104 | createIdleTime(file, idleTime, idleDate) 105 | 106 | idleTimeToUpdate_obj = timedelta( 107 | hours=int(idleTimeToUpdate_split[1].split(":")[0]), 108 | minutes=int(idleTimeToUpdate_split[1].split(":")[1]), 109 | ) 110 | 111 | newIdleTime_obj = idleTimeToUpdate_obj + idleTime 112 | if newIdleTime_obj.days > 0: 113 | print("Invalid/Impossible idletime, skipping update") 114 | sys.exit(1) 115 | 116 | newIdleTime_split = str(newIdleTime_obj).split(":") 117 | newIdleTimeStr = newIdleTime_split[0].zfill(2) + ":" + newIdleTime_split[1] 118 | print( 119 | "Updating idletime for", 120 | idleDate.strftime(DATE_FORMAT), 121 | "to " + newIdleTimeStr, 122 | ) 123 | 124 | file.seek(0) 125 | idleTimesList[-1] = idleDate.strftime(DATE_FORMAT) + " - " + newIdleTimeStr + "\n" 126 | file.writelines(idleTimesList) 127 | 128 | 129 | def createIdleTime(file, idleTime, idleDate): 130 | file.seek(0) 131 | idleDatesTimes = file.readlines() 132 | 133 | if idleTime.days == 1: 134 | newIdleTimeStr = "23:59" 135 | else: 136 | newIdleTime_split = str(idleTime).split(":") 137 | newIdleTimeStr = newIdleTime_split[0].zfill(2) + ":" + newIdleTime_split[1] 138 | 139 | newIdleDateTimeStr = f"{idleDate.strftime(DATE_FORMAT)} - {newIdleTimeStr}" 140 | 141 | insertionIndex = -1 142 | for i, idleDateTime in enumerate(idleDatesTimes): 143 | if idleDateTime: 144 | idleDate_obj = datetime.strptime( 145 | idleDateTime.split(" - ")[0] + CURRENT_TIME.strftime("%Y"), 146 | DATE_FORMAT + "%Y", 147 | ).date() 148 | if idleDate_obj < idleDate: 149 | insertionIndex = i 150 | break 151 | idleDatesTimes.insert(insertionIndex, newIdleDateTimeStr + "\n") 152 | print( 153 | "Creating new idletime for ", 154 | idleDate.strftime(DATE_FORMAT), 155 | ": " + newIdleTimeStr, 156 | ) 157 | 158 | file.seek(0) 159 | file.writelines(idleDatesTimes) 160 | 161 | 162 | def addIdleTimeToFile(idleTime_seconds, idleDate=CURRENT_TIME.date()): 163 | idleTime = timedelta(seconds=idleTime_seconds) 164 | if idleDate == CURRENT_TIME.date(): 165 | elapsedTimeForIdleDate = timedelta( 166 | hours=CURRENT_TIME.hour, minutes=CURRENT_TIME.minute 167 | ) 168 | else: 169 | elapsedTimeForIdleDate = timedelta(hours=24) 170 | 171 | idleTimeForPreviousDay = idleTime - elapsedTimeForIdleDate 172 | 173 | # print("For ", idleDate.strftime(DATE_FORMAT)) 174 | # print("Idle time:", idleTime) 175 | # print("Elapsed time:", elapsedTimeForIdleDate) 176 | # print("Idle time for previous day:", idleTimeForPreviousDay) 177 | 178 | if idleTimeForPreviousDay > timedelta(0): 179 | print("Idletime for previous day detected, splitting...") 180 | addIdleTimeToFile( 181 | idleTimeForPreviousDay.total_seconds(), idleDate - timedelta(days=1) 182 | ) 183 | idleTime = elapsedTimeForIdleDate 184 | with open(IDLETIME_FILE, "r+") as file: 185 | latestIdleStr = file.readline() 186 | if latestIdleStr: 187 | latestIdleDate_obj = datetime.strptime( 188 | latestIdleStr.split(" - ")[0] + CURRENT_TIME.strftime("%Y"), 189 | DATE_FORMAT + "%Y", 190 | ).date() 191 | else: 192 | createIdleTime(file, idleTime, idleDate) 193 | return 194 | if latestIdleDate_obj >= idleDate: 195 | updateIdleTime(file, idleTime, idleDate) 196 | else: 197 | createIdleTime(file, idleTime, idleDate) 198 | 199 | 200 | def parseArgs(): 201 | global IDLETIME_FILE, MAX_BAR_LENGTH, BAR_CHARACTER, DAYS_TO_SHOW, WITH_IDLETIMES 202 | parser = argparse.ArgumentParser(description="Show screen time for the last n days") 203 | parser.add_argument( 204 | "-d", 205 | "--days", 206 | type=int, 207 | nargs="?", 208 | help="Number of days to show screen time for (default: %(default)d)", 209 | default=DAYS_TO_SHOW, 210 | ) 211 | parser.add_argument( 212 | "-i", 213 | "--with-idletimes", 214 | help="Also show idle times", 215 | action="store_true", 216 | ) 217 | parser.add_argument( 218 | "-s", 219 | "--style", 220 | type=int, 221 | help="Style for bar character (1-9) (default: 1)", 222 | default=1, 223 | metavar="NUMBER(1-9)", 224 | ) 225 | parser.add_argument( 226 | "-m", 227 | "--max-length", 228 | type=int, 229 | help="Specify length of longest bar to auto adjust other bars relatively (default: %(default)d)", 230 | default=MAX_BAR_LENGTH, 231 | metavar="LENGTH", 232 | ) 233 | parser.add_argument( 234 | "-f", 235 | "--idletime-file", 236 | type=argparse.FileType("a+"), 237 | help="Specify the file to read/write idletimes (default: %(default)s)", 238 | default=IDLETIME_FILE, 239 | metavar="FILE", 240 | ) 241 | parser.add_argument( 242 | "-a", 243 | "--add-idletime", 244 | type=int, 245 | help="Add idle time for today (in seconds)", 246 | metavar="SECONDS", 247 | ) 248 | args = parser.parse_args() 249 | 250 | if args.add_idletime: 251 | addIdleTimeToFile(args.add_idletime) 252 | sys.exit(0) 253 | 254 | IDLETIME_FILE = args.idletime_file.name 255 | MAX_BAR_LENGTH = args.max_length 256 | DAYS_TO_SHOW = args.days 257 | WITH_IDLETIMES = args.with_idletimes 258 | 259 | match args.style: 260 | case 1: 261 | BAR_CHARACTER = "❙" 262 | case 2: 263 | BAR_CHARACTER = "🬋" 264 | case 3: 265 | BAR_CHARACTER = "▆" 266 | case 4: 267 | BAR_CHARACTER = "❘" 268 | case 5: 269 | BAR_CHARACTER = "❚" 270 | case 6: 271 | BAR_CHARACTER = "█" 272 | case 7: 273 | BAR_CHARACTER = "━" 274 | case 8: 275 | BAR_CHARACTER = "▭" 276 | case 9: 277 | BAR_CHARACTER = "╼" 278 | 279 | 280 | def updateTimePerDayDict(timePerDayDict, rebootDateObj, rebootDuration): 281 | rebootDurationWithoutDays = timedelta( 282 | hours=rebootDuration.seconds // 3600, minutes=rebootDuration.seconds // 60 % 60 283 | ) 284 | if rebootDuration.days > 0: 285 | for i in range(1, rebootDuration.days + 1): 286 | timePerDayDict[(rebootDateObj + timedelta(days=i))] = timedelta(days=1) 287 | try: 288 | timePerDayDict[rebootDateObj] += rebootDurationWithoutDays 289 | except KeyError: 290 | timePerDayDict[rebootDateObj] = rebootDurationWithoutDays 291 | 292 | 293 | def fillMissingDaysWithZeroTime(timePerDayDict, rebootDateObj): 294 | if rebootDateObj not in timePerDayDict: 295 | latestZeroTimeDay = rebootDateObj + timedelta(days=1) 296 | oldestZeroTimeDay = latestZeroTimeDay 297 | while ( 298 | oldestZeroTimeDay not in timePerDayDict 299 | and oldestZeroTimeDay <= CURRENT_TIME.date() 300 | ): 301 | oldestZeroTimeDay += timedelta(days=1) 302 | for i in range(1, (oldestZeroTimeDay - latestZeroTimeDay).days + 1): 303 | if len(timePerDayDict) == DAYS_TO_SHOW: 304 | return 305 | updateTimePerDayDict( 306 | timePerDayDict, oldestZeroTimeDay - timedelta(days=i), timedelta(0) 307 | ) 308 | 309 | 310 | def handleRunningRebootLine(latestRunningRebootLine, timePerDayDict): 311 | runningRebootDateStr = " ".join(latestRunningRebootLine[5:7]) 312 | runningRebootDateStr = runningRebootDateStr[:4] + runningRebootDateStr[4:].zfill(2) 313 | runningRebootDateObj = datetime.strptime( 314 | runningRebootDateStr + str(CURRENT_TIME.year), "%b %d%Y" 315 | ).date() 316 | rebootTime = datetime.strptime( 317 | f"{latestRunningRebootLine[-3]} {runningRebootDateStr} {CURRENT_TIME.year}", 318 | "%H:%M %b %d %Y", 319 | ) 320 | updateTimePerDayDict( 321 | timePerDayDict, runningRebootDateObj, CURRENT_TIME - rebootTime 322 | ) 323 | 324 | 325 | def parseRebootLogs(): 326 | rebootOutput = os.popen("last reboot") 327 | rebootLine = rebootOutput.readline()[:-1] 328 | timePerDayDict = {} 329 | latestRunningRebootLine = None 330 | 331 | while rebootLine: 332 | rebootLine = rebootLine.split() 333 | rebootDateStr = " ".join(rebootLine[5:7]) 334 | rebootDateStr = rebootDateStr[:4] + rebootDateStr[4:].zfill(2) 335 | rebootDateObj = datetime.strptime( 336 | rebootDateStr + str(CURRENT_TIME.year), "%b %d%Y" 337 | ).date() 338 | 339 | if rebootDateObj not in timePerDayDict and len(timePerDayDict) > DAYS_TO_SHOW: 340 | return timePerDayDict 341 | 342 | fillMissingDaysWithZeroTime(timePerDayDict, rebootDateObj) 343 | 344 | if len(rebootLine) < 3: 345 | break 346 | if rebootLine[-1] == "running": 347 | latestRunningRebootLine = rebootLine 348 | rebootLine = rebootOutput.readline() 349 | continue 350 | 351 | if latestRunningRebootLine: 352 | handleRunningRebootLine(latestRunningRebootLine, timePerDayDict) 353 | latestRunningRebootLine = None 354 | 355 | try: 356 | # (HH:MM) 357 | rebootTimeObj = datetime.strptime(rebootLine[-1], "(%H:%M)") 358 | rebootDuration = timedelta( 359 | hours=rebootTimeObj.hour, 360 | minutes=rebootTimeObj.minute, 361 | ) 362 | except ValueError: 363 | try: 364 | # (DD+HH:MM) 365 | rebootTimeObj = datetime.strptime( 366 | f"{rebootLine[-1][1:-1].zfill(8)} {CURRENT_TIME.year}", 367 | "%d+%H:%M %Y", 368 | ) 369 | rebootDuration = timedelta( 370 | days=rebootTimeObj.day, 371 | hours=rebootTimeObj.hour, 372 | minutes=rebootTimeObj.minute, 373 | ) 374 | except ValueError: 375 | # weird durations like (-HH:MM) 376 | rebootDuration = timedelta(0) 377 | 378 | updateTimePerDayDict(timePerDayDict, rebootDateObj, rebootDuration) 379 | rebootLine = rebootOutput.readline()[:-1] 380 | 381 | return timePerDayDict 382 | 383 | 384 | def parseIdleTimes(timePerDayDict): 385 | try: 386 | with open(IDLETIME_FILE, "r") as file: 387 | idletimesDict = {} 388 | idletimes = file.read().split("\n") 389 | for idletime in idletimes: 390 | if idletime: 391 | idletime = idletime.split(" - ") 392 | idleDateStr = idletime[0].strip() 393 | idleDateObj = datetime.strptime( 394 | idleDateStr + str(CURRENT_TIME.year), DATE_FORMAT + "%Y" 395 | ).date() 396 | if ( 397 | idleDateObj not in timePerDayDict 398 | and len(timePerDayDict) == DAYS_TO_SHOW 399 | ): 400 | break 401 | idletimeObj = datetime.strptime(idletime[1], TIME_FORMAT) 402 | idletimeObj = timedelta( 403 | hours=idletimeObj.hour, minutes=idletimeObj.minute 404 | ) 405 | idletimesDict[idleDateObj] = idletimeObj 406 | except FileNotFoundError: 407 | print(colored(bold("No idletime file found"), "red")) 408 | idletimesDict = {} 409 | return idletimesDict 410 | 411 | 412 | def getSecondsPerBar(timePerDayDict, idletimesDict): 413 | maxTime = timedelta(0) 414 | count = 0 415 | for day in timePerDayDict: 416 | count += 1 417 | if count > DAYS_TO_SHOW: 418 | break 419 | netDuration = timePerDayDict[day] - ( 420 | timedelta(0) if WITH_IDLETIMES else (idletimesDict.get(day, timedelta(0))) 421 | ) 422 | if netDuration > maxTime: 423 | maxTime = netDuration 424 | return maxTime.total_seconds() / MAX_BAR_LENGTH 425 | 426 | 427 | def printAllDays(timePerDayDict, idletimesDict): 428 | count = 0 429 | for day in timePerDayDict: 430 | count += 1 431 | if count > DAYS_TO_SHOW: 432 | break 433 | printTime( 434 | day.strftime(DATE_FORMAT), 435 | timePerDayDict[day], 436 | idletimesDict.get(day, timedelta(0)).total_seconds(), 437 | ) 438 | 439 | 440 | def getTotalTimeAndDays(timePerDayDict, idletimesDict): 441 | totalTime = timedelta(0) 442 | numOfDays = 0 443 | for day in timePerDayDict: 444 | numOfDays += 1 445 | if numOfDays > DAYS_TO_SHOW: 446 | break 447 | totalTime += timePerDayDict[day] - ( 448 | timedelta(0) if WITH_IDLETIMES else idletimesDict.get(day, timedelta(0)) 449 | ) 450 | return totalTime, numOfDays - 1 451 | 452 | 453 | def printTotalTime(totalTime, numOfDays): 454 | print( 455 | colored("Total (", "cyan") 456 | + colored(numOfDays, COLOR_TIMES) 457 | + colored(" days): ", "cyan") 458 | + bold( 459 | colored( 460 | f"{str(totalTime.days)+'d ' if totalTime.days else ''}{totalTime.seconds // 3600}h {totalTime.seconds // 60 % 60}m", 461 | "yellow", 462 | ) 463 | ) 464 | + ( 465 | colored(" (including idle times): ", COLOR_DATES) 466 | if WITH_IDLETIMES 467 | else colored(": ", COLOR_DATES) 468 | ) 469 | ) 470 | 471 | 472 | def printAverageTime(timePerDayDict, totalTime): 473 | avgTime = totalTime / len(timePerDayDict) 474 | print( 475 | colored("Average: ", "cyan") 476 | + bold( 477 | colored( 478 | f"{str(avgTime.days)+'d ' if avgTime.days else ''}{avgTime.seconds // 3600}h {avgTime.seconds // 60 % 60}m", 479 | "yellow", 480 | ) 481 | ) 482 | ) 483 | 484 | 485 | def main(): 486 | global SECONDS_PER_BAR 487 | 488 | parseArgs() 489 | timePerDayDict = parseRebootLogs() 490 | idletimesDict = parseIdleTimes(timePerDayDict) 491 | if SECONDS_PER_BAR == "auto": 492 | SECONDS_PER_BAR = getSecondsPerBar(timePerDayDict, idletimesDict) 493 | printAllDays(timePerDayDict, idletimesDict) 494 | totalTime, numOfDays = getTotalTimeAndDays(timePerDayDict, idletimesDict) 495 | printTotalTime(totalTime, numOfDays) 496 | printAverageTime(timePerDayDict, totalTime) 497 | 498 | 499 | if __name__ == "__main__": 500 | main() 501 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="scrntime", 5 | version="1.4.1", 6 | py_modules=["scrntime"], 7 | entry_points={ 8 | "console_scripts": [ 9 | "scrntime=scrntime:main", 10 | ], 11 | }, 12 | install_requires=[], 13 | author="Sahaj Bhatt", 14 | author_email="sahajb0606@gmail.com", 15 | description="A CLI for displaying daily screentime with afk/idle time support", 16 | url="https://github.com/sahaj-b/scrntime.git", 17 | license="BSD", 18 | classifiers=[ 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: BSD License", 21 | "Operating System :: POSIX :: Linux", 22 | ], 23 | ) 24 | -------------------------------------------------------------------------------- /xprintidle_scrntime.sh: -------------------------------------------------------------------------------- 1 | #!/bin/env bash 2 | 3 | IDLE_THRESHOLD=150 4 | 5 | while true; do 6 | idle_ms=$(xprintidle) 7 | idle_sec=$((idle_ms / 1000)) 8 | echo "Idle time: $idle_sec seconds" 9 | 10 | if [ $idle_sec -ge $IDLE_THRESHOLD ]; then 11 | start_time=$(date +%s) 12 | echo "Idle state detected. Monitoring until activity resumes..." 13 | 14 | while [ $idle_sec -ge $IDLE_THRESHOLD ]; do 15 | idle_ms=$(xprintidle) 16 | idle_sec=$((idle_ms / 1000)) 17 | sleep 5 18 | done 19 | 20 | end_time=$(date +%s) 21 | idle_duration=$((end_time - start_time)) 22 | echo "Activity resumed after $idle_duration seconds of idle time." 23 | 24 | scrntime -a "$idle_duration" 25 | fi 26 | 27 | sleep 45 28 | done 29 | --------------------------------------------------------------------------------