Skip to content

Commit 73488e7

Browse files
Add Recent Files menu item
1 parent 2b01472 commit 73488e7

2 files changed

Lines changed: 107 additions & 1 deletion

File tree

‎src/morgul/app.py‎

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,23 @@
9090
from morgul.password_ui import SetPasswordDialog, UnlockPasswordDialog
9191
from morgul.render import to_html
9292
from morgul.session import (
93+
RECENT_LIMIT,
9394
SESSION_VERSION,
9495
SessionIndex,
9596
TabMeta,
9697
TabPayload,
9798
blob_is_encrypted,
99+
clear_recent,
98100
clear_session,
99101
decode_tab_blob,
100102
encode_tab_blob,
101103
load_index,
104+
load_recent,
102105
new_tab_id,
103106
prune_tab_blobs,
104107
read_tab_blob,
105108
save_index,
109+
save_recent,
106110
write_tab_blob,
107111
)
108112
from morgul.syncmap import preview_pos_to_source, source_pos_to_preview
@@ -1605,6 +1609,7 @@ def __init__(self) -> None:
16051609
self._find_dialog = FindReplaceDialog(self)
16061610
self._wrap_action: QAction | None = None
16071611
self._preview_action: QAction | None = None
1612+
self._recent = load_recent()
16081613

16091614
self._build_menus()
16101615
if not self._restore_session():
@@ -1648,6 +1653,8 @@ def _build_menus(self) -> None:
16481653
file_menu.addAction(
16491654
self._act("&Open...", QKeySequence.StandardKey.Open, self._open)
16501655
)
1656+
self._recent_menu = file_menu.addMenu("Open &Recent")
1657+
self._rebuild_recent_menu()
16511658
file_menu.addAction(
16521659
self._act("&Save", QKeySequence.StandardKey.Save, self._save)
16531660
)
@@ -2082,7 +2089,10 @@ def _open(self) -> None:
20822089
)
20832090
if not path_str:
20842091
return
2085-
path = Path(path_str)
2092+
self._open_path(Path(path_str))
2093+
2094+
def _open_path(self, path: Path) -> None:
2095+
"""Open *path*, falling back to the active blank tab when possible."""
20862096
try:
20872097
text, password = self._read_document(path)
20882098
except MorgulFormatError as exc:
@@ -2106,8 +2116,54 @@ def _open(self) -> None:
21062116
else:
21072117
target = self._new_tab()
21082118
target.load_text(text, path=path, password=password)
2119+
self._add_recent(path)
21092120
self.tab_meta_changed()
21102121

2122+
def _rebuild_recent_menu(self) -> None:
2123+
"""Repopulate the Open Recent submenu from ``self._recent``."""
2124+
self._recent_menu.clear()
2125+
if not self._recent:
2126+
empty = self._recent_menu.addAction("No recent files")
2127+
empty.setEnabled(False)
2128+
return
2129+
for path_str in self._recent:
2130+
action = self._recent_menu.addAction(path_str)
2131+
action.triggered.connect(lambda _=False, p=path_str: self._open_recent(p))
2132+
self._recent_menu.addSeparator()
2133+
clear = self._recent_menu.addAction("Clear Recent Files")
2134+
clear.triggered.connect(self._clear_recent)
2135+
2136+
def _add_recent(self, path: Path) -> None:
2137+
"""Push *path* to the front of the recent list and persist it."""
2138+
entry = str(path)
2139+
with contextlib.suppress(ValueError):
2140+
self._recent.remove(entry)
2141+
self._recent.insert(0, entry)
2142+
del self._recent[RECENT_LIMIT:]
2143+
with contextlib.suppress(OSError):
2144+
save_recent(self._recent)
2145+
self._rebuild_recent_menu()
2146+
2147+
def _clear_recent(self) -> None:
2148+
"""Empty the recent list and its menu."""
2149+
self._recent.clear()
2150+
with contextlib.suppress(OSError):
2151+
clear_recent()
2152+
self._rebuild_recent_menu()
2153+
2154+
def _open_recent(self, path_str: str) -> None:
2155+
"""Open a file chosen from the Open Recent submenu."""
2156+
path = Path(path_str)
2157+
if not path.is_file():
2158+
QMessageBox.warning(self, "Morgul", f"File not found:\n{path}")
2159+
if path_str in self._recent:
2160+
self._recent.remove(path_str)
2161+
with contextlib.suppress(OSError):
2162+
save_recent(self._recent)
2163+
self._rebuild_recent_menu()
2164+
return
2165+
self._open_path(path)
2166+
21112167
def _read_document(self, path: Path) -> tuple[str, str | None]:
21122168
"""Load Markdown or MORGUL from *path*.
21132169
@@ -2158,6 +2214,7 @@ def _save(self) -> None:
21582214
except OSError as exc:
21592215
QMessageBox.warning(self, "Morgul", f"Could not save file:\n{exc}")
21602216
return
2217+
self._add_recent(tab.path)
21612218
tab.dirty = False
21622219
self.tab_meta_changed()
21632220

@@ -2194,6 +2251,7 @@ def _save_as(self) -> None:
21942251
QMessageBox.warning(self, "Morgul", f"Could not save file:\n{exc}")
21952252
return
21962253
tab.path = path
2254+
self._add_recent(path)
21972255
tab.dirty = False
21982256
self.tab_meta_changed()
21992257

‎src/morgul/session.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
SESSION_VERSION = 1
1515

16+
# Max entries remembered in the File → Open Recent list.
17+
RECENT_LIMIT = 10
18+
1619

1720
def session_root() -> Path:
1821
"""Return ``~/.morgul``.
@@ -304,3 +307,48 @@ def clear_session() -> None:
304307
if root.is_dir():
305308
for path in root.glob("*.tab"):
306309
path.unlink(missing_ok=True)
310+
311+
312+
def recent_file() -> Path:
313+
"""Return ``~/.morgul/recent.json``.
314+
315+
Returns:
316+
Path to the Open Recent list file.
317+
"""
318+
return session_root() / "recent.json"
319+
320+
321+
def load_recent() -> list[str]:
322+
"""Read the recent-files list, newest first.
323+
324+
Returns:
325+
Up to :data:`RECENT_LIMIT` file paths, or ``[]`` when missing/invalid.
326+
"""
327+
path = recent_file()
328+
if not path.is_file():
329+
return []
330+
try:
331+
data = json.loads(path.read_text(encoding="utf-8"))
332+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
333+
return []
334+
if not isinstance(data, list):
335+
return []
336+
return [s for s in data if isinstance(s, str)][:RECENT_LIMIT]
337+
338+
339+
def save_recent(files: list[str]) -> None:
340+
"""Persist *files* (newest first) to ``~/.morgul/recent.json``."""
341+
ensure_session_dirs()
342+
path = recent_file()
343+
tmp = path.with_suffix(".tmp")
344+
tmp.write_text(
345+
json.dumps(files[:RECENT_LIMIT], ensure_ascii=False, indent=2) + "\n",
346+
encoding="utf-8",
347+
newline="\n",
348+
)
349+
tmp.replace(path)
350+
351+
352+
def clear_recent() -> None:
353+
"""Delete the recent-files list."""
354+
recent_file().unlink(missing_ok=True)

0 commit comments

Comments
 (0)