├── .gitignore ├── LICENSE ├── README.md ├── alfred ├── README.md └── Timestamp Record.alfredworkflow ├── assets ├── timeline.css ├── timelinecss.py ├── tsr-alfred-installation.gif ├── tsr-raycast-installation.gif ├── tsr-raycast-tse.gif ├── tsr-raycast-tsl.gif ├── tsr-raycast-tsn.gif ├── tsr-raycast-tsr-full.gif ├── tsr-raycast-tsr.gif └── tsr-raycast-tsv.gif └── raycast ├── tse.py ├── tsl.py ├── tsn.py ├── tsr.py └── tsv.py /.gitignore: -------------------------------------------------------------------------------- 1 | records/ 2 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 techbranch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Low-overhead time recorder 2 | 3 | Least invasive, csv based, linear time recorder that doesn't keep your data hostage. 4 | 5 | Your csv files are stored in the `~/tsr` directory 6 | 7 | ### Demo with a few entries and visualisation 8 | 9 |

10 | 11 |

12 | 13 | ### Motivation 14 | 15 | In my daily work I often struggle with frequent context switches and stopping to write documentation. This tool helps me alleviate both of those problems. 16 | 17 | `tsr` lets me easily mark the context switches, making them much more consious and observable. Having a written record helps me measure the real scale of the problem and pinpoint productivity sinks. 18 | 19 | `tsn` allows me to write notes as I go. This at least gives me a skeleton documentation once a feature is done, minimising the effort needed to write a proper document later. 20 | 21 | I tried a few other tools over the years, but the number of features they offer often distracted me from using it efficiently, making time tracking an effort on its own. 22 | 23 | While `tsr` may not have an extensive list of features, it has proven to be effective for my needs, and I believe it may also benefit others in similar situations. 24 | 25 | ### Details and usage 26 | 27 | This tool uses a simple two-column csv format for storing records 28 | 29 | ``` 30 | datetime,tags delimited by space 31 | ``` 32 | 33 | Each record should mark a newly started activity, that way context switches are much more consious and observable. 34 | 35 | The tag has a default value of 'next' to minimise the time required to operate the tool, you can edit the records manually later. 36 | 37 | ``` 38 | 'tsr' 39 | ``` 40 | 41 | Adding new activities with tags is also quite simple 42 | 43 | ``` 44 | tsr vpn connector 45 | tsr client migration 46 | tsr lunch 47 | tsr incident interrupted 2431 48 | tsr work eod 49 | ``` 50 | 51 | This would result in creating a file `record.csv` with the following content: 52 | ``` 53 | 2023-03-17 10:01:55.000000,vpn connector 54 | 2023-03-17 12:34:00.000000,client migration 55 | 2023-03-17 14:36:00.000000,lunch 56 | 2023-03-17 15:03:55.000000,incident interrupted 2431 57 | 2023-03-17 17:46:57.000000,work eod 58 | ``` 59 | 60 | Based on the output, you can create custom reports and do your visualisation wizardry 61 | 62 | ## Available options 63 | 64 | ### TSR - Record 65 | 66 | Creates a new timestamped entry in the `records.csv` file. 67 | 68 |

69 | 70 |

71 | 72 | ### TSN - Note 73 | 74 | Creates a timestamped note in the `notes.csv` file. 75 | 76 | Notes are attached to the record they correspond to in the `tsv` view. 77 | 78 |

79 | 80 |

81 | 82 | ### TSL - Latest 83 | 84 | Displays the latest entry and its duration. 85 | 86 |

87 | 88 |

89 | 90 | ### TSV - View 91 | 92 | Builds a self-contained offline HTML page that displays entries on a timeline. 93 | 94 | Once built, you can use the html file on its own as it doesn't have any external dependencies. 95 | 96 | Since this is statically built, the page will not self-update with new entries. 97 | You'll have to use `tsv` each time you want to see the up-to-date timeline view. 98 | 99 | This does not start a webserver of any kind, it's a completely static HTML page. 100 | 101 |

102 | 103 |

104 | 105 | ### TSE - Edit 106 | 107 | Simply opens Finder in the records directory so you can edit or lookup files manually 108 | 109 |

110 | 111 |

112 | 113 | ## Installation 114 | 115 | This tool is available for both `Alfred` and `Raycast`. 116 | 117 | ### Raycast 118 | 119 | Installing the extension is as simple as pointing Raycast at the directory where the scripts are. 120 | 121 | You can clone this repository or download it as a zip, then point Raycast Extensions at the `raycast` directory where you downloaded this repository. 122 | 123 |

124 | 125 |

126 | 127 | ### Alfred 128 | 129 | Installing the workflow is as simple as double clicking the `Timestamp Record.alfredworkflow` package in the `alfred` directory and hitting "Install" once in Alfred's installation window. 130 | 131 |

132 | 133 |

134 | -------------------------------------------------------------------------------- /alfred/README.md: -------------------------------------------------------------------------------- 1 | ### Installation 2 | 3 | Installing the workflow is as simple as double clicking the `Timestamp Record.alfredworkflow` package here in this directory and hitting "Install" once in Alfred's installation window. 4 | 5 |

6 | 7 |

8 | -------------------------------------------------------------------------------- /alfred/Timestamp Record.alfredworkflow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tech-branch/tsr/9df00da38cfbc02887d4dae47cc3f641880e0b31/alfred/Timestamp Record.alfredworkflow -------------------------------------------------------------------------------- /assets/timeline.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | /* Set a background color */ 6 | body { 7 | background-color: #3B4252; 8 | font-family: Helvetica, sans-serif; 9 | } 10 | 11 | /* The actual timeline (the vertical ruler) */ 12 | .timeline { 13 | position: relative; 14 | max-width: 1200px; 15 | margin: 0 auto; 16 | } 17 | 18 | /* The actual timeline (the vertical ruler) */ 19 | .timeline::after { 20 | content: ''; 21 | position: absolute; 22 | width: 6px; 23 | background-color: #ECEFF4; 24 | top: 0; 25 | bottom: 0; 26 | left: 50%; 27 | margin-left: -3px; 28 | } 29 | 30 | /* Container around content */ 31 | .container { 32 | padding: 10px 40px; 33 | position: relative; 34 | background-color: inherit; 35 | width: 50%; 36 | } 37 | 38 | /* The circles on the timeline */ 39 | .container::after { 40 | content: ''; 41 | position: absolute; 42 | width: 25px; 43 | height: 25px; 44 | right: -17px; 45 | background-color: #ECEFF4; 46 | border: 4px solid #81A1C1; 47 | top: 15px; 48 | border-radius: 50%; 49 | z-index: 1; 50 | } 51 | 52 | /* Place the container to the left */ 53 | .left { 54 | left: 0; 55 | } 56 | 57 | /* Place the container to the right */ 58 | .right { 59 | left: 50%; 60 | } 61 | 62 | /* Add arrows to the left container (pointing right) */ 63 | .left::before { 64 | content: " "; 65 | height: 0; 66 | position: absolute; 67 | top: 22px; 68 | width: 0; 69 | z-index: 1; 70 | right: 30px; 71 | border: medium solid #ECEFF4; 72 | border-width: 10px 0 10px 10px; 73 | border-color: transparent transparent transparent #ECEFF4; 74 | } 75 | 76 | /* Add arrows to the right container (pointing left) */ 77 | .right::before { 78 | content: " "; 79 | height: 0; 80 | position: absolute; 81 | top: 22px; 82 | width: 0; 83 | z-index: 1; 84 | left: 30px; 85 | border: medium solid #ECEFF4; 86 | border-width: 10px 10px 10px 0; 87 | border-color: transparent #ECEFF4 transparent transparent; 88 | } 89 | 90 | /* Fix the circle for containers on the right side */ 91 | .right::after { 92 | left: -16px; 93 | } 94 | 95 | /* The actual content */ 96 | .content { 97 | padding: 20px 30px; 98 | background-color: #ECEFF4; 99 | position: relative; 100 | border-radius: 6px; 101 | } 102 | 103 | /* Media queries - Responsive timeline on screens less than 600px wide */ 104 | @media screen and (max-width: 600px) { 105 | /* Place the timelime to the left */ 106 | .timeline::after { 107 | left: 31px; 108 | } 109 | 110 | /* Full-width containers */ 111 | .container { 112 | width: 100%; 113 | padding-left: 70px; 114 | padding-right: 25px; 115 | } 116 | 117 | /* Make sure that all arrows are pointing leftwards */ 118 | .container::before { 119 | left: 60px; 120 | border: medium solid #ECEFF4; 121 | border-width: 10px 10px 10px 0; 122 | border-color: transparent #ECEFF4 transparent transparent; 123 | } 124 | 125 | /* Make sure all circles are at the same spot */ 126 | .left::after, .right::after { 127 | left: 15px; 128 | } 129 | 130 | /* Make all right containers behave like the left ones */ 131 | .right { 132 | left: 0%; 133 | } 134 | } -------------------------------------------------------------------------------- /assets/timelinecss.py: -------------------------------------------------------------------------------- 1 | # This is for the Alfred extension to embed the CSS within the package 2 | 3 | TIMELINE_CSS = """ 4 | * { 5 | box-sizing: border-box; 6 | } 7 | 8 | /* Set a background color */ 9 | body { 10 | background-color: #3B4252; 11 | font-family: Helvetica, sans-serif; 12 | } 13 | 14 | /* The actual timeline (the vertical ruler) */ 15 | .timeline { 16 | position: relative; 17 | max-width: 1200px; 18 | margin: 0 auto; 19 | } 20 | 21 | /* The actual timeline (the vertical ruler) */ 22 | .timeline::after { 23 | content: ''; 24 | position: absolute; 25 | width: 6px; 26 | background-color: #ECEFF4; 27 | top: 0; 28 | bottom: 0; 29 | left: 50%; 30 | margin-left: -3px; 31 | } 32 | 33 | /* Container around content */ 34 | .container { 35 | padding: 10px 40px; 36 | position: relative; 37 | background-color: inherit; 38 | width: 50%; 39 | } 40 | 41 | /* The circles on the timeline */ 42 | .container::after { 43 | content: ''; 44 | position: absolute; 45 | width: 25px; 46 | height: 25px; 47 | right: -17px; 48 | background-color: #ECEFF4; 49 | border: 4px solid #81A1C1; 50 | top: 15px; 51 | border-radius: 50%; 52 | z-index: 1; 53 | } 54 | 55 | /* Place the container to the left */ 56 | .left { 57 | left: 0; 58 | } 59 | 60 | /* Place the container to the right */ 61 | .right { 62 | left: 50%; 63 | } 64 | 65 | /* Add arrows to the left container (pointing right) */ 66 | .left::before { 67 | content: " "; 68 | height: 0; 69 | position: absolute; 70 | top: 22px; 71 | width: 0; 72 | z-index: 1; 73 | right: 30px; 74 | border: medium solid #ECEFF4; 75 | border-width: 10px 0 10px 10px; 76 | border-color: transparent transparent transparent #ECEFF4; 77 | } 78 | 79 | /* Add arrows to the right container (pointing left) */ 80 | .right::before { 81 | content: " "; 82 | height: 0; 83 | position: absolute; 84 | top: 22px; 85 | width: 0; 86 | z-index: 1; 87 | left: 30px; 88 | border: medium solid #ECEFF4; 89 | border-width: 10px 10px 10px 0; 90 | border-color: transparent #ECEFF4 transparent transparent; 91 | } 92 | 93 | /* Fix the circle for containers on the right side */ 94 | .right::after { 95 | left: -16px; 96 | } 97 | 98 | /* The actual content */ 99 | .content { 100 | padding: 20px 30px; 101 | background-color: #ECEFF4; 102 | position: relative; 103 | border-radius: 6px; 104 | } 105 | 106 | /* Media queries - Responsive timeline on screens less than 600px wide */ 107 | @media screen and (max-width: 600px) { 108 | /* Place the timelime to the left */ 109 | .timeline::after { 110 | left: 31px; 111 | } 112 | 113 | /* Full-width containers */ 114 | .container { 115 | width: 100%; 116 | padding-left: 70px; 117 | padding-right: 25px; 118 | } 119 | 120 | /* Make sure that all arrows are pointing leftwards */ 121 | .container::before { 122 | left: 60px; 123 | border: medium solid #ECEFF4; 124 | border-width: 10px 10px 10px 0; 125 | border-color: transparent #ECEFF4 transparent transparent; 126 | } 127 | 128 | /* Make sure all circles are at the same spot */ 129 | .left::after, .right::after { 130 | left: 15px; 131 | } 132 | 133 | /* Make all right containers behave like the left ones */ 134 | .right { 135 | left: 0%; 136 | } 137 | } 138 | """ -------------------------------------------------------------------------------- /assets/tsr-alfred-installation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tech-branch/tsr/9df00da38cfbc02887d4dae47cc3f641880e0b31/assets/tsr-alfred-installation.gif -------------------------------------------------------------------------------- /assets/tsr-raycast-installation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tech-branch/tsr/9df00da38cfbc02887d4dae47cc3f641880e0b31/assets/tsr-raycast-installation.gif -------------------------------------------------------------------------------- /assets/tsr-raycast-tse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tech-branch/tsr/9df00da38cfbc02887d4dae47cc3f641880e0b31/assets/tsr-raycast-tse.gif -------------------------------------------------------------------------------- /assets/tsr-raycast-tsl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tech-branch/tsr/9df00da38cfbc02887d4dae47cc3f641880e0b31/assets/tsr-raycast-tsl.gif -------------------------------------------------------------------------------- /assets/tsr-raycast-tsn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tech-branch/tsr/9df00da38cfbc02887d4dae47cc3f641880e0b31/assets/tsr-raycast-tsn.gif -------------------------------------------------------------------------------- /assets/tsr-raycast-tsr-full.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tech-branch/tsr/9df00da38cfbc02887d4dae47cc3f641880e0b31/assets/tsr-raycast-tsr-full.gif -------------------------------------------------------------------------------- /assets/tsr-raycast-tsr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tech-branch/tsr/9df00da38cfbc02887d4dae47cc3f641880e0b31/assets/tsr-raycast-tsr.gif -------------------------------------------------------------------------------- /assets/tsr-raycast-tsv.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tech-branch/tsr/9df00da38cfbc02887d4dae47cc3f641880e0b31/assets/tsr-raycast-tsv.gif -------------------------------------------------------------------------------- /raycast/tse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env /usr/bin/python3 2 | 3 | # Required parameters: 4 | # @raycast.schemaVersion 1 5 | # @raycast.title tse 6 | # @raycast.mode compact 7 | 8 | # Optional parameters: 9 | # @raycast.icon 📝 10 | # @raycast.packageName Timestamp Recorder Editor 11 | 12 | # Documentation: 13 | # @raycast.description Edit your TSR entries 14 | # @raycast.author Tomasz Sobota 15 | # @raycast.authorURL https://techbranch.net 16 | 17 | import webbrowser 18 | import os 19 | 20 | home_path = os.path.expanduser('~') 21 | REPOSITORY_PATH = home_path+"/tsr" 22 | 23 | webbrowser.open('file://'+os.path.realpath(REPOSITORY_PATH)) 24 | print("Opening") 25 | -------------------------------------------------------------------------------- /raycast/tsl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env /usr/bin/python3 2 | 3 | # Required parameters: 4 | # @raycast.schemaVersion 1 5 | # @raycast.title tsl 6 | # @raycast.mode compact 7 | 8 | # Optional parameters: 9 | # @raycast.icon ⏰ 10 | # @raycast.packageName Timestamp Recorder Current 11 | 12 | # Documentation: 13 | # @raycast.description Check what's your latest activity and how much time passed 14 | # @raycast.author Tomasz Sobota 15 | # @raycast.authorURL https://techbranch.net 16 | 17 | import datetime 18 | import csv 19 | import os 20 | 21 | # ------------ 22 | # PARAMETERS 23 | # ------------ 24 | # modify to your preference 25 | 26 | home_path = os.path.expanduser('~') 27 | FILE_PATH = home_path+"/tsr/record.csv" 28 | 29 | # --------- 30 | 31 | current_timestamp = datetime.datetime.now() 32 | 33 | output = f"" 34 | 35 | # make sure directories exist 36 | os.makedirs(os.path.dirname(FILE_PATH), exist_ok=True) 37 | 38 | with open(FILE_PATH, "r") as csvfile: 39 | reader = csv.reader(csvfile) 40 | for row in reader: 41 | 42 | if len(row) == 0: 43 | # omit empty rows 44 | continue 45 | 46 | raw_datetime = row[0] 47 | raw_tags = row[1] 48 | 49 | parsed_datetime = datetime.datetime.fromisoformat(raw_datetime) 50 | dt_delta = current_timestamp - parsed_datetime 51 | dt_total_minutes = int(round(dt_delta.total_seconds()/60, 0)) 52 | dt_hours = int(dt_total_minutes/60) 53 | dt_minutes = dt_total_minutes-dt_hours*60 54 | 55 | delta_pretty = f"{dt_hours}h {dt_minutes}m" 56 | # that way we'll only see the last entry: 57 | output = f"{raw_tags} since {delta_pretty} ago" 58 | 59 | print(output) 60 | -------------------------------------------------------------------------------- /raycast/tsn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env /usr/bin/python3 2 | 3 | # Required parameters: 4 | # @raycast.schemaVersion 1 5 | # @raycast.title tsn 6 | # @raycast.mode compact 7 | 8 | # Optional parameters: 9 | # @raycast.icon 📝 10 | # @raycast.argument1 { "type": "text", "name":"notes", "placeholder": "note content" } 11 | # @raycast.packageName Timestamp Recorder Notes 12 | 13 | # Documentation: 14 | # @raycast.description Simple file-based notes for the ts recorder 15 | # @raycast.author Tomasz Sobota 16 | # @raycast.authorURL https://techbranch.net 17 | 18 | import datetime 19 | import sys 20 | import os 21 | 22 | # ------------ 23 | # PARAMETERS 24 | # ------------ 25 | # modify to your preference 26 | 27 | home_path = os.path.expanduser('~') 28 | FILE_PATH = home_path+"/tsr/notes.csv" 29 | 30 | 31 | # ---------------------------- 32 | # Read the script parameters 33 | # 34 | 35 | notes = "" 36 | 37 | try: 38 | # read notes from input 39 | notes = sys.argv[1] 40 | except IndexError: 41 | # no notes provided 42 | notes = "no note provided" 43 | 44 | if notes == "": 45 | # if we're still seeing empty notes list 46 | notes = "no note provided" 47 | 48 | # --------- 49 | 50 | timestamp = str(datetime.datetime.now()) 51 | 52 | output = f"\n{timestamp},{notes}" 53 | 54 | # make sure directories exist 55 | os.makedirs(os.path.dirname(FILE_PATH), exist_ok=True) 56 | 57 | # plain text output 58 | text_file = open(FILE_PATH, "a") 59 | _ = text_file.write(output) 60 | text_file.close() 61 | -------------------------------------------------------------------------------- /raycast/tsr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env /usr/bin/python3 2 | 3 | # Required parameters: 4 | # @raycast.schemaVersion 1 5 | # @raycast.title tsr 6 | # @raycast.mode compact 7 | 8 | # Optional parameters: 9 | # @raycast.icon ⏰ 10 | # @raycast.argument1 { "type": "text", "name":"tags", "placeholder": "optional tags", "optional": true } 11 | # @raycast.packageName Timestamp Recorder 12 | 13 | # Documentation: 14 | # @raycast.description Simple file-based ts recorder with tags 15 | # @raycast.author Tomasz Sobota 16 | # @raycast.authorURL https://techbranch.net 17 | 18 | import datetime 19 | import sys 20 | import os 21 | 22 | # ------------ 23 | # PARAMETERS 24 | # ------------ 25 | # modify to your preference 26 | 27 | home_path = os.path.expanduser('~') 28 | FILE_PATH = home_path+"/tsr/record.csv" 29 | 30 | 31 | # ---------------------------- 32 | # Read the script parameters 33 | # 34 | 35 | tags = "" 36 | 37 | try: 38 | # read tags from input 39 | tags = sys.argv[1] 40 | except IndexError: 41 | # no tags provided 42 | tags = "next" 43 | 44 | if tags == "": 45 | # if we're still seeing empty tags list 46 | tags = "next" 47 | 48 | # --------- 49 | 50 | timestamp = str(datetime.datetime.now()) 51 | 52 | output = f"\n{timestamp},{tags}" 53 | 54 | # make sure directories exist 55 | os.makedirs(os.path.dirname(FILE_PATH), exist_ok=True) 56 | 57 | # plain text output 58 | text_file = open(FILE_PATH, "a") 59 | _ = text_file.write(output) 60 | text_file.close() 61 | -------------------------------------------------------------------------------- /raycast/tsv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env /usr/bin/python3 2 | 3 | # Required parameters: 4 | # @raycast.schemaVersion 1 5 | # @raycast.title tsv 6 | # @raycast.mode compact 7 | 8 | # Optional parameters: 9 | # @raycast.icon ⏰ 10 | # @raycast.packageName Timestamp Recorder HTML view 11 | 12 | # Documentation: 13 | # @raycast.description Compile your records into a timeline 14 | # @raycast.author Tomasz Sobota 15 | # @raycast.authorURL https://techbranch.net 16 | 17 | import datetime 18 | import webbrowser 19 | import csv 20 | import os 21 | 22 | 23 | class RecordType: 24 | TSR = 1 25 | TSN = 2 26 | 27 | class RecordSide: 28 | LEFT = "left" 29 | RIGHT = "right" 30 | 31 | 32 | # ------------ 33 | # PARAMETERS 34 | # ------------ 35 | # modify to your preference 36 | 37 | home_path = os.path.expanduser('~') 38 | TSR_FILE_PATH = home_path+"/tsr/record.csv" 39 | TSN_FILE_PATH = home_path+"/tsr/notes.csv" 40 | HTML_OUTPUT_PATH = home_path+"/tsr/record.html" 41 | 42 | CSS_ASSETS_PATH = "../assets/timeline.css" 43 | 44 | DEFAULT_ENTRY_SIDE = RecordSide.LEFT 45 | 46 | # --------- 47 | # Helpers 48 | # --------- 49 | 50 | 51 | # function assigning side to a record type 52 | def get_timeline_side(record_type: RecordType): 53 | if record_type == RecordType.TSR: 54 | return RecordSide.LEFT 55 | elif record_type == RecordType.TSN: 56 | return RecordSide.RIGHT 57 | else: 58 | return DEFAULT_ENTRY_SIDE 59 | 60 | def csv_to_timeline_entries(trows: list): 61 | containers = [] 62 | for row in trows: 63 | 64 | ts_side = get_timeline_side(row[2]) 65 | pretty_date = row[0].strftime('%H:%M on %d %b %Y') 66 | 67 | html_container = f'
\n
\n' 68 | html_container += f'

{row[1]}

\n' 69 | html_container += f'

{pretty_date}

\n' 70 | html_container += '
\n
\n' 71 | 72 | containers.append(html_container) 73 | # return containers as a single string with reversed order 74 | return "".join(containers[::-1]) 75 | 76 | 77 | def generate_html_template(embedded_html: str, stylesheet: str): 78 | css = "" 79 | if stylesheet: 80 | css = f""" 81 | 84 | """ 85 | 86 | html_template = f""" 87 | 88 | 89 | {css} 90 | 91 | 92 | 93 |
94 | {embedded_html} 95 |
96 | 97 | 98 | """ 99 | return html_template 100 | 101 | def html_template_to_file(html_template: str, file_path: str): 102 | """Save html template to file""" 103 | fp = file_path 104 | if file_path == None: 105 | fp = HTML_OUTPUT_PATH 106 | 107 | with open(fp, "w") as f: 108 | f.write(html_template) 109 | 110 | def read_csv(filepath: str, recordtype: RecordType) -> list: 111 | rows_buffer = [] 112 | with open(filepath, "r") as csvfile: 113 | reader = csv.reader(csvfile) 114 | for row in reader: 115 | 116 | if len(row) == 0: 117 | # omit empty rows 118 | continue 119 | 120 | raw_datetime = row[0] 121 | raw_data = row[1] 122 | parsed_datetime = datetime.datetime.fromisoformat(raw_datetime) 123 | rows_buffer.append([parsed_datetime, raw_data, recordtype]) 124 | return rows_buffer 125 | 126 | def read_file_to_str(filepath): 127 | with open(filepath, 'r') as file: 128 | return file.read() 129 | 130 | 131 | # ------ 132 | # Main 133 | # ------ 134 | 135 | # make sure directories exist 136 | os.makedirs(os.path.dirname(TSR_FILE_PATH), exist_ok=True) 137 | os.makedirs(os.path.dirname(TSN_FILE_PATH), exist_ok=True) 138 | 139 | tsr_rows = [] 140 | tsn_rows = [] 141 | 142 | tsr_rows = read_csv(TSR_FILE_PATH, RecordType.TSR) 143 | tsn_rows = read_csv(TSN_FILE_PATH, RecordType.TSN) 144 | all_rows = tsr_rows + tsn_rows 145 | sorted_rows = sorted(all_rows, key=lambda x: x[0]) 146 | 147 | css_stylesheet = read_file_to_str(CSS_ASSETS_PATH) 148 | html_entries = csv_to_timeline_entries(sorted_rows) 149 | html_template = generate_html_template(html_entries, css_stylesheet) 150 | _ = html_template_to_file(html_template, HTML_OUTPUT_PATH) 151 | 152 | webbrowser.open('file://'+os.path.realpath(HTML_OUTPUT_PATH)) 153 | print("Compiled the html report.") 154 | --------------------------------------------------------------------------------