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"
|