This commit is contained in:
Ram-Z 2026-01-07 17:31:44 -08:00 committed by GitHub
commit 2cc1b14602
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 207 additions and 62 deletions

View File

@ -7,6 +7,7 @@
import re import re
import sys import sys
import html import html
import io as _io
import os.path import os.path
import collections import collections
import functools import functools
@ -813,7 +814,8 @@ class AbstractDownloadItem(QObject):
if filename is None: # pragma: no cover if filename is None: # pragma: no cover
log.downloads.error("No filename to open the download!") log.downloads.error("No filename to open the download!")
return return
self.pdfjs_requested.emit(os.path.basename(filename), dirname = os.path.basename(os.path.dirname(filename))
self.pdfjs_requested.emit(os.path.join(dirname, os.path.basename(filename)),
self.url()) self.url())
def cancel_for_origin(self) -> bool: def cancel_for_origin(self) -> bool:
@ -1373,8 +1375,8 @@ class TempDownloadManager:
# Make sure that the filename is not too long # Make sure that the filename is not too long
suggested_name = utils.elide_filename(suggested_name, 50) suggested_name = utils.elide_filename(suggested_name, 50)
# pylint: disable=consider-using-with # pylint: disable=consider-using-with
fobj = tempfile.NamedTemporaryFile(dir=tmpdir.name, delete=False, tmpfiledir = tempfile.mkdtemp(dir=tmpdir.name)
suffix='_' + suggested_name) fobj = _io.open(os.path.join(tmpfiledir, suggested_name), 'w+b')
self.files.append(fobj) self.files.append(fobj)
return fobj return fobj

View File

@ -517,8 +517,6 @@ def qute_pdfjs(url: QUrl) -> _HandlerRet:
filename = QUrlQuery(url).queryItemValue('filename') filename = QUrlQuery(url).queryItemValue('filename')
if not filename: if not filename:
raise UrlInvalidError("Missing filename") raise UrlInvalidError("Missing filename")
if '/' in filename or os.sep in filename:
raise RequestDeniedError("Path separator in filename.")
path = _pdf_path(filename) path = _pdf_path(filename)
with open(path, 'rb') as f: with open(path, 'rb') as f:

View File

@ -130,6 +130,23 @@ session.lazy_restore:
default: false default: false
desc: Load a restored tab as soon as it takes focus. desc: Load a restored tab as soon as it takes focus.
session.save_when_close:
type: Bool
default: True
desc: >-
Whether to automatically save a session when it is closed.
session.startup_sessions:
type:
name: List
valtype: String
none_ok: true
default: null
#TODO The default value doesn't seem to actually be set to None, so this doesn't work
desc: >-
Which sessions to load on startup. None will load the last saved sessions
from the sate file, if you want to not load any sessions, pass an empty list.
backend: backend:
type: type:
name: String name: String
@ -2665,6 +2682,7 @@ window.title_format:
- current_title - current_title
- title_sep - title_sep
- id - id
- session
- scroll_pos - scroll_pos
- host - host
- backend - backend
@ -2672,7 +2690,8 @@ window.title_format:
- current_url - current_url
- protocol - protocol
- audio - audio
default: '{perc}{current_title}{title_sep}qutebrowser' #TODO default should not display session, especially if no session is set
default: '[{session}] {perc}{current_title}{title_sep}qutebrowser'
desc: | desc: |
Format to use for the window title. The same placeholders like for Format to use for the window title. The same placeholders like for
`tabs.title.format` are defined. `tabs.title.format` are defined.

View File

@ -241,6 +241,7 @@ class TabbedBrowser(QWidget):
self._global_marks: MutableMapping[str, tuple[QPoint, QUrl]] = {} self._global_marks: MutableMapping[str, tuple[QPoint, QUrl]] = {}
self.default_window_icon = self._window().windowIcon() self.default_window_icon = self._window().windowIcon()
self.is_private = private self.is_private = private
self.session = "_nosession"
self.tab_deque = TabDeque() self.tab_deque = TabDeque()
config.instance.changed.connect(self._on_config_changed) config.instance.changed.connect(self._on_config_changed)
quitter.instance.shutting_down.connect(self.shutdown) quitter.instance.shutting_down.connect(self.shutdown)
@ -317,6 +318,9 @@ class TabbedBrowser(QWidget):
return return
fields = self.widget.get_tab_fields(idx) fields = self.widget.get_tab_fields(idx)
fields['id'] = self._win_id fields['id'] = self._win_id
#TODO update on window session changed
#TODO don't leak _nosession into window title
fields['session'] = self.session
title = title_format.format(**fields) title = title_format.format(**fields)
# prevent hanging WMs and similar issues with giant URLs # prevent hanging WMs and similar issues with giant URLs

View File

@ -256,9 +256,35 @@ class SessionManager(QObject):
data['history'].append(item_data) data['history'].append(item_data)
return data return data
def _save_window(self, *, win_id=None, with_history=True):
"""Get a dict with data for single window and its tabs."""
main_window = objreg.get('main-window', scope='window', window=win_id)
# We could be in the middle of destroying a window here
if sip.isdeleted(main_window):
return None
tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id)
win_data: _JsonType = {}
win_data['session'] = tabbed_browser.session
active_window = objects.qapp.activeWindow()
if getattr(active_window, 'win_id', None) == win_id:
win_data['active'] = True
win_data['geometry'] = bytes(main_window.saveGeometry())
win_data['tabs'] = []
if tabbed_browser.is_private:
win_data['private'] = True
for i, tab in enumerate(tabbed_browser.widgets()):
active = i == tabbed_browser.widget.currentIndex()
win_data['tabs'].append(self._save_tab(tab, active,
with_history=with_history))
return win_data
def _save_all(self, *, only_window=None, with_private=False, with_history=True): def _save_all(self, *, only_window=None, with_private=False, with_history=True):
"""Get a dict with data for all windows/tabs.""" """Get a dict with data for all windows/tabs."""
data: _JsonType = {'windows': []} session_data: _JsonType = {}
if only_window is not None: if only_window is not None:
winlist: Iterable[int] = [only_window] winlist: Iterable[int] = [only_window]
else: else:
@ -267,30 +293,18 @@ class SessionManager(QObject):
for win_id in sorted(winlist): for win_id in sorted(winlist):
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
main_window = objreg.get('main-window', scope='window',
window=win_id)
# We could be in the middle of destroying a window here
if sip.isdeleted(main_window):
continue
if tabbed_browser.is_private and not with_private: if tabbed_browser.is_private and not with_private:
continue continue
win_data: _JsonType = {} if tabbed_browser.session not in session_data:
active_window = objects.qapp.activeWindow() session_data[tabbed_browser.session] = {'windows': []} # type: _JsonType
if getattr(active_window, 'win_id', None) == win_id:
win_data['active'] = True win_data = self._save_window(win_id=win_id, with_history=with_history)
win_data['geometry'] = bytes(main_window.saveGeometry()) if win_data is not None:
win_data['tabs'] = [] session_data[tabbed_browser.session]['windows'].append(win_data)
if tabbed_browser.is_private:
win_data['private'] = True return session_data
for i, tab in enumerate(tabbed_browser.widgets()):
active = i == tabbed_browser.widget.currentIndex()
win_data['tabs'].append(self._save_tab(tab, active,
with_history=with_history))
data['windows'].append(win_data)
return data
def _get_session_name(self, name): def _get_session_name(self, name):
"""Helper for save to get the name to save the session to. """Helper for save to get the name to save the session to.
@ -308,6 +322,13 @@ class SessionManager(QObject):
name = 'default' name = 'default'
return name return name
def _dump_session(self, path, data):
try:
with qtutils.savefile_open(path) as f:
utils.yaml_dump(data, f)
except (OSError, UnicodeEncodeError, yaml.YAMLError) as e:
raise SessionError(e)
def save(self, name, last_window=False, load_next_time=False, def save(self, name, last_window=False, load_next_time=False,
only_window=None, with_private=False, with_history=True): only_window=None, with_private=False, with_history=True):
"""Save a named session. """Save a named session.
@ -325,35 +346,54 @@ class SessionManager(QObject):
Return: Return:
The name of the saved session. The name of the saved session.
""" """
name = self._get_session_name(name)
path = self._get_session_path(name)
log.sessions.debug("Saving session {} to {}...".format(name, path))
if last_window: if last_window:
data = self._last_window_session session_data = self._last_window_session
if data is None: if session_data is None:
log.sessions.error("last_window_session is None while saving!") log.sessions.error("last_window_session is None while saving!")
return None return None
else: else:
data = self._save_all(only_window=only_window, session_data = self._save_all(only_window=only_window,
with_private=with_private, with_private=with_private,
with_history=with_history) with_history=with_history)
log.sessions.vdebug( # type: ignore[attr-defined] log.sessions.vdebug( # type: ignore[attr-defined]
"Saving data: {}".format(data)) "Saving data: {}".format(session_data))
try: if name == default:
with qtutils.savefile_open(path) as f: # save all active sessions by default
utils.yaml_dump(data, f) names = list(session_data.keys())
except (OSError, UnicodeEncodeError, yaml.YAMLError) as e: # don't save sessionless windows (TODO should be configurable)
raise SessionError(e) if '_nosession' in names:
names.remove('_nosession')
elif os.path.isabs(name):
_save_all_to(name)
if load_next_time:
configfiles.state['general']['session'] = name
return [name]
else:
# could possibly support `:session_save session1 session2`?
names = [name]
# current session is not used atm (could be session of current active window)
for s in names:
path = self._get_session_path(s)
self._dump_session(path, session_data.get(s, { 'windows' : [] }))
if load_next_time: if load_next_time:
configfiles.state['general']['session'] = name configfiles.state['general']['session'] = ','.join(names)
return name return names
def _save_all_to(self, name):
session_data = self._save_all()
data = {'windows': []} # type: _JsonType
for d in session_data.values():
data['windows'].extend(d['windows'])
path = self._get_session_path(name)
self._dump_session(path, data)
def _save_autosave(self): def _save_autosave(self):
"""Save the autosave session.""" """Save the autosave session."""
try: try:
self.save('_autosave') self._save_all_to('_autosave')
except SessionError as e: except SessionError as e:
log.sessions.error("Failed to save autosave session: {}".format(e)) log.sessions.error("Failed to save autosave session: {}".format(e))
@ -372,6 +412,7 @@ class SessionManager(QObject):
def save_last_window_session(self): def save_last_window_session(self):
"""Temporarily save the session for the last closed window.""" """Temporarily save the session for the last closed window."""
#TODO needs to be saved to correct session file and not break if current is None
self._last_window_session = self._save_all() self._last_window_session = self._save_all()
def _load_tab(self, new_tab, data): # noqa: C901 def _load_tab(self, new_tab, data): # noqa: C901
@ -462,6 +503,7 @@ class SessionManager(QObject):
private=win.get('private', None)) private=win.get('private', None))
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=window.win_id) window=window.win_id)
tabbed_browser.session = win.get('session')
tab_to_focus = None tab_to_focus = None
for i, tab in enumerate(win['tabs']): for i, tab in enumerate(win['tabs']):
new_tab = tabbed_browser.tabopen(background=False) new_tab = tabbed_browser.tabopen(background=False)
@ -566,6 +608,77 @@ def session_load(name: str, *,
log.sessions.debug("Loaded & deleted session {}.".format(name)) log.sessions.debug("Loaded & deleted session {}.".format(name))
@cmdutils.register()
@cmdutils.argument('name', completion=miscmodels.session)
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
def session_window_set(name: str, *,
win_id: int = None) -> None:
"""Set session for current window
Args:
name: The name of the session.
"""
if not isinstance(name, Sentinel) and name.startswith('_') and not force:
raise cmdutils.CommandError("{} is an internal session, a window "
"cannot be assigned to it".format(name))
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.session = name
#FIXME Probably not meant to call this from here
tabbed_browser._update_window_title("session")
@cmdutils.register()
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
def session_window_unset(*, win_id: int = None) -> None:
"""Unset session for current window """
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.session = "_nosession"
#FIXME Probably not meant to call this from here
tabbed_browser._update_window_title("session")
@cmdutils.register()
@cmdutils.argument('name', completion=miscmodels.session)
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
def session_close(name: str = None, *,
save: bool = False,
nosave: bool = False,
force: bool = False,
win_id: int = None) -> None:
"""Close a session.
Args:
name: The name of the session. If not given the session of the
current window is closed.
save: Save the session before closing it
nosave: Don't save the session, overrides config.session.save_when_close
"""
if save and nosave:
raise cmdutils.CommandError("Both --save and --nosave have been given.")
if name is not None and name.startswith('_') and not force:
raise cmdutils.CommandError("{} is an internal session, use --force "
"to close anyways.".format(name))
elif name is None:
tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id)
name = tabbed_browser.session
log.sessions.vdebug("Closing session: {}".format(name))
if not nosave and (save or config.val.session.save_when_close):
#TODO also needs to handle private windows if required
session_manager.save(name)
windows = list(objreg.window_registry.values())
for w in windows:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=w.win_id)
if tabbed_browser.session == name:
w.close()
@cmdutils.register() @cmdutils.register()
@cmdutils.argument('name', completion=miscmodels.session) @cmdutils.argument('name', completion=miscmodels.session)
@cmdutils.argument('win_id', value=cmdutils.Value.win_id) @cmdutils.argument('win_id', value=cmdutils.Value.win_id)
@ -601,18 +714,19 @@ def session_save(name: ArgType = default, *,
assert not name.startswith('_') assert not name.startswith('_')
try: try:
if only_active_window: if only_active_window:
name = session_manager.save(name, only_window=win_id, names = session_manager.save(name, only_window=win_id,
with_private=True, with_private=True,
with_history=not no_history) with_history=not no_history)
else: else:
name = session_manager.save(name, with_private=with_private, names = session_manager.save(name, with_private=with_private,
with_history=not no_history) with_history=not no_history)
except SessionError as e: except SessionError as e:
raise cmdutils.CommandError("Error while saving session: {}".format(e)) raise cmdutils.CommandError("Error while saving session: {}".format(e))
if quiet: if quiet:
log.sessions.debug("Saved session {}.".format(name)) log.sessions.debug("Saved sessions {}.".format(','.join(names)))
else: else:
message.info("Saved session {}.".format(name)) #TODO handle Sentinel
message.info("Saved session {}.".format(','.join(names)))
@cmdutils.register() @cmdutils.register()
@ -644,26 +758,34 @@ def load_default(name):
Args: Args:
name: The name of the session to load, or None to read state file. name: The name of the session to load, or None to read state file.
""" """
if name is None and session_manager.exists('_autosave'): if name is not None:
name = '_autosave' names = name.split(',')
elif name is None: elif session_manager.exists('_autosave'):
names = ['_autosave']
elif config.val.session.startup_sessions is not None:
names = config.val.session.startup_sessions
if not isinstance(names, list):
names = [names]
else:
try: try:
name = configfiles.state['general']['session'] names = configfiles.state['general']['session'].split(',')
except KeyError: except KeyError:
# No session given as argument and none in the session file -> # No session given as argument and none in the session file ->
# start without loading a session # start without loading a session
return return
try: for name in names:
session_manager.load(name) try:
except SessionNotFoundError: session_manager.load(name)
message.error("Session {} not found!".format(name)) except SessionNotFoundError:
except SessionError as e: message.error("Session {} not found!".format(name))
message.error("Failed to load session {}: {}".format(name, e)) except SessionError as e:
message.error("Failed to load session {}: {}".format(name, e))
try: try:
del configfiles.state['general']['session'] del configfiles.state['general']['session']
except KeyError: except KeyError:
pass pass
# If this was a _restart session, delete it. # If this was a _restart session, delete it.
#TODO don't forget to handle this special session too
if name == '_restart': if name == '_restart':
session_manager.delete('_restart') session_manager.delete('_restart')

View File

@ -141,7 +141,7 @@ class TestSaveAll:
# FIXME can this ever actually happen? # FIXME can this ever actually happen?
assert not objreg.window_registry assert not objreg.window_registry
data = sess_man._save_all() data = sess_man._save_all()
assert not data['windows'] assert not data
@webengine_refactoring_xfail @webengine_refactoring_xfail
def test_no_active_window(self, sess_man, fake_window, stubs, def test_no_active_window(self, sess_man, fake_window, stubs,