9090from morgul .password_ui import SetPasswordDialog , UnlockPasswordDialog
9191from morgul .render import to_html
9292from 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)
108112from 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
0 commit comments