From d4d0c1a19f5fc5df0f082df1fb7323141175a310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Sun, 10 May 2020 11:08:05 +0200 Subject: Add function to calculate unique prefix of two strings --- poezio/common.py | 20 ++++++++++++++++++++ test/test_common.py | 21 ++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) 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/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" -- cgit v1.2.3 From c1863addfd33d3d3f3f03bcb036a0966097914cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Sun, 10 May 2020 14:38:43 +0200 Subject: Add function to find a tab by unique prefix --- poezio/core/tabs.py | 33 ++++++++++++++++++++++++++++++++- test/test_tabs.py | 22 ++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/poezio/core/tabs.py b/poezio/core/tabs.py index abea7313..c5ecb206 100644 --- a/poezio/core/tabs.py +++ b/poezio/core/tabs.py @@ -24,7 +24,7 @@ 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 @@ -139,6 +139,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/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) -- cgit v1.2.3 From eab4615fe046d40aefdf4cd18643f99183fc4aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Sun, 10 May 2020 11:21:56 +0200 Subject: Add /wup command The `/wup` command selects a tab by the prefix of its name only. The `/win` will do a substring match based on the first tab going from the current tab which matches the substring. This can be confusing, especially since `/win` matches on different types of tab "names" not only on the name which is shown in the info bar by default. The `/wup` command exclusively matches based on the prefix of the tab.name string. This has the advantage that it is consistent, deterministic and independent of the currently selected tab. --- doc/source/commands.rst | 9 +++++++++ poezio/core/commands.py | 14 ++++++++++++++ poezio/core/core.py | 6 ++++++ poezio/core/tabs.py | 1 + 4 files changed, 30 insertions(+) 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 `` + + 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 [status message]`` 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 + """ + 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..eac9d539 100644 --- a/poezio/core/core.py +++ b/poezio/core/core.py @@ -1709,6 +1709,12 @@ class Core: usage='', shortdesc='Go to the specified room', completion=self.completion.win) + self.register_command( + 'wup', + self.command.wup, + usage='', + 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 c5ecb206..d5909d39 100644 --- a/poezio/core/tabs.py +++ b/poezio/core/tabs.py @@ -29,6 +29,7 @@ from collections import defaultdict from slixmpp import JID from poezio import tabs from poezio.events import EventHandler +from poezio.config import config class Tabs: -- cgit v1.2.3 From a15e52dc39e7c916804c788ff1e832fd6f58c735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Sun, 10 May 2020 11:21:27 +0200 Subject: Add option to show only the unique prefix of tab names When the set of tabs used fluctuate, the memory of which number belongs to which chat becomes difficult to work with. Non-numbers can be used to navigate tabs with `/win`, however, it is difficult to know which letters are required to select a certain tab. This option introduces a display mode for tab names where only the unique prefix of the tab name is shown, saving space and providing with a minimal string which can be used with `/win`. --- data/default_config.cfg | 7 +++++++ doc/source/configuration.rst | 11 +++++++++++ poezio/config.py | 1 + poezio/windows/info_bar.py | 34 +++++++++++++++++++++++++++++++++- 4 files changed, 52 insertions(+), 1 deletion(-) 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/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/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/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: -- cgit v1.2.3 From 1e7ce43789aae67fcfbb4b6fbe49a299fda1cfa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Sun, 10 May 2020 11:46:31 +0200 Subject: Add keyboard action to go to room by unique prefix This is especially useful in combination with the newly introduced `unique_prefix_tab_names` config option. It has no default binding. --- doc/source/keys.rst | 9 +++++++++ poezio/core/core.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) 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/core/core.py b/poezio/core/core.py index eac9d539..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()) -- cgit v1.2.3