clean-workspace 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Michael Bianco
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.
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.1
2
+ Name: clean-workspace
3
+ Version: 0.1.0
4
+ Summary: Collect all browser URLs, output to terminal, and archive to todoist
5
+ License: MIT
6
+ Author: Michael Bianco
7
+ Author-email: mike@mikebian.co
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Requires-Dist: chrome-bookmarks (>=2020.10.25,<2021.0.0)
14
+ Requires-Dist: pyobjc-framework-ScriptingBridge (>=9.0.1,<10.0.0)
15
+ Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
16
+ Requires-Dist: todoist-api-python (>=2.0.1,<3.0.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Clean Workspace: Archive Web Browser Tabs
20
+
21
+ I've been experimenting with how to make my mornings more productive. One glitch I've found in my mind is I can easily
22
+ get distracted by open tabs on my browser, especially if I'm trying to write or read something which I want to give
23
+ my full attention to. I've found that if I close all my tabs, I can focus better on the task at hand. However, I don't
24
+ want to lose any interesting tabs so I never actually do that.
25
+
26
+ This is simple utility to automate this process. It will close all your tabs (in both Safari & Chrome), and send them to [todoist](https://mikebian.co/todoist) (and output) them to the terminal.
27
+
28
+ We'll see if this actually helps!
29
+
30
+ ## Installation
31
+
32
+ ```shell
33
+ poetry install
34
+ poetry run clean-workspace
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ - Add your todoist token to `.envrc` and `direnv allow .`
40
+ - Customize the url and domain blacklist
41
+
42
+ ### Collecting Tab Description Via AppleScript
43
+
44
+ Here's a quick script you can use to collect a description of what you were working on via applescript:
45
+
46
+ ```shell
47
+ dialogResult=$(
48
+ osascript <<EOT
49
+ set dialogResult to display dialog "What were you working on yesterday?" buttons {"OK"} default button "OK" giving up after 300 default answer ""
50
+ return text returned of dialogResult
51
+ EOT
52
+ )
53
+ ```
54
+
55
+ ## Inspiration
56
+
57
+ - https://gist.github.com/aleks-mariusz/cc27b21f2c5b91fbd285
58
+ - https://github.com/tominsam/shelf-python/blob/f357d9b147fa651034b71501edabf65f59d5befa/extractors/ComAppleSafari.py#L11
59
+
@@ -0,0 +1,40 @@
1
+ # Clean Workspace: Archive Web Browser Tabs
2
+
3
+ I've been experimenting with how to make my mornings more productive. One glitch I've found in my mind is I can easily
4
+ get distracted by open tabs on my browser, especially if I'm trying to write or read something which I want to give
5
+ my full attention to. I've found that if I close all my tabs, I can focus better on the task at hand. However, I don't
6
+ want to lose any interesting tabs so I never actually do that.
7
+
8
+ This is simple utility to automate this process. It will close all your tabs (in both Safari & Chrome), and send them to [todoist](https://mikebian.co/todoist) (and output) them to the terminal.
9
+
10
+ We'll see if this actually helps!
11
+
12
+ ## Installation
13
+
14
+ ```shell
15
+ poetry install
16
+ poetry run clean-workspace
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ - Add your todoist token to `.envrc` and `direnv allow .`
22
+ - Customize the url and domain blacklist
23
+
24
+ ### Collecting Tab Description Via AppleScript
25
+
26
+ Here's a quick script you can use to collect a description of what you were working on via applescript:
27
+
28
+ ```shell
29
+ dialogResult=$(
30
+ osascript <<EOT
31
+ set dialogResult to display dialog "What were you working on yesterday?" buttons {"OK"} default button "OK" giving up after 300 default answer ""
32
+ return text returned of dialogResult
33
+ EOT
34
+ )
35
+ ```
36
+
37
+ ## Inspiration
38
+
39
+ - https://gist.github.com/aleks-mariusz/cc27b21f2c5b91fbd285
40
+ - https://github.com/tominsam/shelf-python/blob/f357d9b147fa651034b71501edabf65f59d5befa/extractors/ComAppleSafari.py#L11
@@ -0,0 +1,206 @@
1
+ import os
2
+ import plistlib
3
+ import sys
4
+ import typing as t
5
+
6
+ from ScriptingBridge import SBApplication
7
+ import chrome_bookmarks
8
+
9
+ def todoist_client():
10
+ # extract todoist api key from environment without throwing an exception
11
+ todoist_api_key = os.environ.get("TODOIST_API_KEY", None)
12
+
13
+ if not todoist_api_key:
14
+ print("todoist api key not found in environment")
15
+ return
16
+
17
+ print("todoist api key found, adding to todoist")
18
+
19
+ from todoist_api_python.api import TodoistAPI
20
+
21
+ api = TodoistAPI(todoist_api_key)
22
+ return api
23
+
24
+ def export_to_todoist(task_content, description):
25
+ # TODO should also support .env here as well
26
+ import dotenv
27
+ dotenv.load_dotenv(".envrc")
28
+
29
+ api = todoist_client()
30
+ if not api:
31
+ return
32
+
33
+ import datetime
34
+
35
+ project_name = os.environ.get("TODOIST_PROJECT", "Learning")
36
+ project = None
37
+ projects = api.get_projects()
38
+ project_matches = [project for project in projects if project.name == project_name]
39
+
40
+ if len(project_matches) == 1:
41
+ project = project_matches[0]
42
+
43
+
44
+ # find a label called "web-archive" or create it
45
+ label_name = os.environ.get("TODOIST_LABEL", "web-archive")
46
+ labels = api.get_labels()
47
+ label_matches = [label for label in labels if label.name == label_name]
48
+
49
+ # assigning label for debugging
50
+ if len(label_matches) == 0:
51
+ # https://developer.todoist.com/rest/v1/#create-a-new-label
52
+ print(f"could not find {label_name} label, creating it")
53
+ label = api.add_label(name=label_name)
54
+ else:
55
+ label = label_matches[0]
56
+
57
+ # https://developer.todoist.com/rest/v2#create-a-new-task
58
+ response = api.add_task(
59
+ # set content to "web archive CURRENT_DAY" using format YYYY-MM-DD
60
+ content="{}web archive {}".format(
61
+ description, datetime.datetime.now().strftime("%Y-%m-%d")
62
+ ),
63
+ description=task_content,
64
+ # date is serialized in the task description, no need for a due date
65
+ due_string="no date",
66
+ labels=[label_name],
67
+ project_id=project.id if project else None,
68
+ )
69
+
70
+ def get_browser_urls() -> t.List[str]:
71
+ browser_urls = []
72
+ chrome = SBApplication.applicationWithBundleIdentifier_("com.google.Chrome")
73
+
74
+ for window in chrome.windows():
75
+ for tab in window.tabs():
76
+ browser_urls.append((tab.URL(), tab.name()))
77
+
78
+ safari = SBApplication.applicationWithBundleIdentifier_("com.apple.safari")
79
+
80
+ for window in safari.windows():
81
+ for tab in window.tabs():
82
+ browser_urls.append((tab.URL(), tab.name()))
83
+ # it doesn't look possible to close out the tabs with SBApplication :/
84
+ # instead we just close out the whole application below
85
+
86
+ return browser_urls
87
+
88
+ def get_bookmarks_urls() -> t.List[str]:
89
+ raw_bookmark_urls = [bookmark.url for bookmark in chrome_bookmarks.urls]
90
+
91
+ # read all safari bookmarks, don't include these in the printout
92
+ with open(os.path.expanduser("~") + "/Library/Safari/Bookmarks.plist", "rb") as f:
93
+ bookmarks_plist = plistlib.load(f)
94
+
95
+ """
96
+ In [31]: [bookmark["Title"] for bookmark in bookmarks["Children"]]
97
+ Out[31]: ['History', 'BookmarksBar', 'BookmarksMenu', 'com.apple.ReadingList']
98
+ """
99
+
100
+ safari_bookmarks = [
101
+ child
102
+ for child in bookmarks_plist["Children"]
103
+ if child["Title"] == "BookmarksBar"
104
+ ][0]["Children"]
105
+
106
+ raw_safari_bookmark_urls = [bookmark["URLString"] for bookmark in safari_bookmarks]
107
+
108
+ raw_bookmark_urls.extend(raw_safari_bookmark_urls)
109
+
110
+ return [bookmark.split("#")[0] for bookmark in raw_bookmark_urls]
111
+
112
+ def quit_browsers():
113
+ os.system("osascript -e 'quit app \"Safari\"'")
114
+ os.system("osascript -e 'quit app \"Chrome\"'")
115
+
116
+ def main():
117
+ if not is_internet_connected():
118
+ print("internet is not connected")
119
+ return
120
+
121
+ # TODO maybe optionally collect via applescript input dialog? We'd need to develop a proper interface for the CLI at that point.
122
+ # get first CLI argument if it exists
123
+ if len(sys.argv) > 1 and sys.argv[1].strip():
124
+ tab_description = sys.argv[1].strip() + " "
125
+ else:
126
+ tab_description = ""
127
+
128
+ browser_urls = get_browser_urls()
129
+
130
+ # if page is blank, there is no url or string does not contain http
131
+ browser_urls = [x for x in browser_urls if x[0] is not None and "http" in x[0]]
132
+
133
+ # remove duplicates
134
+ browser_urls = list(set(browser_urls))
135
+
136
+ # sort (in place) list of urls by domain name of url
137
+ browser_urls.sort(key=lambda x: x[0].split("/")[2])
138
+
139
+ # strip all anchors from the urls
140
+ browser_urls = [(url.split("#")[0], name) for url, name in browser_urls]
141
+
142
+ # TODO load these files from a home config file
143
+
144
+ # user configurable blacklist for urls you don't want to archive
145
+ url_blacklist = []
146
+ with open("blacklist_urls.txt", "r") as f:
147
+ url_blacklist = f.read().splitlines()
148
+
149
+ domain_blacklist = []
150
+ with open("blacklist_domains.txt", "r") as f:
151
+ domain_blacklist = f.read().splitlines()
152
+ # add a `www.` prefix to each domain in the blacklist and merge it with the existing list
153
+ domain_blacklist = domain_blacklist + [
154
+ "www." + domain for domain in domain_blacklist
155
+ ]
156
+
157
+ bookmark_urls = get_bookmarks_urls()
158
+
159
+ # TODO output skipped domains
160
+ # TODO support wildcard subdomains, *.sentry.io
161
+ # filter all urls with blacklisted domains
162
+ browser_urls = [x for x in browser_urls if x[0].split("/")[2] not in domain_blacklist]
163
+
164
+ # join url and name with "-" and print to stdout
165
+ todoist_content = ""
166
+ for url_with_name in browser_urls:
167
+ if (
168
+ # if the url is in the bookmark list of chrome or safari, skip it
169
+ url_with_name[0] not in bookmark_urls
170
+ # TODO should allow for regex in the URL matching, or at least globbing
171
+ # if the url
172
+ and url_with_name[0] not in url_blacklist
173
+ ):
174
+ todoist_content += "* " + " - ".join(url_with_name) + "\n"
175
+ else:
176
+ print(f"skipping url\t{url_with_name[0]}")
177
+
178
+ if not todoist_content.strip():
179
+ print("no urls to add, exiting")
180
+ quit_browsers()
181
+ sys.exit()
182
+
183
+ print(f"\n{todoist_content}\n")
184
+
185
+ # since we've archived all content we can now close out Safari & Chrome
186
+ quit_browsers()
187
+
188
+ export_to_todoist(todoist_content, tab_description)
189
+
190
+ archive_old_tasks()
191
+
192
+ def archive_old_tasks():
193
+ # find all old tasks (>1mo) and archive them
194
+ # optionally only do this when there is not a custom name in the title
195
+ pass
196
+
197
+ def is_internet_connected():
198
+ import socket
199
+ s = socket.socket(socket.AF_INET)
200
+ try:
201
+ s.connect(("google.com",80))
202
+ return True
203
+ except socket.error as e: return False
204
+
205
+ if __name__ == "__main__":
206
+ main()
@@ -0,0 +1,26 @@
1
+ [tool.poetry]
2
+ name = "clean-workspace"
3
+ version = "0.1.0"
4
+ description = "Collect all browser URLs, output to terminal, and archive to todoist"
5
+ authors = ["Michael Bianco <mike@mikebian.co>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+ pyobjc-framework-ScriptingBridge = "^9.0.1"
12
+ todoist-api-python = "^2.0.1"
13
+ python-dotenv = "^1.0.0"
14
+ chrome-bookmarks = "^2020.10.25"
15
+
16
+ [tool.poetry.group.dev.dependencies]
17
+ ipython = "^8.12.0"
18
+ ipython-autoimport = "^0.4"
19
+ pdbr = "^0.8.2"
20
+
21
+ [tool.poetry.scripts]
22
+ clean-workspace = "clean_workspace:main"
23
+
24
+ [build-system]
25
+ requires = ["poetry-core"]
26
+ build-backend = "poetry.core.masonry.api"