Skip to content

Commit 316561b

Browse files
committed
Adding CJK support
1 parent e460967 commit 316561b

File tree

6 files changed

+128
-51
lines changed

6 files changed

+128
-51
lines changed

Changelog

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
2019-10-18 s-n-g
2+
* CJK Unified Ideographs supported by the line edittor
3+
* On python 2, trying to edit a station whose name contains
4+
non-ASCII characters is prohibited and will end up in
5+
displaying a relevant message
6+
* Search term will not be lost when resizing the window
7+
* Fixing issues with presenting search history
8+
19
2019-09-08 s-n-g
210
* Adding station editor ("a" and "A" to add a station, "e" to edit)
311
* Line editor supports unlimited string length

README.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ <h2 id="table-of-contents">Table of contents <span style="padding-left: 10px;"><
3737
<li><a href="#config-file">Config file</a></li>
3838
<li><a href="#about-playlist-files">About Playlist files</a></li>
3939
<li><a href="#search-function">Search function</a></li>
40+
<li><a href="#line-editor">Line editor</a></li>
4041
<li><a href="#moving-stations-around">Moving stations around</a></li>
4142
<li><a href="#specifying-stations-encoding">Specifying stations’ encoding</a></li>
4243
<li><a href="#player-detection-selection">Player detection / selection</a></li>
@@ -184,6 +185,16 @@ <h2 id="search-function">Search function <span style="padding-left: 10px;"><sup
184185
<p>One can always get help by pressing the “<strong>?</strong>” key.</p>
185186
<p>After a search term has been successfully found (search is case insensitive), next occurrence can be obtained using the “<strong>n</strong>” key and previous occurrence can be obtained using the “<strong>N</strong>” key.</p>
186187
<p style="margin: 1.5em 4em 0 4em; text-indent: -2.5em;"><strong>Note:</strong> <strong>Python 2</strong> users are confined in typing ASCII characters only.</p>
188+
<h2 id="line-editor">Line editor <span style="padding-left: 10px;"><sup style="font-size: 50%"><a href="#" title="Go to top of the page">Top</a></sup></style></h2>
189+
<p><strong>PyRadio</strong><em>Search function</em>” and “<em>Station edior</em>” use a <em>line editor</em> to permit typing and editing stations’ data.</p>
190+
<p>The <em>line editor</em> works both on <strong>Python 2</strong> and <strong>Python 3</strong>, but does not provide the same functionality for both versions:</p>
191+
<ul>
192+
<li>In <strong>Python 2</strong>, only ASCII characters can be inserted.</li>
193+
<li>In <strong>Python 3</strong>, no such restriction exists. Furthermore, using CJK characters is also supported.</li>
194+
</ul>
195+
<h3 id="cjk-characters-support">CJK characters support</h3>
196+
<p>The <em>line editor</em> supports the insertion of <a target="_blank" href="https://en.wikipedia.org/wiki/CJK_Unified_Ideographs">CJK Unified Ideographs</a>, as described on <a target="_blank" href="https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block)">CJK Unified Ideographs (Unicode block)</a> also known as URO, abbreviation of Unified Repertoire and Ordering. These characters, although encoded as a single code-point (character), actually take up a 2-character space, when rendered on the terminal.</p>
197+
<p>A depiction of the editor’s behavior can be seen at this image: <a target="_blank" href="https://members.hellug.gr/sng/pyradio/pyradio-editor.jpg">pyradio-editor.jpg</a>.</p>
187198
<h2 id="moving-stations-around">Moving stations around <span style="padding-left: 10px;"><sup style="font-size: 50%"><a href="#" title="Go to top of the page">Top</a></sup></style></h2>
188199
<p>Rearranging the order of the stations in the playlist is another feature <strong>PyRadio</strong> offers.</p>
189200
<p>All you have to do is specify the <em>source</em> station (the station to be moved) and the position it will be moved to (<em>target</em>).</p>

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Ben Dowling - [https://github.com/coderholic](https://github.com/coderholic)
1313
* [Config file](#config-file)
1414
* [About Playlist files](#about-playlist-files)
1515
* [Search function](#search-function)
16+
* [Line editor](#line-editor)
1617
* [Moving stations around](#moving-stations-around)
1718
* [Specifying stations' encoding](#specifying-stations-encoding)
1819
* [Player detection / selection](#player-detection-selection)
@@ -223,6 +224,22 @@ After a search term has been successfully found (search is case insensitive), ne
223224

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

227+
## Line editor
228+
229+
**PyRadio** "*Search function*" and "*Station edior*" use a *line editor* to permit typing and editing stations' data.
230+
231+
The *line editor* works both on **Python 2** and **Python 3**, but does not provide the same functionality for both versions:
232+
233+
234+
* In **Python 2**, only ASCII characters can be inserted.
235+
* In **Python 3**, no such restriction exists. Furthermore, using CJK characters is also supported.
236+
237+
### CJK characters support
238+
239+
The *line editor* supports the insertion of [CJK Unified Ideographs](https://en.wikipedia.org/wiki/CJK_Unified_Ideographs), as described on [CJK Unified Ideographs (Unicode block)](https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block)) also known as URO, abbreviation of Unified Repertoire and Ordering. These characters, although encoded as a single code-point (character), actually take up a 2-character space, when rendered on the terminal.
240+
241+
A depiction of the editor's behavior can be seen at this image: [pyradio-editor.jpg](https://members.hellug.gr/sng/pyradio/pyradio-editor.jpg).
242+
226243
## Moving stations around
227244

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

pyradio.1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,17 @@ After a search term has been successfully found (search is case insensitive), ne
243243
\fBPython 2\fR users are confined in typing ASCII characters only.
244244

245245

246+
.SH CJK CHARACTERS SUPPORT
246247

248+
The \fIline editor\fR supports the insertion of \fICJK Unified Ideographs [1]\fR, as described on \fICJK Unified Ideographs (Unicode block) [2]\fR, also known as URO, abbreviation of Unified Repertoire and Ordering. These characters, although encoded as a single code-point (character), actually take up a 2-character space, when rendered on the terminal.
249+
250+
A depiction of the editor's behavior can be seen at this image:
251+
252+
\fIhttps://members.hellug.gr/sng/pyradio/pyradio-editor.jpg\fR
253+
254+
[1] \fIhttps://en.wikipedia.org/wiki/CJK_Unified_Ideographs\fR
255+
256+
[2] \fIhttps://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block)\fR
247257

248258

249259
.SH MOVING STATIONS AROUND

pyradio/edit.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ def __init__(self, parent, width, begin_y, begin_x, **kwargs):
3232
self._range_command='range'
3333

3434
def show(self, parent_win, repaint=False):
35+
if repaint:
36+
tmp = self.string
3537
if parent_win is not None:
3638
self.parent_win = parent_win
3739

@@ -51,25 +53,30 @@ def show(self, parent_win, repaint=False):
5153
self._caption_win.addstr(0, x-1, '┤'.encode('utf-8'), self.box_color)
5254
except:
5355
pass
54-
if not repaint:
56+
if repaint:
57+
self.string = tmp
58+
self.keep_restore_data()
59+
self._caption_win.refresh()
60+
self.refreshEditWindow(opening=True)
61+
else:
5562
self.string = self._displayed_string = ''
56-
self._curs_pos = self._disp_curs_pos = 0
63+
self._first = self._curs_pos = self._disp_curs_pos = 0
5764
self._edit_win.erase()
5865
self._edit_win.chgat(0, 0, 1, self.cursor_color)
5966
if self._has_history:
6067
self._input_history.reset_index()
61-
self._caption_win.refresh()
62-
self._edit_win.refresh()
68+
self._caption_win.refresh()
69+
self._edit_win.refresh()
6370

6471
def _get_history_next(self):
6572
""" callback function for key down """
6673
if self._has_history:
67-
self.string = self._input_history.return_history(1)
74+
self.string = self._input_history.return_history(1, self.string)
6875

6976
def _get_history_previous(self):
7077
""" callback function for key up """
7178
if self._has_history:
72-
self.string = self._input_history.return_history(-1)
79+
self.string = self._input_history.return_history(-1, self.string)
7380

7481
def get_next(self, a_list, start=0, stop=None):
7582
if self.string:
@@ -485,7 +492,7 @@ def keypress(self, char):
485492
self._encoding = 'utf-8'
486493
self._orig_encoding = self._encoding
487494
for i in range(0,2):
488-
self._line_editor[i]._reset_position = True
495+
self._line_editor[i]._go_to_end()
489496
elif self._focus <= 1:
490497
"""
491498
Returns:

pyradio/simple_curses_widgets.py

Lines changed: 68 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,22 @@ def string(self, val):
212212
self._go_to_end()
213213

214214
def _is_cjk(self):
215+
""" Check if string contains CJK characters.
216+
If string is empty reset history index """
215217
old_cjk = self._cjk
216218
if len(self.string) == cjklen(self.string):
217219
self._cjk = False
218220
else:
219221
self._cjk = True
222+
if self.string == '' and self._has_history:
223+
self._input_history.reset_index()
220224
if logger.isEnabledFor(logging.DEBUG) and self._cjk != old_cjk:
221-
logger.debug('CJK editing is {}'.format('ON' if self._cjk else 'OFF'))
225+
logger.debug('=== CJK editing is {} ==='.format('ON' if self._cjk else 'OFF'))
226+
227+
def keep_restore_data(self):
228+
""" Keep a copy of current editor state
229+
so that it can be restored later. """
230+
self._restore_data = [ self.string, self._displayed_string, self._curs_pos, self._disp_curs_pos, self._first ]
222231

223232
def getmaxyx(self):
224233
return self._caption_win.getmaxyx()
@@ -320,9 +329,7 @@ def refreshEditWindow(self, opening=False):
320329

321330
if self.focused:
322331
if logger.isEnabledFor(logging.DEBUG):
323-
logger.debug('refreshEditWindow: first={0}, curs={1}, dcurs={2}, max={3}'.format(self._first, self._curs_pos, self._disp_curs_pos, self._max_chars_to_display))
324-
logger.debug('refreshEditWindow: full string: "{}"'.format(self.string))
325-
logger.debug('refreshEditWindow: displayed string: "{}"'.format(self._displayed_string))
332+
logger.debug('refreshEditWindow:\n first={0}, curs={1}, dcurs={2}, max={3}\n len={4}, cjklen={5}\n string="{6}"\n len={7}, cjklen={8}\n disstr="{9}"'.format(self._first, self._curs_pos, self._disp_curs_pos, self._max_chars_to_display, len(self.string), cjklen(self.string), self.string, len(self._displayed_string), cjklen(self._displayed_string), self._displayed_string))
326333
self._edit_win.chgat(0, self._disp_curs_pos, 1, self.cursor_color)
327334

328335
self._edit_win.refresh()
@@ -498,52 +505,60 @@ def _previous_word(self):
498505
break
499506
if pos == 0:
500507
# word_delimiter not found:
501-
self._curs_pos = 0
502-
self._first =0
508+
self._go_to_start()
509+
return
503510
else:
504511
# word delimiter found
505512
if str_len < self._max_chars_to_display or \
506513
pos >= self._first:
507514
# pos is on screen
508-
#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))
509515
self._curs_pos = pos - self._first + 1
510-
#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))
511516
else:
512-
#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))
513517
self._first = n + 1
514518
self._curs_pos = 0
519+
self._displayed_string = self.string[self._first:self._first+self._max_chars_to_display]
520+
self._disp_curs_pos = cjklen(self._displayed_string[:self._curs_pos])
521+
while cjklen(self._displayed_string) > self._max_chars_to_display:
522+
self._displayed_string = self._displayed_string[:-1]
515523

516524
def _next_word(self):
517-
pos = cjklen(self._string)
518-
str_len = pos
525+
if self._at_end_of_sting():
526+
return
527+
if self._first + self._curs_pos + 1 >= len(self.string):
528+
self._go_to_end()
529+
return
530+
pos = 0
519531
for n in range(self._first + self._curs_pos + 1, len(self.string)):
520532
if self._string[n] in self._word_delim:
521533
pos = n
522534
break
523-
if pos == str_len:
524-
# word delimiter not found
525-
self._first = str_len - self._max_chars_to_display
526-
if self._first < 0:
527-
self._first = 0
528-
self._curs_pos = pos - self._first
529-
#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))
530-
else:
531-
# word delimiter found
532-
if str_len < self._max_chars_to_display or \
533-
pos < self._first + self._max_chars_to_display:
535+
if pos >= len(self.string):
536+
pos = 0
537+
if pos > 0:
538+
if pos < len(self._displayed_string):
534539
# pos is on screen
535-
self._curs_pos = pos - self._first + 1
540+
self._curs_pos = pos + 1 - self._first
541+
self._disp_curs_pos = cjklen(self._displayed_string[:self._curs_pos])
536542
else:
537-
# pos is not on screen
538-
#logger.error('DE 1 pos = {0}, len = {1}, max = {2}'.format(pos, str_len, self._max_chars_to_display))
539-
self._first = pos
540-
self._curs_pos = 1
541-
pos = 0
542-
while str_len - (self._first + pos + 1) < self._max_chars_to_display:
543-
pos -= 1
544-
self._first = self._first + pos + 1
545-
self._curs_pos = self._curs_pos + abs(pos) - 1
546-
#logger.error('DE 2 pos = {0}, len = {1}, max = {2}'.format(pos, str_len, self._max_chars_to_display))
543+
if pos < self._first + len(self._displayed_string):
544+
# pos is on middle and on screen
545+
self._curs_pos = pos - self._first + 1
546+
self._disp_curs_pos = cjklen(self._displayed_string[:self._curs_pos])
547+
else:
548+
# pos is off screen
549+
self._first = 0
550+
self._curs_pos = pos + 2
551+
self._displayed_string = tmp = self.string[:self._curs_pos]
552+
while cjklen(tmp) > self._max_chars_to_display:
553+
self._first += 1
554+
tmp = self._displayed_string[self._first:]
555+
self._displayed_string = tmp
556+
self._curs_pos = len(self._displayed_string) - 1
557+
self._disp_curs_pos = cjklen(self._displayed_string[:-1])
558+
559+
else:
560+
# word delimiter not found
561+
self._go_to_end()
547562

548563
def _go_right(self):
549564
if self.string and not self._at_end_of_sting():
@@ -606,13 +621,14 @@ def _go_left(self):
606621
while cjklen(self._displayed_string) > self._max_chars_to_display:
607622
self._displayed_string = self._displayed_string[:-1]
608623
else:
624+
logger.error('simple')
609625
self._go_left_simple()
610626

611627
def _go_left_simple(self):
612628
if len(self.string) < self._max_chars_to_display:
613-
self._curs_pos -= 1
614-
if self._curs_pos < 0:
615-
self._curs_pos = 0
629+
self._curs_pos -= 1
630+
if self._curs_pos < 0:
631+
self._curs_pos = 0
616632
else:
617633
if self._curs_pos == 0:
618634
self._first -= 1
@@ -680,10 +696,9 @@ def keypress(self, win, char):
680696
# display help
681697
if logger.isEnabledFor(logging.DEBUG):
682698
logger.debug('action: help')
683-
self._restore_data = [ self.string, self._displayed_string, self._curs_pos, self._disp_curs_pos, self._first ]
699+
self.keep_restore_data()
684700
return 2
685701

686-
687702
elif char in (curses.KEY_ENTER, ord('\n'), ord('\r')):
688703
""" ENTER """
689704
if logger.isEnabledFor(logging.DEBUG):
@@ -745,7 +760,7 @@ def keypress(self, win, char):
745760
if self.string:
746761
if logger.isEnabledFor(logging.DEBUG):
747762
logger.debug('action: LEFT')
748-
self._go_left()
763+
self._go_left()
749764

750765
elif char in (curses.KEY_HOME, curses.ascii.SOH):
751766
""" KEY_HOME, ^A """
@@ -880,6 +895,8 @@ def keypress(self, win, char):
880895
else:
881896
self._string = self._string[:self._first + self._curs_pos] + chr(char) + self._string[self._first + self._curs_pos:]
882897
self._add_to_end = False
898+
self._curs_pos+=1
899+
self._disp_curs_pos = self._curs_pos
883900
self._displayed_string=self.string[self._first:self._first+self._max_chars_to_display]
884901
else:
885902
if platform.startswith('win'):
@@ -894,6 +911,7 @@ def keypress(self, win, char):
894911
self._displayed_string=self._string[self._first:self._first+self._curs_pos]
895912
else:
896913
self._string = self._string[:self._first + self._curs_pos] + char + self._string[self._first + self._curs_pos:]
914+
self._curs_pos+=1
897915
self._add_to_end = False
898916
self._displayed_string=self.string[self._first:self._first+self._max_chars_to_display]
899917
if self._add_to_end:
@@ -910,6 +928,10 @@ def keypress(self, win, char):
910928
# adding to middle of string
911929
while cjklen(self._displayed_string) > self._max_chars_to_display:
912930
self._displayed_string=self._displayed_string[:-1]
931+
if self._cjk:
932+
self._disp_curs_pos = cjklen(self._displayed_string[:self._curs_pos])
933+
else:
934+
self._disp_curs_pos=self._curs_pos
913935

914936
self.refreshEditWindow()
915937
return 1
@@ -954,7 +976,7 @@ def get_check_next_byte():
954976
if is_wide(out) and not self._cjk:
955977
self._cjk = True
956978
if logger.isEnabledFor(logging.DEBUG):
957-
logger.debug('CJK editing is ON')
979+
logger.debug('=== CJK editing is ON ===')
958980
return out
959981

960982
def _encode_string(self, data):
@@ -1004,8 +1026,8 @@ def run(self):
10041026
class SimpleCursesLineEditHistory(object):
10051027

10061028
def __init__(self):
1007-
self._history = []
1008-
self._active_history_index = -1
1029+
self._history = [ '' ]
1030+
self._active_history_index = 0
10091031

10101032
def add_to_history(self, a_string):
10111033
if a_string:
@@ -1021,17 +1043,19 @@ def add_to_history(self, a_string):
10211043
self._history.append(a_string)
10221044
self._active_history_index = len(self._history)
10231045

1024-
def return_history(self, direction):
1046+
def return_history(self, direction, current_string):
10251047
if self._history:
10261048
self._active_history_index += direction
10271049
if self._active_history_index <= -1:
10281050
self._active_history_index = len(self._history) - 1
10291051
elif self._active_history_index >= len(self._history):
10301052
self._active_history_index = 0
10311053
ret = self._history[self._active_history_index]
1054+
if ret.lower() == current_string.lower():
1055+
return self.return_history(direction, current_string)
10321056
return ret
10331057
return ''
10341058

10351059
def reset_index(self):
1036-
self._active_history_index = -1
1060+
self._active_history_index = 0
10371061

0 commit comments

Comments
 (0)