Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to search and find text in the current track #39

Merged
merged 1 commit into from
Aug 20, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
sirken authored May 27, 2024

Verified

This commit was signed with the committer’s verified signature.
sparrc Cameron Sparr
commit efa16d410b764d5401a4743735409c1479d68c45
4 changes: 4 additions & 0 deletions lyrics/lyrics.cfg
Original file line number Diff line number Diff line change
@@ -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

154 changes: 150 additions & 4 deletions lyrics/window.py
Original file line number Diff line number Diff line change
@@ -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)