diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 1c6806e56..511003978 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -35,6 +35,7 @@ |<>|Set the CSS media type. |<>|Whether to remove finished downloads automatically. |<>|Whether to hide the statusbar unless a message is shown. +|<>|The format to use for the window title. The following placeholders are defined: |============== .Quick reference for section ``network'' @@ -466,6 +467,17 @@ Valid values: Default: +pass:[false]+ +[[ui-window-title-format]] +=== window-title-format +The format to use for the window title. The following placeholders are defined: + +* `{perc}`: The percentage as a string like `[10%]`. +* `{perc_raw}`: The raw percentage, e.g. `10` +* `{title}`: The title of the current webpage +* `{title_sep}`: The string ` - ` if a title is set, empty otherwise. + +Default: +pass:[{perc}{title}{title_sep}qutebrowser]+ + == network Settings related to the network. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 06863d772..a5336a397 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -241,6 +241,18 @@ DATA = collections.OrderedDict([ ('hide-statusbar', SettingValue(typ.Bool(), 'false'), "Whether to hide the statusbar unless a message is shown."), + + ('window-title-format', + SettingValue(typ.FormatString(fields=['perc', 'perc_raw', 'title', + 'title_sep']), + '{perc}{title}{title_sep}qutebrowser'), + "The format to use for the window title. The following placeholders " + "are defined:\n\n" + "* `{perc}`: The percentage as a string like `[10%]`.\n" + "* `{perc_raw}`: The raw percentage, e.g. `10`\n" + "* `{title}`: The title of the current webpage\n" + "* `{title_sep}`: The string ` - ` if a title is set, empty " + "otherwise.") )), ('network', sect.KeyValue( diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index d74aa6a0a..55606f45e 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -826,6 +826,32 @@ class Directory(BaseType): return os.path.expanduser(value) +class FormatString(BaseType): + + """A string with '{foo}'-placeholders.""" + + typestr = 'format-string' + + def __init__(self, fields, none_ok=False): + super().__init__(none_ok) + self.fields = fields + + def validate(self, value): + if not value: + if self._none_ok: + return + else: + raise configexc.ValidationError(value, "may not be empty!") + s = self.transform(value) + try: + return s.format(**{k: '' for k in self.fields}) + except KeyError as e: + raise configexc.ValidationError(value, "Invalid placeholder " + "{}".format(e)) + except ValueError as e: + raise configexc.ValidationError(value, str(e)) + + class WebKitBytes(BaseType): """A size with an optional suffix. diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index c24732624..9e38b2f3c 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -113,6 +113,7 @@ class TabbedBrowser(tabwidget.TabWidget): # https://github.com/The-Compiler/qutebrowser/issues/119 self.setIconSize(QSize(12, 12)) objreg.get('config').changed.connect(self.update_favicons) + objreg.get('config').changed.connect(self.update_window_title) def __repr__(self): return utils.get_repr(self, count=self.count()) @@ -128,21 +129,23 @@ class TabbedBrowser(tabwidget.TabWidget): w.append(self.widget(i)) return w - def _update_window_title(self): + @config.change_filter('ui', 'window-title-format') + def update_window_title(self): """Change the window title to match the current tab.""" idx = self.currentIndex() tabtitle = self.tabText(idx) widget = self.widget(idx) + fields = {} if widget.load_status == webview.LoadStatus.loading: - title = '[{}%] '.format(widget.progress) + fields['perc'] = '[{}%] '.format(widget.progress) else: - title = '' - if not tabtitle: - title += 'qutebrowser' - else: - title += '{} - qutebrowser'.format(tabtitle) - self.window().setWindowTitle(title) + fields['perc'] = '' + fields['perc_raw'] = widget.progress + fields['title'] = tabtitle + fields['title_sep'] = ' - ' if tabtitle else '' + fmt = config.get('ui', 'window-title-format') + self.window().setWindowTitle(fmt.format(**fields)) def _connect_tab_signals(self, tab): """Set up the needed signals for tab.""" @@ -431,7 +434,7 @@ class TabbedBrowser(tabwidget.TabWidget): else: self.setTabIcon(idx, QIcon()) if idx == self.currentIndex(): - self._update_window_title() + self.update_window_title() @pyqtSlot() def on_cur_load_started(self): @@ -467,7 +470,7 @@ class TabbedBrowser(tabwidget.TabWidget): return self.setTabText(idx, text.replace('&', '&&')) if idx == self.currentIndex(): - self._update_window_title() + self.update_window_title() @pyqtSlot(webview.WebView, str) def on_url_text_changed(self, tab, url): @@ -539,7 +542,7 @@ class TabbedBrowser(tabwidget.TabWidget): scope='window', window=self._win_id) self._now_focused = tab self.current_tab_changed.emit(tab) - self._update_window_title() + self.update_window_title() self._tab_insert_idx_left = self.currentIndex() self._tab_insert_idx_right = self.currentIndex() + 1 @@ -561,7 +564,7 @@ class TabbedBrowser(tabwidget.TabWidget): color = utils.interpolate_color(start, stop, perc, system) self.tabBar().set_tab_indicator_color(idx, color) if idx == self.currentIndex(): - self._update_window_title() + self.update_window_title() def on_load_finished(self, tab): """Adjust tab indicator when loading finished. @@ -584,7 +587,7 @@ class TabbedBrowser(tabwidget.TabWidget): color = utils.interpolate_color(start, stop, 100, system) self.tabBar().set_tab_indicator_color(idx, color) if idx == self.currentIndex(): - self._update_window_title() + self.update_window_title() def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. diff --git a/qutebrowser/test/config/test_configtypes.py b/qutebrowser/test/config/test_configtypes.py index 3ec3e6cb5..adc4ada9a 100644 --- a/qutebrowser/test/config/test_configtypes.py +++ b/qutebrowser/test/config/test_configtypes.py @@ -1976,5 +1976,45 @@ class UrlListTests(unittest.TestCase): self.assertEqual(self.t.transform(''), None) +class FormatStringTests(unittest.TestCase): + + """Test FormatString.""" + + def setUp(self): + self.t = configtypes.FormatString(fields=('foo', 'bar')) + + def test_transform(self): + """Test if transform doesn't alter the value.""" + self.assertEqual(self.t.transform('foo {bar} baz'), 'foo {bar} baz') + + def test_validate_simple(self): + """Test validate with a simple string.""" + self.t.validate('foo bar baz') + + def test_validate_placeholders(self): + """Test validate with placeholders.""" + self.t.validate('{foo} {bar} baz') + + def test_validate_invalid_placeholders(self): + """Test validate with invalid placeholders.""" + with self.assertRaises(configexc.ValidationError): + self.t.validate('{foo} {bar} {baz}') + + def test_validate_invalid_placeholders_syntax(self): + """Test validate with invalid placeholders syntax.""" + with self.assertRaises(configexc.ValidationError): + self.t.validate('{foo} {bar') + + def test_validate_empty(self): + """Test validate with empty string and none_ok = False.""" + with self.assertRaises(configexc.ValidationError): + self.t.validate('') + + def test_validate_empty_none_ok(self): + """Test validate with empty string and none_ok = True.""" + t = configtypes.FormatString(none_ok=True, fields=()) + t.validate('') + + if __name__ == '__main__': unittest.main()