diff options
-rw-r--r-- | data/default_config.cfg | 7 | ||||
-rw-r--r-- | doc/source/commands.rst | 9 | ||||
-rw-r--r-- | doc/source/configuration.rst | 11 | ||||
-rw-r--r-- | doc/source/keys.rst | 9 | ||||
-rw-r--r-- | poezio/common.py | 20 | ||||
-rw-r--r-- | poezio/config.py | 1 | ||||
-rw-r--r-- | poezio/core/commands.py | 14 | ||||
-rw-r--r-- | poezio/core/core.py | 35 | ||||
-rw-r--r-- | poezio/core/tabs.py | 34 | ||||
-rw-r--r-- | poezio/windows/info_bar.py | 34 | ||||
-rw-r--r-- | test/test_common.py | 21 | ||||
-rw-r--r-- | test/test_tabs.py | 22 |
12 files changed, 214 insertions, 3 deletions
diff --git a/data/default_config.cfg b/data/default_config.cfg index 908a3d70..aef4d62a 100644 --- a/data/default_config.cfg +++ b/data/default_config.cfg @@ -548,6 +548,13 @@ use_bookmarks_method = # “true” should be the most comfortable value #lazy_resize = true +# If set to true and if show_tab_names is set, the info bar will only show +# the unique prefix of each tab name instead of the full name. This saves a +# lot of space if many tabs exist or are active. +# Best used with the `/wup` command or the `_go_to_room_name` action to select +# a tab based on the prefix. +#unique_prefix_tab_names = false + [bindings] # Bindings are keyboard shortcut aliases. You can use them # to define your own keys and bind them with some functions diff --git a/doc/source/commands.rst b/doc/source/commands.rst index dab2eceb..5ea69abd 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -93,6 +93,15 @@ These commands work in *any* tab. Go to the matching tab. If the argument is a number, it goes to the tab with that number. Otherwise, it goes to the next tab whose name contains the given string. + /wup + + **Usage:** ``/wup <prefix>`` + + Go to the tab whose name starts with `prefix`. If multiple tabs start + with that prefix, no action is taken. + + (Mnemonic: Window by Unique Prefix) + /status **Usage:** ``/status <availability> [status message]`` diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 9619022a..aceb6fb4 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -793,6 +793,17 @@ or the way messages are displayed. If you want to show the tab name in the bottom Tab bar, set this to ``true``. + unique_prefix_tab_names + + **Default value:** ``false`` + + If this and :term:`show_tab_names` is set to true, only the shortest + unique prefix of each tab name is shown instead of the full name. This + can declutter the interface in an instance with many tabs shown in the + interface, while not having to use numbers (which may change completely due to reordering). + + Takes precedence over `use_tab_nicks`. + show_tab_numbers **Default value:** ``true`` diff --git a/doc/source/keys.rst b/doc/source/keys.rst index dc5fa35b..03ab2071 100644 --- a/doc/source/keys.rst +++ b/doc/source/keys.rst @@ -374,6 +374,15 @@ Actions list Similar to F4. +**_go_to_room_name**: Jump to a tab by unique prefix. + + Similar to :term:`/wup` and the default *Alt-j*. This action will take + input as long as there is at least one tab name starting with the input + given so far. If there is exactly one tab matching, the action completes + and the current tab is switched over to the tab matching the input. If + no tab matches, the action completes without any change. This means that + you can typically abort the action with Escape. + Status actions ~~~~~~~~~~~~~~ diff --git a/poezio/common.py b/poezio/common.py index ba179310..7cddc306 100644 --- a/poezio/common.py +++ b/poezio/common.py @@ -17,6 +17,7 @@ import subprocess import time import string import logging +import itertools from slixmpp import JID, InvalidJID, Message from poezio.poezio_shlex import shlex @@ -468,3 +469,22 @@ def safeJID(*args, **kwargs) -> JID: exc_info=True, ) return JID('') + + +def unique_prefix_of(a: str, b: str) -> str: + """ + Return the unique prefix of `a` with `b`. + + Corner cases: + + * If `a` and `b` share no prefix, the first letter of `a` is returned. + * If `a` and `b` are equal, `a` is returned. + * If `a` is a prefix of `b`, `a` is returned. + * If `b` is a prefix of `a`, `b` plus the first letter of `a` after the + common prefix is returned. + """ + for i, (ca, cb) in enumerate(itertools.zip_longest(a, b)): + if ca != cb: + return a[:i+1] + # both are equal, return a + return a diff --git a/poezio/config.py b/poezio/config.py index 8da71071..09d465cd 100644 --- a/poezio/config.py +++ b/poezio/config.py @@ -136,6 +136,7 @@ DEFAULT_CONFIG = { 'theme': 'default', 'themes_dir': '', 'tmp_image_dir': '', + 'unique_prefix_tab_names': False, 'use_bookmarks_method': '', 'use_log': True, 'use_remote_bookmarks': True, diff --git a/poezio/core/commands.py b/poezio/core/commands.py index 6bf1d338..46dab5cc 100644 --- a/poezio/core/commands.py +++ b/poezio/core/commands.py @@ -219,6 +219,20 @@ class CommandCore: return self.core.tabs.set_current_tab(match) + @command_args_parser.quoted(1) + def wup(self, args): + """ + /wup <prefix of name> + """ + if args is None: + return self.help('wup') + + prefix = args[0] + _, match = self.core.tabs.find_by_unique_prefix(prefix) + if match is None: + return + self.core.tabs.set_current_tab(match) + @command_args_parser.quoted(2) def move_tab(self, args): """ diff --git a/poezio/core/core.py b/poezio/core/core.py index 06d56062..8ac88dd4 100644 --- a/poezio/core/core.py +++ b/poezio/core/core.py @@ -209,6 +209,7 @@ class Core: '_show_plugins': self.command.plugins, '_show_xmltab': self.command.xml_tab, '_toggle_pane': self.toggle_left_pane, + "_go_to_room_name": self.go_to_room_name, ###### status actions ###### '_available': lambda: self.command.status('available'), '_away': lambda: self.command.status('away'), @@ -1108,6 +1109,34 @@ class Core: keyboard.continuation_keys_callback = read_next_digit + def go_to_room_name(self) -> None: + room_name_jump = [] + + def read_next_letter(s) -> None: + nonlocal room_name_jump + room_name_jump.append(s) + any_matched, unique_tab = self.tabs.find_by_unique_prefix( + "".join(room_name_jump) + ) + + if not any_matched: + return + + if unique_tab is not None: + self.tabs.set_current_tab(unique_tab) + # NOTE: returning here means that as soon as the tab is + # matched, normal input resumes. If we do *not* return here, + # any further characters matching the prefix of the tab will + # be swallowed (and a lot of tab switching will happen...), + # until a non-matching character or escape or something is + # pressed. + # This behaviour *may* be desirable. + return + + keyboard.continuation_keys_callback = read_next_letter + + keyboard.continuation_keys_callback = read_next_letter + def go_to_roster(self) -> None: "Select the roster as the current tab" self.tabs.set_current_tab(self.tabs.first()) @@ -1709,6 +1738,12 @@ class Core: usage='<number or name>', shortdesc='Go to the specified room', completion=self.completion.win) + self.register_command( + 'wup', + self.command.wup, + usage='<prefix>', + shortdesc='Go to the tab whose name uniquely starts with prefix', + completion=self.completion.win) self.commands['w'] = self.commands['win'] self.register_command( 'move_tab', diff --git a/poezio/core/tabs.py b/poezio/core/tabs.py index abea7313..d5909d39 100644 --- a/poezio/core/tabs.py +++ b/poezio/core/tabs.py @@ -24,11 +24,12 @@ have become [0|1|2|3], with the tab "4" renumbered to "3" if gap tabs are disabled. """ -from typing import List, Dict, Type, Optional, Union +from typing import List, Dict, Type, Optional, Union, Tuple from collections import defaultdict from slixmpp import JID from poezio import tabs from poezio.events import EventHandler +from poezio.config import config class Tabs: @@ -139,6 +140,37 @@ class Tabs: return self._tabs[i] return None + def find_by_unique_prefix(self, prefix: str) -> Tuple[bool, Optional[tabs.Tab]]: + """ + Get a tab by its unique name prefix, ignoring case. + + :return: A tuple indicating the presence of any match, as well as the + uniquely matched tab (if any). + + The first element, a boolean, in the returned tuple indicates whether + at least one tab matched. + + The second element (a Tab) in the returned tuple is the uniquely + matched tab, if any. If multiple or no tabs match the prefix, the + second element in the tuple is :data:`None`. + """ + + # TODO: should this maybe use something smarter than .lower()? + # something something stringprep? + prefix = prefix.lower() + candidate = None + any_matched = False + for tab in self._tabs: + if not tab.name.lower().startswith(prefix): + continue + any_matched = True + if candidate is not None: + # multiple tabs match -> return None + return True, None + candidate = tab + + return any_matched, candidate + def by_name_and_class(self, name: str, cls: Type[tabs.Tab]) -> Optional[tabs.Tab]: """Get a tab with its name and class""" diff --git a/poezio/windows/info_bar.py b/poezio/windows/info_bar.py index ac900103..e94e1810 100644 --- a/poezio/windows/info_bar.py +++ b/poezio/windows/info_bar.py @@ -6,6 +6,7 @@ The GlobalInfoBar can be either horizontal or vertical (VerticalGlobalInfoBar). """ import logging +import itertools log = logging.getLogger(__name__) import curses @@ -13,6 +14,7 @@ import curses from poezio.config import config from poezio.windows.base_wins import Win from poezio.theming import get_theme, to_curses_attr +from poezio.common import unique_prefix_of class GlobalInfoBar(Win): @@ -33,6 +35,34 @@ class GlobalInfoBar(Win): show_nums = config.get('show_tab_numbers') use_nicks = config.get('use_tab_nicks') show_inactive = config.get('show_inactive_tabs') + unique_prefix_tab_names = config.get('unique_prefix_tab_names') + + if unique_prefix_tab_names: + unique_prefixes = [None] * len(self.core.tabs) + sorted_tab_indices = sorted( + (str(tab.name), i) + for i, tab in enumerate(self.core.tabs) + ) + prev_name = "" + for (name, i), next_item in itertools.zip_longest( + sorted_tab_indices, sorted_tab_indices[1:]): + # TODO: should this maybe use something smarter than .lower()? + # something something stringprep? + name = name.lower() + prefix_prev = unique_prefix_of(name, prev_name) + if next_item is not None: + prefix_next = unique_prefix_of(name, next_item[0].lower()) + else: + prefix_next = name[0] + + # to be unique, we have to use the longest prefix + if len(prefix_next) > len(prefix_prev): + prefix = prefix_next + else: + prefix = prefix_prev + + unique_prefixes[i] = prefix + prev_name = name for nb, tab in enumerate(self.core.tabs): if not tab: @@ -46,7 +76,9 @@ class GlobalInfoBar(Win): if show_names: self.addstr(' ', to_curses_attr(color)) if show_names: - if use_nicks: + if unique_prefix_tab_names: + self.addstr(unique_prefixes[nb], to_curses_attr(color)) + elif use_nicks: self.addstr("%s" % str(tab.get_nick()), to_curses_attr(color)) else: diff --git a/test/test_common.py b/test/test_common.py index d7bc2b8b..b6560fc9 100644 --- a/test/test_common.py +++ b/test/test_common.py @@ -11,7 +11,7 @@ from poezio.common import (_datetime_tuple as datetime_tuple, get_utc_time, get_local_time, shell_split, _find_argument_quoted as find_argument_quoted, _find_argument_unquoted as find_argument_unquoted, parse_str_to_secs, - parse_secs_to_str, safeJID) + parse_secs_to_str, safeJID, unique_prefix_of) def test_utc_time(): delta = timedelta(seconds=-3600) @@ -63,3 +63,22 @@ def test_parse_secs_to_str(): def test_safeJID(): assert safeJID('toto@titi/tata') == JID('toto@titi/tata') assert safeJID('toto@…') == JID('') + +def test_unique_prefix_of__no_shared_prefix(): + assert unique_prefix_of("a", "b") == "a" + assert unique_prefix_of("foo", "bar") == "f" + assert unique_prefix_of("foo", "") == "f" + +def test_unique_prefix_of__equal(): + assert unique_prefix_of("foo", "foo") == "foo" + +def test_unique_prefix_of__a_prefix(): + assert unique_prefix_of("foo", "foobar") == "foo" + +def test_unique_prefix_of__b_prefix(): + assert unique_prefix_of("foobar", "foo") == "foob" + +def test_unique_prefix_of__normal_shared_prefix(): + assert unique_prefix_of("foobar", "foobaz") == "foobar" + assert unique_prefix_of("fnord", "funky") == "fn" + assert unique_prefix_of("asbestos", "aspergers") == "asb" diff --git a/test/test_tabs.py b/test/test_tabs.py index 0a6930d4..6989bd67 100644 --- a/test/test_tabs.py +++ b/test/test_tabs.py @@ -183,3 +183,25 @@ def test_slice(): tabs.append(dummy3) assert tabs[1:2][0] is dummy2 + +def test_find_by_unique_prefix(): + DummyTab.reset() + tabs = Tabs(h) + t1 = DummyTab() + t2 = DummyTab() + t3 = DummyTab() + tabs.append(t1) + tabs.append(t2) + tabs.append(t3) + + t1.name = "foo" + t2.name = "bar" + t3.name = "fnord" + + assert tabs.find_by_unique_prefix("f") == (True, None) + assert tabs.find_by_unique_prefix("b") == (True, t2) + assert tabs.find_by_unique_prefix("fo") == (True, t1) + assert tabs.find_by_unique_prefix("fn") == (True, t3) + assert tabs.find_by_unique_prefix("fx") == (False, None) + assert tabs.find_by_unique_prefix("x") == (False, None) + assert tabs.find_by_unique_prefix("") == (True, None) |