From efa16d410b764d5401a4743735409c1479d68c45 Mon Sep 17 00:00:00 2001 From: Ken Perry <sirken@users.noreply.github.com> Date: Sun, 26 May 2024 22:55:08 -0600 Subject: [PATCH 1/2] Find in lyrics percent statusbar (#1) Add lyrics search functionality and a track percentage status bar. Enables scrolling 100% to the bottom of the track so that search and manual scroll are compatible --- lyrics/lyrics.cfg | 4 ++ lyrics/window.py | 154 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 154 insertions(+), 4 deletions(-) diff --git a/lyrics/lyrics.cfg b/lyrics/lyrics.cfg index 21ec904..19014a3 100644 --- a/lyrics/lyrics.cfg +++ b/lyrics/lyrics.cfg @@ -11,6 +11,7 @@ mpd_port=6600 mpd_pass= #colors #offset=1 +statusbar=on [BINDINGS] up=arrow_up @@ -31,6 +32,9 @@ autoswitchtoggle=a delete=d edit=e +find=/ +find-next=n +find-prev=p help=h diff --git a/lyrics/window.py b/lyrics/window.py index c448c88..8b0126c 100644 --- a/lyrics/window.py +++ b/lyrics/window.py @@ -51,7 +51,7 @@ def input(self, window, key): elif key == self.binds['delete']: if window.player.track.delete_lyrics(): - window.stdscr.addstr(window.height - 1, window.width - 10, + window.stdscr.addstr(window.height - 1, 1, ' Deleted ', curses.A_REVERSE) elif key == self.binds['help']: window.stdscr.erase() @@ -64,11 +64,13 @@ def input(self, window, key): window.current_pos = 0 window.player.refresh(cache=True) window.update_track() + elif key == self.binds['find']: + window.find() # autoswitch toggle elif key == self.binds['autoswitchtoggle']: window.player.autoswitch = not window.player.autoswitch - window.stdscr.addstr(window.height - 1, window.width - 18, + window.stdscr.addstr(window.height - 1, 1, f" Autoswitch: {'on' if window.player.autoswitch else 'off'} ", curses.A_REVERSE) class HelpPage: @@ -134,6 +136,7 @@ def main(self): class Window: def __init__(self, stdscr, player, timeout): + self.options = Config('OPTIONS') self.stdscr = stdscr self.height, self.width = stdscr.getmaxyx() self.player = player @@ -143,6 +146,7 @@ def __init__(self, stdscr, player, timeout): self.pad_offset = 1 self.text_padding = 5 self.keys = Key() + self.find_position = 0 curses.use_default_colors() self.stdscr.timeout(timeout) @@ -170,7 +174,16 @@ def set_titlebar(self): self.stdscr.addstr(1, 1, track_info[1], curses.A_REVERSE | curses.A_BOLD | curses.A_DIM) self.stdscr.addstr(2, 1, track_info[2], curses.A_REVERSE) - + + def set_statusbar(self): + if self.options['statusbar'] == 'on': + text = self.player.track.get_text(wrap=True, width=self.width - self.text_padding) + lines = text.split('\n') + if self.current_pos < 0: + self.current_pos = 0 + pct_progress = f' {int(self.current_pos * 100 / len(lines)) + 1}% ' + self.stdscr.insstr(self.height - 1, self.width - len(pct_progress), pct_progress, curses.A_DIM) + def set_offset(self): if self.player.track.alignment == 0: # center align @@ -181,9 +194,10 @@ def set_offset(self): self.pad_offset = (self.width - self.player.track.width) def scroll_down(self, step=1): - if self.current_pos < self.player.track.length - (self.height * 0.5): + if self.current_pos < self.player.track.length - step: self.current_pos += step else: + self.current_pos = self.player.track.length - 1 self.stdscr.addstr(self.height - 1, 1, 'END', curses.A_REVERSE) def scroll_up(self, step=1): @@ -194,6 +208,137 @@ def scroll_up(self, step=1): self.stdscr.clrtoeol() self.current_pos -= step + def find_check_keys(self, key=None, lines_map=[]): + if key == self.keys.binds['find-next']: + self.stdscr.addstr(self.height - 1, self.width - 3, 'n ') + self.stdscr.clrtoeol() + # reached end of matches, loop back to start + if self.find_position + 1 >= len(lines_map): + self.find_position = 0 + else: + self.find_position += 1 + return True + elif key == self.keys.binds['find-prev']: + self.stdscr.addstr(self.height - 1, self.width - 3, 'p ') + self.stdscr.clrtoeol() + if self.find_position - 1 < 0: + self.find_position = len(lines_map) - 1 + else: + self.find_position -= 1 + return True + # other keys for more accessibility + elif key == self.keys.binds['down']: + self.scroll_down() + elif key == self.keys.binds['up']: + self.scroll_up() + elif key == self.keys.binds['step-down']: + self.scroll_down(self.keys.binds['step-size']) + elif key == self.keys.binds['step-up']: + self.scroll_up(self.keys.binds['step-size']) + elif key == self.keys.binds['find']: + self.find() + return False + + def find(self): + # wait for input + self.stdscr.timeout(-1) + prompt = ':' + self.stdscr.move(self.height - 1, 0) + self.stdscr.clrtoeol() + self.stdscr.addstr(self.height - 1, self.pad_offset, prompt) + self.set_statusbar() + # show cursor and key presses during find + curses.echo() + curses.curs_set(1) + + # (y, x, input max length), case-insensitive + find_string = self.stdscr.getstr(self.height - 1, len(prompt)+self.pad_offset, 100).decode(encoding="utf-8").strip() + + # hide cursor and key presses + curses.curs_set(0) + curses.noecho() + + if find_string: + # use word wrap which covers both wrap/nowrap and ensures line count is accurate + text = self.player.track.get_text(wrap=True, width=self.width - self.text_padding) + lines = text.split('\n') + + # [0,9,10,14] list of lines that contain a match + lines_map = [] + for line_num, line in enumerate(lines): + # case-insensitive match + if find_string.lower() in line.lower(): + lines_map.append(line_num) + + if len(lines_map) > 0: + + # continue search from current position + for line in lines_map: + # >= causes us to stay on the current line for a new search + if line >= self.current_pos: + self.find_position = lines_map.index(line) + break + # otherwise loop back to the start + else: + self.find_position = 0 + + while True: + # update current position based on where we are at in the find + self.current_pos = lines_map[self.find_position] + + # duplicated from main() to manually refresh on find + self.stdscr.clear() + self.set_titlebar() + self.stdscr.refresh() + self.scroll_pad.refresh(self.current_pos, 0, 4, self.pad_offset, self.height - 2, self.width - 1) + + # find & status bar output + find_string_output = f' {find_string} ' + find_count_output = f" {self.find_position + 1}/{len(lines_map)} " + self.stdscr.addstr(self.height - 1, self.pad_offset, find_string_output, curses.A_REVERSE) + self.stdscr.insstr(self.height - 1, self.pad_offset + len(find_string_output) + 1, find_count_output) + # multiple matches, show next/prev + if len(lines_map) > 1: + help_output = f"[{chr(self.keys.binds['find-next'])}]=next, [{chr(self.keys.binds['find-prev'])}]=prev" + self.stdscr.addstr(self.height - 1, self.pad_offset + len(find_string_output) + len(find_count_output) + 2, help_output) + self.set_statusbar() + + # highlight found text + line_text = lines[self.current_pos] + # case-insensitive, [4, 5, 21] + found_index_list = [i for i in range(len(line_text)) if line_text.lower().startswith(find_string.lower(), i)] + # loop over each character in the line, highlight found strings + highlight_end = -1 + for cpos, char in enumerate(line_text): + attr = curses.A_NORMAL + # start of found string + if cpos in found_index_list: + highlight_end = cpos + len(find_string) + attr = curses.A_REVERSE + # inside string + elif highlight_end > cpos: + attr = curses.A_REVERSE + self.stdscr.addch(4, self.pad_offset + cpos, char, attr) + + # after finding a match in a line, stop, wait for input + self.stdscr.timeout(10000) + key = self.stdscr.getch() + result = self.find_check_keys(key, lines_map) + if not result: + break + + else: + output = ' not found ' + self.stdscr.insstr(self.height - 1, self.pad_offset + len(prompt) + len(find_string) + 2, output, curses.A_REVERSE) + self.set_statusbar() + # timeout or key press + self.stdscr.timeout(5000) + key = self.stdscr.getch() + self.find_check_keys(key, lines_map) + + # clear search line + self.stdscr.clear() + def update_track(self): self.stdscr.clear() self.scroll_pad.clear() @@ -228,6 +373,7 @@ def main(self): self.keys.input(self, key) self.set_titlebar() + self.set_statusbar() self.stdscr.refresh() self.scroll_pad.refresh(self.current_pos, 0, 4, self.pad_offset, self.height - 2, self.width - 1) From 80536d213e03b70082c12a065351609540c0b35d Mon Sep 17 00:00:00 2001 From: Samarth Jugran <jugransamarth@gmail.com> Date: Tue, 20 Aug 2024 16:41:26 +0530 Subject: [PATCH 2/2] reset timeout after using search --- lyrics/__init__.py | 2 +- lyrics/window.py | 12 +++++++----- setup.py | 3 ++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lyrics/__init__.py b/lyrics/__init__.py index b5e2b64..07c2e44 100644 --- a/lyrics/__init__.py +++ b/lyrics/__init__.py @@ -4,7 +4,7 @@ CONFIG_PATH = Path.home().joinpath('.config', 'lyrics-in-terminal','lyrics.cfg') -__version__ = '1.5.1-dev' +__version__ = '1.6.0-dev' if not CONFIG_PATH.exists(): from shutil import copy diff --git a/lyrics/window.py b/lyrics/window.py index 8b0126c..8d26e0d 100644 --- a/lyrics/window.py +++ b/lyrics/window.py @@ -147,9 +147,10 @@ def __init__(self, stdscr, player, timeout): self.text_padding = 5 self.keys = Key() self.find_position = 0 + self.timeout = timeout curses.use_default_colors() - self.stdscr.timeout(timeout) + self.stdscr.timeout(self.timeout) self.set_up() def set_up(self): @@ -177,11 +178,10 @@ def set_titlebar(self): def set_statusbar(self): if self.options['statusbar'] == 'on': - text = self.player.track.get_text(wrap=True, width=self.width - self.text_padding) - lines = text.split('\n') + lines = self.player.track.length if self.current_pos < 0: self.current_pos = 0 - pct_progress = f' {int(self.current_pos * 100 / len(lines)) + 1}% ' + pct_progress = f' {round(self.current_pos * 100 / lines) + 1}% ' self.stdscr.insstr(self.height - 1, self.width - len(pct_progress), pct_progress, curses.A_DIM) def set_offset(self): @@ -252,7 +252,8 @@ def find(self): curses.curs_set(1) # (y, x, input max length), case-insensitive - find_string = self.stdscr.getstr(self.height - 1, len(prompt)+self.pad_offset, 100).decode(encoding="utf-8").strip() + find_string = self.stdscr.getstr(self.height - 1, len(prompt) + self.pad_offset, 100) + find_string = find_string.decode(encoding="utf-8").strip() # hide cursor and key presses curses.curs_set(0) @@ -338,6 +339,7 @@ def find(self): # clear search line self.stdscr.clear() + self.stdscr.timeout(self.timeout) def update_track(self): self.stdscr.clear() diff --git a/setup.py b/setup.py index 10743d4..fce2c75 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,8 @@ def updateConfigFile(self): packages=find_packages(), entry_points={ 'console_scripts': [ - 'lyrics = lyrics.lyrics_in_terminal:main' + 'lyrics = lyrics.lyrics_in_terminal:main', + 'lyt = lyrics.lyrics_in_terminal:main' ] }, classifiers=[