From 446ec69374e4793986bc4d7d29ea04230a86ed1d Mon Sep 17 00:00:00 2001 From: Spiros Georgaras Date: Sun, 8 Sep 2019 11:00:31 +0300 Subject: [PATCH] Version 0.8.0: Adding station editor --- Changelog | 7 +- README.html | 14 +-- README.md | 29 +++--- pyradio.1 | 52 +++++++--- pyradio/__init__.py | 2 +- pyradio/edit.py | 15 ++- pyradio/radio.py | 8 +- pyradio/simple_curses_widgets.py | 171 +++++++++++++++++-------------- 8 files changed, 171 insertions(+), 127 deletions(-) diff --git a/Changelog b/Changelog index 1e1bf4f9..afbd0970 100644 --- a/Changelog +++ b/Changelog @@ -1,14 +1,15 @@ -2019-00-01 s-n-g +2019-09-08 s-n-g * Adding station editor ("a" and "A" to add a station, "e" to edit) + * Line editor supports unlimited string length * Main help window separated to two pages (navigation with "n" / "p") * Changing "e" to "E" to change a station's encoding * Changing "p" to "P" to jump to playing station / loaded playlist * Adding H, L to jump to top / bottom of screen - * Canging M to jump to middle of screen + * Changing M to jump to middle of screen * Changing volume, saving volume and muting is now available on most windows (pop up and questions) * Manipulating volume (keys m,v) on a help window, will close it - if player not playing. + if player not playing * Adding ^U, ^D to move station up, down * Search string will not be lost after displaying help * PyRadio runs on Windows (finally). Added an installation BAT file, diff --git a/README.html b/README.html index 7ec8926d..3c5107be 100644 --- a/README.html +++ b/README.html @@ -36,11 +36,11 @@

Table of contents <
  • Controls
  • Config file
  • About Playlist files
  • +
  • Search function
  • Moving stations around
  • Specifying stations’ encoding
  • Player detection / selection
  • Player default volume level
  • -
  • Search function
  • PyRadio Themes
  • Session Locking
  • Update notification
  • @@ -177,6 +177,12 @@

    Managing “foreign” playlists

    A playlist that does not reside within the program’s configuration directory is considered a “foreign” playlist. This playlist can only be opened by the -s command line option.

    When this happens, PyRadio will offer you the choice to copy the playlist in its configuration directory, thus making it available for manipulation within the program.

    If a playlist of the same name already exists in the configuration directory, the “foreign” playlist will be time-stamped. For example, if a “foreign” playlist is named “stations.csv”, it will be named “2019-01-11_13-35-47_stations.csv” (provided that the action was taken on January 11, 2019 at 13:35:47).

    +

    Search function Top

    +

    On any window presenting a list of items (stations, playlists, themes) a search function is available by pressing “/”.

    +

    The Search Window supports normal and extend editing and in session history.

    +

    One can always get help by pressing the “?” key.

    +

    After a search term has been successfully found (search is case insensitive), next occurrence can be obtained using the “n” key and previous occurrence can be obtained using the “N” key.

    +

    Note: Python 2 users are confined in typing ASCII characters only.

    Moving stations around Top

    Rearranging the order of the stations in the playlist is another feature PyRadio offers.

    All you have to do is specify the source station (the station to be moved) and the position it will be moved to (target).

    @@ -247,12 +253,6 @@

    MPlayer

    [pyradio] volstep=1 volume=28 -

    Search function Top

    -

    On any window presenting a list of items (stations, playlists, themes) a search function is available by pressing “/”.

    -

    The Search Window supports normal and extend editing and in session history.

    -

    One can always get help by pressing the “?” key.

    -

    After a search term has been successfully found (search is case insensitive), next occurrence can be obtained using the “n” key and previous occurrence can be obtained using the “N” key.

    -

    Note: Python 2 users are confined in typing ASCII characters only.

    PyRadio Themes Top

    PyRadio comes with 6 preconfigured (hard coded) themes:

      diff --git a/README.md b/README.md index 0dc7110c..87430f91 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ Ben Dowling - [https://github.com/coderholic](https://github.com/coderholic) * [Controls](#controls) * [Config file](#config-file) * [About Playlist files](#about-playlist-files) +* [Search function](#search-function) * [Moving stations around](#moving-stations-around) * [Specifying stations' encoding](#specifying-stations-encoding) * [Player detection / selection](#player-detection-selection) * [Player default volume level](#player-default-volume-level) -* [Search function](#search-function) * [PyRadio Themes](#pyradio-themes) * [Session Locking](#session-locking) * [Update notification](#update-notification) @@ -209,6 +209,19 @@ When this happens, **PyRadio** will offer you the choice to copy the playlist in If a playlist of the same name already exists in the configuration directory, the "***foreign***" playlist will be time-stamped. For example, if a "***foreign***" playlist is named "*stations.csv*", it will be named "*2019-01-11_13-35-47_stations.csv*" (provided that the action was taken on January 11, 2019 at 13:35:47). + +## Search function + +On any window presenting a list of items (stations, playlists, themes) a **search function** is available by pressing "**/**". + +The *Search Window* supports normal and extend editing and in session history. + +One can always get help by pressing the "**?**" key. + +After a search term has been successfully found (search is case insensitive), next occurrence can be obtained using the "**n**" key and previous occurrence can be obtained using the "**N**" key. + +**Note:** **Python 2** users are confined in typing ASCII characters only. + ## Moving stations around Rearranging the order of the stations in the playlist is another feature **PyRadio** offers. @@ -349,18 +362,6 @@ Example: volstep=1 volume=28 -## Search function - -On any window presenting a list of items (stations, playlists, themes) a **search function** is available by pressing "**/**". - -The *Search Window* supports normal and extend editing and in session history. - -One can always get help by pressing the "**?**" key. - -After a search term has been successfully found (search is case insensitive), next occurrence can be obtained using the "**n**" key and previous occurrence can be obtained using the "**N**" key. - -**Note:** **Python 2** users are confined in typing ASCII characters only. - ## PyRadio Themes **PyRadio** comes with 6 preconfigured (hard coded) themes: @@ -402,7 +403,7 @@ When the *Theme selection window* is visible, a "**[T]**" string displayed at **PyRadio** uses session locking, which actually means that only the first instance executed within a given session will be able to write to the configuration file. -Subsequent instances will be "*locked*". This means that the user can still play stations, load and edit playlists, load and test themes, but any changes will **not** be recorded in the configuration file. +Subsequent instances will be "*locked*". This means that the user can still play stations, load and edit playlists, load and test themes, but any changes will **not** be recorded in the configuration file. ### Session unlocking diff --git a/pyradio.1 b/pyradio.1 index 023e8ae7..6e98f22f 100644 --- a/pyradio.1 +++ b/pyradio.1 @@ -228,6 +228,45 @@ When this happens, \fBpyradio\fR will offer you the choise to copy the playlist If a playlist of the same name already exists in the configuration directory, the "\fIforeign\fR" playlist will be time-stamped. For example, if a "\fIforeign\fR" playlist is named "\fIstations.csv\fR", it will be named "\fI2019-01-11_13-35-47_stations.csv\fR" (provided that the action was taked on January 11, 2019 at 13:35:47). + +.SH SEARCH FUNCTION + +On any window presenting a list of items (stations, playlists, themes) a \fBsearch function\fR is available by pressing "\fI/\fR". + +The \fISearch Window\fR supports normal and extend editing and in session history. + +One can always get help by pressing the "\fI?\fR" key. + +After a search term has been successfully found (search is case insensitive), next occurrence can be obtained using the "\fIn\fR" key and previous occurrence can be obtained using the "\fIN\fR" key. + +.IP \fBNote\fR +\fBPython 2\fR users are confined in typing ASCII characters only. + + + + + +.SH MOVING STATIONS AROUND + +Rearranging the order of the stations in the playlist is another feature PyRadio offers. + +All you have to do is specify the \fIsource\fR station (the station to be moved) and the position it will be moved to (\fItarget\fR). + +There are three way to do that: + +.RS 5 + +.IP 1. +Press \fICtrl-U\fR or \fICtrl-D\fR to move the current station up or down. +.IP 2. +Type a station number and press \fICtrl-U\fR or \fICtrl-D\fR to move the current station there. +.IP 3. +Go to the position you want to move a station to, and press "\fIJ\fR". This will tag this position (making it the target of the move). Then go to the station you want to move and press \fICtrl-U\fR or \fICtrl-D\fR to move it there. + + + + + .SH SPECIFYING STATIONS' ENCODING Normally, stations provide information about their status (including the title of the song playing, which \fBpyradio\fR displays) in Unicode (\fIutf-8\fR encoded). Therefore, \fBpyradio\fR will use \fIutf-8\fR to decode such data, by default. @@ -372,19 +411,6 @@ volstep=1 .br volume=28 -.SH SEARCH FUNCTION - -On any window presenting a list of items (stations, playlists, themes) a \fBsearch function\fR is available by pressing "\fI/\fR". - -The \fISearch Window\fR supports normal and extend editing and in session history. - -One can always get help by pressing the "\fI?\fR" key. - -After a search term has been successfully found (search is case insensitive), next occurrence can be obtained using the "\fIn\fR" key and previous occurrence can be obtained using the "\fIN\fR" key. - -.IP \fBNote\fR -\fBPython 2\fR users are confined in typing ASCII characters only. - .SH PYRADIO THEMES .PP diff --git a/pyradio/__init__.py b/pyradio/__init__.py index 0de297c3..5074021b 100644 --- a/pyradio/__init__.py +++ b/pyradio/__init__.py @@ -1,6 +1,6 @@ " pyradio -- Console radio player. " -version_info = (0, 7, 9) +version_info = (0, 8, 0) __version__ = version = '.'.join(map(str, version_info)) __project__ = __name__ diff --git a/pyradio/edit.py b/pyradio/edit.py index e7e7d668..e98ce912 100644 --- a/pyradio/edit.py +++ b/pyradio/edit.py @@ -251,7 +251,6 @@ def show(self, item=None): self._show_title() - logger.error('DE maxY = {}'.format(self.maxY)) if self.maxY < 22 or self.maxX < 72: txt = ' Window too small to display content ' error_win = curses.newwin(3, len(txt) + 2, int(self.maxY / 2) - 1, int((self.maxX - len(txt)) / 2)) @@ -448,7 +447,11 @@ def keypress(self, char): self.new_station = None ret = -1 else: - if char in ( ord('\t'), 9, curses.KEY_DOWN): + if char in (curses.KEY_EXIT, 27, ord('q')) and \ + self.focus > 1: + self.new_station = None + ret = -1 + elif char in ( ord('\t'), 9, curses.KEY_DOWN): self.focus +=1 elif char == curses.KEY_UP: self.focus -=1 @@ -470,15 +473,9 @@ def keypress(self, char): # cancel self.new_station = None ret = -1 - elif char in (curses.KEY_EXIT, 27): - self.new_station = None - ret = -1 elif char == ord('s') and self._focus > 1: ret = self._return_station() self.focus = abs(ret + 2) - elif char == ord('q') and self._focus > 1: - self.new_station = None - ret = -1 elif char == ord('?') and self.focus <= 1: ret = 2 elif (char in (curses.ascii.DC2, 18) and not self._adding) or \ @@ -490,6 +487,8 @@ def keypress(self, char): else: self._encoding = 'utf-8' self._orig_encoding = self._encoding + for i in range(0,2): + self._line_editor[i]._reset_position = True elif self._focus <= 1: """ Returns: diff --git a/pyradio/radio.py b/pyradio/radio.py index ae1a43b0..82189416 100644 --- a/pyradio/radio.py +++ b/pyradio/radio.py @@ -122,7 +122,7 @@ class PyRadio(object): """ editor class """ _station_editor = None - + _force_exit = False _help_metrics = {} @@ -443,10 +443,6 @@ def _get_redisplay_index(): def refreshBody(self, start=0): self._update_redisplay_list() - #if logger.isEnabledFor(logging.ERROR): - # logger.error('DE {}'.format(self.ws._dq)) - if logger.isEnabledFor(logging.DEBUG): - logger.debug('refreshBody: redisplay windows: {}'.format(self._redisplay_list)) end = len(self._redisplay_list) if end == 0: end = 1 for n in range(start, end): @@ -2306,6 +2302,7 @@ def keypress(self, char): self.startPos += 1 self.ws.close_window() + self._station_editor = None self.refreshBody() elif ret == 2: # display line editor help @@ -2906,7 +2903,6 @@ def keypress(self, char): return if self.ws.operation_mode == self.ws.NORMAL_MODE: - logger.error('DE here') if char in ( ord('a'), ord('A') ): self._station_editor = PyRadioEditor(self.stations, self.selection, self.bodyWin) if char == ord('A'): diff --git a/pyradio/simple_curses_widgets.py b/pyradio/simple_curses_widgets.py index 98c06fd1..a0d8fe21 100644 --- a/pyradio/simple_curses_widgets.py +++ b/pyradio/simple_curses_widgets.py @@ -67,6 +67,14 @@ class SimpleCursesLineEdit(object): log = None _log_file = '' + _reset_position = False + + _word_delim = (' ', '-', '_', '+', '=', + '~', '~', '!', '@', '#', + '$', '%', '^', '&', '*', '(', ')', + '[', ']', '{', '}', '|', '\\', '/', + ) + def __init__(self, parent, width, begin_y, begin_x, **kwargs): self._parent_win = parent @@ -197,9 +205,9 @@ def _calculate_window_metrics(self): else: self._disp_caption = '' width = len(self._disp_caption) + self._max_chars_to_display + 4 - logger.error('DE 0 width = {0}, max_chars_to_display = {1}'.format(width, self._max_chars_to_display)) + #logger.error('DE 0 width = {0}, max_chars_to_display = {1}'.format(width, self._max_chars_to_display)) self._max_chars_to_display = self.width - len(self._disp_caption) - 4 - logger.error('DE 1 width = {0}, max_chars_to_display = {1}'.format(width, self._max_chars_to_display)) + #logger.error('DE 1 width = {0}, max_chars_to_display = {1}'.format(width, self._max_chars_to_display)) if self._boxed: self._height = 3 else: @@ -209,7 +217,7 @@ def _calculate_window_metrics(self): self._max_chars_to_display += 1 if self.log is not None: self.log('string_len = {}'.format(self._max_chars_to_display)) - logger.error('DE 2 width = {0}, max_chars_to_display = {1}'.format(width, self._max_chars_to_display)) + #logger.error('DE 2 width = {0}, max_chars_to_display = {1}'.format(width, self._max_chars_to_display)) return def _prepare_to_show(self): @@ -257,12 +265,19 @@ def refreshEditWindow(self, opening=False): self._edit_win.addstr(0, 0, self._string[self._first:self._first+self._max_chars_to_display], active_edit_color) else: self._curs_pos = 0 + if self._reset_position: + self._reset_position = False + if len(self.string) < self._max_chars_to_display: + self._first = 0 + self._curs_pos = len(self.string) + else: + self._curs_pos = self._max_chars_to_display + self._first = len(self.string) - self._max_chars_to_display if self.log is not None: self.log(' - curs_pos = {}\n'.format(self._curs_pos)) if self.focused: self._edit_win.chgat(0, self._curs_pos, 1, self.cursor_color) - logger.error('curs = {}'.format(self._curs_pos)) - logger.error('DE string length = {}'.format(len(self.string))) + #logger.error('DE string length = {}'.format(len(self.string))) self._edit_win.refresh() @@ -309,6 +324,8 @@ def _delete_char(self): if self._first + self._max_chars_to_display > len(self.string): if self._first > 0: self._first -= 1 + if self._curs_pos < self._max_chars_to_display: + self._curs_pos += 1 def _backspace_char(self): if self._first + self._curs_pos > 0: @@ -323,6 +340,64 @@ def _backspace_char(self): if self._curs_pos > 0: self._curs_pos -= 1 + def _previous_word(self): + if self._first + self._curs_pos > 0: + pos = 0 + str_len = len(self.string) + for n in range(self._first + self._curs_pos - 1, 0, -1): + if self._string[n] in self._word_delim: + if n < self._first + self._curs_pos - 1: + pos = n + break + if pos == 0: + # word_delimiter not found: + self._curs_pos = 0 + self._first =0 + else: + # word delimiter found + if str_len < self._max_chars_to_display or \ + pos >= self._first: + # pos is on screen + #logger.error('DE 1 pos = {0}, first = {1}, curs = {2}, len = {3}, max = {4}'.format(pos, self._first, self._curs_pos, str_len, self._max_chars_to_display)) + self._curs_pos = pos - self._first + 1 + #logger.error('DE 2 pos = {0}, first = {1}, curs = {2}, len = {3}, max = {4}'.format(pos, self._first, self._curs_pos, str_len, self._max_chars_to_display)) + else: + #logger.error('DE 3 pos = {0}, first = {1}, curs = {2}, len = {3}, max = {4}'.format(pos, self._first, self._curs_pos, str_len, self._max_chars_to_display)) + self._first = n + 1 + self._curs_pos = 0 + + def _next_word(self): + pos = len(self._string) + str_len = pos + for n in range(self._first + self._curs_pos + 1, len(self.string)): + if self._string[n] in self._word_delim: + pos = n + break + if pos == str_len: + # word delimiter not found + self._first = str_len - self._max_chars_to_display + if self._first < 0: + self._first = 0 + self._curs_pos = pos - self._first + #logger.error('DE x pos = {0}, first = {1}, curs = {2}, len = {3}, max = {4}'.format(pos, self._first, self._curs_pos, str_len, self._max_chars_to_display)) + else: + # word delimiter found + if str_len < self._max_chars_to_display or \ + pos < self._first + self._max_chars_to_display: + # pos is on screen + self._curs_pos = pos - self._first + 1 + else: + # pos is not on screen + #logger.error('DE 1 pos = {0}, len = {1}, max = {2}'.format(pos, str_len, self._max_chars_to_display)) + self._first = pos + self._curs_pos = 1 + pos = 0 + while str_len - (self._first + pos + 1) < self._max_chars_to_display: + pos -= 1 + self._first = self._first + pos + 1 + self._curs_pos = self._curs_pos + abs(pos) - 1 + #logger.error('DE 2 pos = {0}, len = {1}, max = {2}'.format(pos, str_len, self._max_chars_to_display)) + def keypress(self, win, char): """ returns: @@ -331,11 +406,6 @@ def keypress(self, win, char): 0: exit edit mode, string is valid -1: cancel """ - word_delim = (' ', '-', '_', '+', '=', - '~', '~', '!', '@', '#', - '$', '%', '^', '&', '*', '(', ')', - '[', ']', '{', '}', '|', '\\', '/', - ) #self._log_file='/home/spiros/edit.log' #self._log_file='C:\\Users\\spiros\\edit.log' #self.log = self._log @@ -352,23 +422,14 @@ def keypress(self, win, char): return 1 elif char == 422: """ A-F, move to next word """ - pos = len(self._string) - for n in range(self._curs_pos + 1, len(self._string)): - if self._string[n] in word_delim: - pos = n - break - self._curs_pos = pos - self.refreshEditWindow() + if self.string: + self._next_word() + self.refreshEditWindow() return 1 elif char == 418: """ A-B, move to previous word """ - pos = 0 - for n in range(self._curs_pos - 1, 0, -1): - if self._string[n] in word_delim: - pos = n - break - self._curs_pos = pos - self.refreshEditWindow() + if self.string: + self._previous_word() return 1 if char in (ord('?'), ): @@ -390,7 +451,8 @@ def keypress(self, win, char): """ ESCAPE """ self._string = '' self._curs_pos = 0 - self._input_history.reset_index() + if self._input_history: + self._input_history.reset_index() return -1 else: if self.log is not None: @@ -401,70 +463,35 @@ def keypress(self, win, char): self.string = self._string[:self._first + self._curs_pos] elif char in (ord('f'), ): """ A-F, move to next word """ - pos = len(self._string) - for n in range(self._first + self._curs_pos + 1, len(self._string)): - if self._string[n] in word_delim: - pos = n - break - if pos == len(self._string): - # word delimiter not found - self._curs_pos = pos - self._first - self._first = len(self.string) - self._max_chars_to_display - if self._first < 0: - self._first = 0 - else: - # word delimiter found - if len(self.string) < self._max_chars_to_display or \ - pos < self._first + self._max_chars_to_display: - # pos is on screen - self._curs_pos = pos - self._first + 1 - else: - # pos is not on screen - logger.error('DE 1 pos = {0}, len = {1}, max = {2}'.format(pos, len(self.string), self._max_chars_to_display)) - self._first = pos - self._curs_pos = 1 - pos = 0 - while len(self.string) - (self._first + pos + 1) < self._max_chars_to_display: - pos -= 1 - self._first = self._first + pos + 1 - self._curs_pos = self._curs_pos + abs(pos) - 1 - logger.error('DE 2 pos = {0}, len = {1}, max = {2}'.format(pos, len(self.string), self._max_chars_to_display)) - + if self.string: + self._next_word() elif char in (ord('b'), ): """ A-B, move to previous word """ - pos = 0 - for n in range(self._first + self._curs_pos - 1, 0, -1): - if self._string[n] in word_delim: - pos = n - break - if pos == 0: - self._curs_pos = pos - self._first = 0 - else: - pass + if self.string: + self._previous_word() else: return 1 elif char in (curses.KEY_RIGHT, ): """ KEY_RIGHT """ if self.string: - logger.error('DE max = {0}, curs = {1}, first = {2}'.format(self._max_chars_to_display, self._curs_pos, self._first)) + #logger.error('DE max = {0}, curs = {1}, first = {2}'.format(self._max_chars_to_display, self._curs_pos, self._first)) if len(self.string) < self._max_chars_to_display: self._curs_pos += 1 - logger.error('DE 1 curs increased = {}'.format(self._curs_pos)) + #logger.error('DE 1 curs increased = {}'.format(self._curs_pos)) if self._curs_pos > len(self.string): self._curs_pos = len(self.string) else: if len(self._string) < self._first + self._curs_pos: self._curs_pos = len(self._string) - self._max_chars_to_display - logger.error('DE 2 curs modified = {}'.format(self._curs_pos)) + #logger.error('DE 2 curs modified = {}'.format(self._curs_pos)) else: if self._curs_pos == self._max_chars_to_display: if len(self._string) > self._first + self._curs_pos: self._first += 1 - logger.error('DE 3 first increased = {}'.format(self._first)) + #logger.error('DE 3 first increased = {}'.format(self._first)) else: self._curs_pos += 1 - logger.error('DE 4 curs increased = {}'.format(self._curs_pos)) + #logger.error('DE 4 curs increased = {}'.format(self._curs_pos)) elif char in (curses.KEY_LEFT, ): """ KEY_LEFT """ if self.string: @@ -576,11 +603,6 @@ def keypress(self, win, char): else: if self.log is not None: self.log('====================\n') - #if len(self._string) + 1 == self._max_width: - if len(self._string) == self._max_chars_to_display: - logger.error('DE max width reached {0} - {1}'.format(len(self._string), self._max_chars_to_display)) - #self._first += 1 - # return 1 if version_info < (3, 0): if 32 <= char < 127: # accept only ascii characters @@ -641,7 +663,6 @@ def get_check_next_byte(): else: buf = bytearray(bytes) out = self._decode_string(buf) - logger.error('de out = "{0}", len = {1}'.format(out, len(out))) #out = buf.decode('utf-8') return out