pop-note 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,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: pop-note
3
+ Version: 0.1.0
4
+ Summary: A single note you can pop up and hide with a keybinding.
5
+ Author-email: "@readwithai" <talwrii@gmail.com>
6
+ Project-URL: Homepage, https://github.com/talwrii/pop-note
7
+ Project-URL: Repository, https://github.com/talwrii/pop-note
8
+ Project-URL: Issues, https://github.com/talwrii/pop-note/issues
9
+ Keywords: notes,popup,quake,tkinter
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: tomli; python_version < "3.11"
13
+
14
+ # pop-note
15
+ A single editable note that you can pop up and hide with a keybinding. Requires X11 (normally used with linux) (easy fix tho)
16
+
17
+ Unreviewed ai-generated code.
18
+
19
+ ## Motivation
20
+ I don't have any window space and my computer only accepts two monitors. I don't want to change anything about my editor, or note taking app.
21
+
22
+ Philosophy, don't separate process and capture.
23
+
24
+ ## Alternatives
25
+ So many alternatives:
26
+
27
+ * Buy a monitor or a new computer and put it somewhere. Monitors are basically free nowerdays.
28
+ * Put your notes on another desktop and toggle desktops. Get it to launch at start up
29
+ * Have a shortcut to raise you editor / editor window (run-or-raise)
30
+ * Using something capture based like org mode, where you capture the information and process it latter
31
+ * Go to your daily not in obsidan and use the back button
32
+
33
+ But for now I just want this and I am feeling lazy.
34
+
35
+ This was kind of influenced by "quake style pop-down" terminals. But I am too lazy to implement a pop-down syle animation.
36
+
37
+ ## Feature requests
38
+ No.
39
+
40
+ This is one of thos apps which is designed to have no features. If you want somethign clever, use one of the approaches above.
41
+
42
+ I might however add the features I want.
43
+
44
+
45
+ ## Caveats
46
+ Only works with x11 because tkinter window raising does not work reliably with KDE so I had to use wmctrl to raise windows. Apparently there are different tools for every wayland compositor so if you use one of those you could special case this and send it to me.
47
+
48
+ ## Installation
49
+ pipx install node-
50
+
51
+ ## About me
52
+ I am @readwith. If you are interested in note-taking [read this](https://readwithai.substack.com/p/note-taking-with-obsidian-much-of)
@@ -0,0 +1,39 @@
1
+ # pop-note
2
+ A single editable note that you can pop up and hide with a keybinding. Requires X11 (normally used with linux) (easy fix tho)
3
+
4
+ Unreviewed ai-generated code.
5
+
6
+ ## Motivation
7
+ I don't have any window space and my computer only accepts two monitors. I don't want to change anything about my editor, or note taking app.
8
+
9
+ Philosophy, don't separate process and capture.
10
+
11
+ ## Alternatives
12
+ So many alternatives:
13
+
14
+ * Buy a monitor or a new computer and put it somewhere. Monitors are basically free nowerdays.
15
+ * Put your notes on another desktop and toggle desktops. Get it to launch at start up
16
+ * Have a shortcut to raise you editor / editor window (run-or-raise)
17
+ * Using something capture based like org mode, where you capture the information and process it latter
18
+ * Go to your daily not in obsidan and use the back button
19
+
20
+ But for now I just want this and I am feeling lazy.
21
+
22
+ This was kind of influenced by "quake style pop-down" terminals. But I am too lazy to implement a pop-down syle animation.
23
+
24
+ ## Feature requests
25
+ No.
26
+
27
+ This is one of thos apps which is designed to have no features. If you want somethign clever, use one of the approaches above.
28
+
29
+ I might however add the features I want.
30
+
31
+
32
+ ## Caveats
33
+ Only works with x11 because tkinter window raising does not work reliably with KDE so I had to use wmctrl to raise windows. Apparently there are different tools for every wayland compositor so if you use one of those you could special case this and send it to me.
34
+
35
+ ## Installation
36
+ pipx install node-
37
+
38
+ ## About me
39
+ I am @readwith. If you are interested in note-taking [read this](https://readwithai.substack.com/p/note-taking-with-obsidian-much-of)
File without changes
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env python3
2
+ """pop-note: a single note you can pop up and hide with a keybinding."""
3
+ import os
4
+ import sys
5
+ import socket
6
+ import threading
7
+ import datetime
8
+ import tkinter as tk
9
+ from pathlib import Path
10
+
11
+ import subprocess
12
+ try:
13
+ import tomllib # Python 3.11+
14
+ except ModuleNotFoundError:
15
+ import tomli as tomllib
16
+
17
+ CONFIG_PATH = Path.home() / ".config" / "pop-note" / "config.toml"
18
+ DEFAULT_NOTE_PATH = Path.home() / "notes" / "pop-note.md"
19
+ DEFAULT_VERSIONS_DIR = Path.home() / ".local" / "share" / "pop-note" / "versions"
20
+ SOCKET_PATH = Path(f"/tmp/pop-note-{os.getuid()}.sock")
21
+ PIDFILE_PATH = Path(f"/tmp/pop-note-{os.getuid()}.pid")
22
+ LOG_DIR = Path.home() / ".local" / "share" / "pop-note" / "logs"
23
+ STATE_PATH = Path.home() / ".local" / "share" / "pop-note" / "state.toml"
24
+
25
+ VERSION = "8"
26
+
27
+
28
+ def load_config():
29
+ note_path = DEFAULT_NOTE_PATH
30
+ versions_dir = DEFAULT_VERSIONS_DIR
31
+ if CONFIG_PATH.exists():
32
+ with open(CONFIG_PATH, "rb") as f:
33
+ cfg = tomllib.load(f)
34
+ if "note_path" in cfg:
35
+ note_path = Path(os.path.expanduser(cfg["note_path"]))
36
+ if "versions_dir" in cfg:
37
+ versions_dir = Path(os.path.expanduser(cfg["versions_dir"]))
38
+ note_path.parent.mkdir(parents=True, exist_ok=True)
39
+ versions_dir.mkdir(parents=True, exist_ok=True)
40
+ if not note_path.exists():
41
+ note_path.write_text("")
42
+ return note_path, versions_dir
43
+
44
+
45
+ def _read_pid():
46
+ try:
47
+ return int(PIDFILE_PATH.read_text().strip())
48
+ except (FileNotFoundError, ValueError):
49
+ return None
50
+
51
+
52
+ def _pid_alive(pid):
53
+ if pid is None:
54
+ return False
55
+ try:
56
+ os.kill(pid, 0)
57
+ return True
58
+ except (ProcessLookupError, PermissionError):
59
+ return False
60
+
61
+
62
+ def _cleanup_stale():
63
+ for p in (SOCKET_PATH, PIDFILE_PATH):
64
+ try:
65
+ p.unlink()
66
+ except FileNotFoundError:
67
+ pass
68
+
69
+
70
+ def _ask_version():
71
+ """Ask the running daemon what version it is. Returns string or None."""
72
+ if not SOCKET_PATH.exists():
73
+ return None
74
+ try:
75
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
76
+ s.settimeout(0.5)
77
+ s.connect(str(SOCKET_PATH))
78
+ s.sendall(b"version\n")
79
+ data = s.recv(64)
80
+ s.close()
81
+ return data.decode().strip()
82
+ except (ConnectionRefusedError, FileNotFoundError, socket.timeout, OSError):
83
+ return None
84
+
85
+
86
+ def try_toggle_existing():
87
+ """If a matching-version daemon is running, toggle it and return True.
88
+ If a stale-version daemon is running, kill it and return False so we restart."""
89
+ pid = _read_pid()
90
+ if not _pid_alive(pid):
91
+ _cleanup_stale()
92
+ return False
93
+ if not SOCKET_PATH.exists():
94
+ return False
95
+ running_version = _ask_version()
96
+ if running_version != VERSION:
97
+ print(f"pop-note: running daemon is version {running_version!r}, "
98
+ f"need {VERSION!r} — restarting", file=sys.stderr)
99
+ kill_existing()
100
+ # Give it a moment to clean up
101
+ import time
102
+ time.sleep(0.2)
103
+ return False
104
+ try:
105
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
106
+ s.settimeout(0.5)
107
+ s.connect(str(SOCKET_PATH))
108
+ s.sendall(b"toggle\n")
109
+ s.close()
110
+ return True
111
+ except (ConnectionRefusedError, FileNotFoundError, socket.timeout, OSError):
112
+ _cleanup_stale()
113
+ return False
114
+
115
+
116
+ def kill_existing():
117
+ pid = _read_pid()
118
+ if not _pid_alive(pid):
119
+ _cleanup_stale()
120
+ print("pop-note: not running")
121
+ return
122
+ try:
123
+ os.kill(pid, 15)
124
+ print(f"pop-note: killed pid {pid}")
125
+ except ProcessLookupError:
126
+ pass
127
+ _cleanup_stale()
128
+
129
+
130
+ def daemonise(log_path):
131
+ """Standard double-fork daemonisation. Redirects stdout/stderr to log_path."""
132
+ if os.fork() > 0:
133
+ os._exit(0)
134
+ os.setsid()
135
+ if os.fork() > 0:
136
+ os._exit(0)
137
+ devnull = os.open(os.devnull, os.O_RDONLY)
138
+ os.dup2(devnull, 0)
139
+ os.close(devnull)
140
+ log_fd = os.open(str(log_path), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
141
+ os.dup2(log_fd, 1)
142
+ os.dup2(log_fd, 2)
143
+ os.close(log_fd)
144
+
145
+
146
+ def latest_log():
147
+ if not LOG_DIR.exists():
148
+ return None
149
+ logs = sorted(LOG_DIR.glob("*.log"))
150
+ return logs[-1] if logs else None
151
+
152
+
153
+ def show_last_log():
154
+ log = latest_log()
155
+ if log is None:
156
+ print("pop-note: no logs found")
157
+ return
158
+ print(f"# {log}")
159
+ sys.stdout.write(log.read_text())
160
+
161
+
162
+ class PopNote:
163
+ def __init__(self, note_path, versions_dir):
164
+ self.note_path = note_path
165
+ self.versions_dir = versions_dir
166
+ self.visible = False
167
+
168
+ self.root = tk.Tk()
169
+ self._wm_title = f"pop-note-{os.getpid()}"
170
+ self.root.title(self._wm_title)
171
+ self.root.geometry("600x400")
172
+ self.text = tk.Text(self.root, wrap="word", font=("monospace", 11),
173
+ undo=True)
174
+ self.text.pack(fill="both", expand=True)
175
+
176
+ # Override window close to hide instead of quit
177
+ self.root.protocol("WM_DELETE_WINDOW", self.hide)
178
+ self.root.bind("<Escape>", lambda e: self.hide())
179
+ self.root.bind("<Configure>", self._on_configure)
180
+
181
+ # Start hidden — first toggle invocation will show it
182
+ self.root.withdraw()
183
+
184
+ # Listen for toggle messages
185
+ self._start_socket_thread()
186
+
187
+ def _start_socket_thread(self):
188
+ if SOCKET_PATH.exists():
189
+ try:
190
+ SOCKET_PATH.unlink()
191
+ except FileNotFoundError:
192
+ pass
193
+ srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
194
+ srv.bind(str(SOCKET_PATH))
195
+ srv.listen(4)
196
+ self._srv = srv
197
+
198
+ def loop():
199
+ while True:
200
+ try:
201
+ conn, _ = srv.accept()
202
+ data = conn.recv(64)
203
+ if b"version" in data:
204
+ try:
205
+ conn.sendall(VERSION.encode() + b"\n")
206
+ except OSError:
207
+ pass
208
+ elif b"toggle" in data:
209
+ self.root.after(0, self.toggle)
210
+ conn.close()
211
+ except OSError:
212
+ break
213
+
214
+ t = threading.Thread(target=loop, daemon=True)
215
+ t.start()
216
+
217
+ def _snapshot(self, label):
218
+ ts = datetime.datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
219
+ content = self.text.get("1.0", "end-1c") if self.visible else self.note_path.read_text()
220
+ path = self.versions_dir / f"{ts}-{label}.md"
221
+ path.write_text(content)
222
+
223
+ def _load_into_text(self):
224
+ self.text.delete("1.0", "end")
225
+ self.text.insert("1.0", self.note_path.read_text())
226
+
227
+ def _save_live(self):
228
+ content = self.text.get("1.0", "end-1c")
229
+ self.note_path.write_text(content)
230
+
231
+ def _on_configure(self, event):
232
+ # Configure fires for child widgets too — only act on the toplevel.
233
+ if event.widget is not self.root:
234
+ return
235
+ if not self.visible:
236
+ return
237
+ self._save_geometry()
238
+
239
+ def _load_geometry(self):
240
+ try:
241
+ with open(STATE_PATH, "rb") as f:
242
+ state = tomllib.load(f)
243
+ return state.get("geometry")
244
+ except (FileNotFoundError, Exception):
245
+ return None
246
+
247
+ def _save_geometry(self):
248
+ try:
249
+ STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
250
+ geom = self.root.winfo_geometry() # "WxH+X+Y"
251
+ STATE_PATH.write_text(f'geometry = "{geom}"\n')
252
+ except Exception as e:
253
+ print(f"failed to save geometry: {e}", flush=True)
254
+
255
+ def _wmctrl_raise(self):
256
+ try:
257
+ # Match by exact title (-F) and activate (-a)
258
+ r = subprocess.run(
259
+ ["wmctrl", "-F", "-a", self._wm_title],
260
+ capture_output=True, text=True,
261
+ )
262
+ if r.returncode != 0:
263
+ print(f"wmctrl raise failed rc={r.returncode} "
264
+ f"stderr={r.stderr.strip()}", flush=True)
265
+ # Fallback: list windows so we can see what's there
266
+ lst = subprocess.run(["wmctrl", "-l"], capture_output=True, text=True)
267
+ print(f"wmctrl -l:\n{lst.stdout}", flush=True)
268
+ self.root.lift()
269
+ self.root.focus_force()
270
+ except FileNotFoundError:
271
+ print("wmctrl not installed; falling back to tk lift", flush=True)
272
+ self.root.lift()
273
+ self.root.focus_force()
274
+
275
+ def _center_on_pointer(self):
276
+ # Center the window on whichever monitor the mouse is currently on.
277
+ self.root.update_idletasks()
278
+ w = self.root.winfo_width() or 600
279
+ h = self.root.winfo_height() or 400
280
+ px = self.root.winfo_pointerx()
281
+ py = self.root.winfo_pointery()
282
+ x = px - w // 2
283
+ y = py - h // 2
284
+ self.root.geometry(f"{w}x{h}+{x}+{y}")
285
+
286
+ def show(self):
287
+ if self.visible:
288
+ return
289
+ self._load_into_text()
290
+ self._snapshot("show")
291
+ self.visible = True
292
+ self.root.deiconify()
293
+ saved = self._load_geometry()
294
+ if saved:
295
+ self.root.geometry(saved)
296
+ else:
297
+ self._center_on_pointer()
298
+ self.root.update_idletasks()
299
+ self._wmctrl_raise()
300
+ self.text.focus_set()
301
+
302
+ def hide(self):
303
+ if not self.visible:
304
+ return
305
+ self._save_live()
306
+ self._save_geometry()
307
+ self._snapshot("hide")
308
+ self.visible = False
309
+ self.root.withdraw()
310
+
311
+ def toggle(self):
312
+ if self.visible:
313
+ self.hide()
314
+ else:
315
+ self.show()
316
+
317
+ def run(self):
318
+ try:
319
+ self.root.mainloop()
320
+ finally:
321
+ try:
322
+ self._srv.close()
323
+ except Exception:
324
+ pass
325
+ try:
326
+ SOCKET_PATH.unlink()
327
+ except FileNotFoundError:
328
+ pass
329
+ try:
330
+ PIDFILE_PATH.unlink()
331
+ except FileNotFoundError:
332
+ pass
333
+
334
+
335
+ def main():
336
+ if "--kill" in sys.argv[1:]:
337
+ kill_existing()
338
+ return
339
+
340
+ if "--last-log" in sys.argv[1:]:
341
+ show_last_log()
342
+ return
343
+
344
+ if try_toggle_existing():
345
+ return
346
+
347
+ # No live instance — start one as a daemon and toggle it open.
348
+ note_path, versions_dir = load_config()
349
+ LOG_DIR.mkdir(parents=True, exist_ok=True)
350
+ ts = datetime.datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
351
+ log_path = LOG_DIR / f"{ts}.log"
352
+ daemonise(log_path)
353
+ PIDFILE_PATH.write_text(str(os.getpid()))
354
+ print(f"pop-note daemon started, pid={os.getpid()}", flush=True)
355
+ try:
356
+ app = PopNote(note_path, versions_dir)
357
+ app.root.after(50, app.show)
358
+ app.run()
359
+ except Exception:
360
+ import traceback
361
+ traceback.print_exc()
362
+ raise
363
+
364
+
365
+ if __name__ == "__main__":
366
+ main()
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: pop-note
3
+ Version: 0.1.0
4
+ Summary: A single note you can pop up and hide with a keybinding.
5
+ Author-email: "@readwithai" <talwrii@gmail.com>
6
+ Project-URL: Homepage, https://github.com/talwrii/pop-note
7
+ Project-URL: Repository, https://github.com/talwrii/pop-note
8
+ Project-URL: Issues, https://github.com/talwrii/pop-note/issues
9
+ Keywords: notes,popup,quake,tkinter
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: tomli; python_version < "3.11"
13
+
14
+ # pop-note
15
+ A single editable note that you can pop up and hide with a keybinding. Requires X11 (normally used with linux) (easy fix tho)
16
+
17
+ Unreviewed ai-generated code.
18
+
19
+ ## Motivation
20
+ I don't have any window space and my computer only accepts two monitors. I don't want to change anything about my editor, or note taking app.
21
+
22
+ Philosophy, don't separate process and capture.
23
+
24
+ ## Alternatives
25
+ So many alternatives:
26
+
27
+ * Buy a monitor or a new computer and put it somewhere. Monitors are basically free nowerdays.
28
+ * Put your notes on another desktop and toggle desktops. Get it to launch at start up
29
+ * Have a shortcut to raise you editor / editor window (run-or-raise)
30
+ * Using something capture based like org mode, where you capture the information and process it latter
31
+ * Go to your daily not in obsidan and use the back button
32
+
33
+ But for now I just want this and I am feeling lazy.
34
+
35
+ This was kind of influenced by "quake style pop-down" terminals. But I am too lazy to implement a pop-down syle animation.
36
+
37
+ ## Feature requests
38
+ No.
39
+
40
+ This is one of thos apps which is designed to have no features. If you want somethign clever, use one of the approaches above.
41
+
42
+ I might however add the features I want.
43
+
44
+
45
+ ## Caveats
46
+ Only works with x11 because tkinter window raising does not work reliably with KDE so I had to use wmctrl to raise windows. Apparently there are different tools for every wayland compositor so if you use one of those you could special case this and send it to me.
47
+
48
+ ## Installation
49
+ pipx install node-
50
+
51
+ ## About me
52
+ I am @readwith. If you are interested in note-taking [read this](https://readwithai.substack.com/p/note-taking-with-obsidian-much-of)
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ pop_note/__init__.py
4
+ pop_note/main.py
5
+ pop_note.egg-info/PKG-INFO
6
+ pop_note.egg-info/SOURCES.txt
7
+ pop_note.egg-info/dependency_links.txt
8
+ pop_note.egg-info/entry_points.txt
9
+ pop_note.egg-info/requires.txt
10
+ pop_note.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pop-note = pop_note.main:main
@@ -0,0 +1,3 @@
1
+
2
+ [:python_version < "3.11"]
3
+ tomli
@@ -0,0 +1 @@
1
+ pop_note
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "pop-note"
3
+ version = "0.1.0"
4
+ description = "A single note you can pop up and hide with a keybinding."
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ keywords = ["notes", "popup", "quake", "tkinter"]
8
+ dependencies = [
9
+ "tomli; python_version < '3.11'",
10
+ ]
11
+
12
+ [[project.authors]]
13
+ name = "@readwithai"
14
+ email = "talwrii@gmail.com"
15
+
16
+ [project.license]
17
+ file = "LICENSE"
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/talwrii/pop-note"
21
+ Repository = "https://github.com/talwrii/pop-note"
22
+ Issues = "https://github.com/talwrii/pop-note/issues"
23
+
24
+ [project.scripts]
25
+ pop-note = "pop_note.main:main"
26
+
27
+ [build-system]
28
+ requires = ["setuptools>=61"]
29
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+