diff --git a/Changelog b/Changelog index 3dcd7e0..0bee129 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,10 @@ +2024-12-04 s-n-g + * version 0.9.3.11.2 - 0.9.3.12-beta2 + * Implementing the Shortcuts Window, making it possible to customize + PyRadio's key bindings + * fixing install.py (#262) + * implementing #252 + 2024-10-17 s-n-g * version 0.9.3.11.1 (BUG FIX) - 0.9.3.12-beta1 * adding -sdd (--show_dirs) command line parameter diff --git a/README.md b/README.md index 018b017..de74b54 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Command line internet radio player. - Station editor (add/edit) with [CJK characters support](#cjk-characters-support) - Configuration editor - Search function + - Customizable key bindings - Easy installation / updating - Runs on Linux, macOS and Windows @@ -65,7 +66,9 @@ In any other case you will have to [build it from source](docs/build.md). **Note:** Please avoid installing **PyRadio** via **pip**. I (user [s-n-g](https://github.com/s-n-g) @ github) am not the creator of this project, nor do I maintain it on [The Python Package Index (PyPI)](https://pypi.org/project/pyradio/). As a result, the version available there is outdated and I cannot provide any support for it. \ \ -Furthermore, please refrain from using any third-party packaging methods, such as **Snap** or **AppImage**. I am not affiliated with these services or projects, and I cannot guarantee the functionality or version of **PyRadio** provided through them. Additionally, I am unable to offer support for any issues related to these packaging methods. +Furthermore, please refrain from using any third-party packaging methods, such as **Snap** or **AppImage**. I am not affiliated with these services or projects, and I cannot guarantee the functionality or version of **PyRadio** provided through them. Additionally, I am unable to offer support for any issues related to these packaging methods. \ +\ +In any of the above cases, if you do ask for help, I will ask you to install your distro package (or build **PyRadio** from source), before attempting to provide any further help. ## Basic usage diff --git a/devel/what_tag b/devel/what_tag index 22bd402..2f85a2c 100755 --- a/devel/what_tag +++ b/devel/what_tag @@ -10,7 +10,7 @@ cYellow="[1;3${colorYellow}m"${cBold} echo -en "pyproject.toml ${cGreen}" grep version pyproject.toml -echo -en "${cReset}__init.py ${cRed}" +echo -en "${cReset}__init__.py ${cRed}" grep '^version_info ' pyradio/__init__.py echo -en "${cReset}install.py ${cYellow}" grep '^PyRadioInstallPyReleaseVersion ' pyradio/install.py @@ -58,4 +58,15 @@ and execute ${cRed}./add-pkgver${cReset} to get AUR package ready for update. " +echo "Running ${cGreen}./devel/get_shortcuts_and_classes.py${cReset}" +python ./devel/get_shortcuts_and_classes.py + +chk=$(LANG=en_US.UTF-8 git st | grep modified) +if [ ! -z "${chk}" ] +then + echo "Not commited files:" + echo "${cRed}${chk}${cReset}" + echo "Please commit them and try again" + exit 1 +fi diff --git a/docs/index.html b/docs/index.html index 1816f0d..206f96c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -187,8 +187,8 @@

Requirements Changelog Top

 
-2024-10-17 s-n-g
-    * version 0.9.3.11.2 (BUG FIX) - 0.9.3.12-beta2
+2024-12-04 s-n-g
+    * version 0.9.3.11.2 - 0.9.3.12-beta2
     * Implementing the Shortcuts Window, making it possible to customize
       PyRadio's key bindings
     * fixing install.py (#262)
diff --git a/docs/index.md b/docs/index.md
index 90b95d1..80cd6c5 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -13,6 +13,7 @@ Command line internet radio player.
 * [Command line options](#command-line-options)
 * [Controls](#controls)
     * [Global shortcuts](#global-shortcuts)
+    * [Customizing key bindings](#customizing-key-bindings)
 * [HTML help](#html-help)
 * [PyRadio Modes](#pyradio-modes)
     * [Secondary Modes](#secondary-modes)
@@ -71,6 +72,7 @@ Command line internet radio player.
 * [Update notification](#update-notification)
 * [Remote Control Server](#remote-control-server)
     * [Remote Control Client](#remote-control-client)
+* [Playing a station in the terminal](#playing-a-station-in-the-terminal)
 * [Debug mode](#debug-mode)
 * [Reporting bugs](#reporting-bugs)
 * [Packaging PyRadio](#packaging-pyradio)
@@ -90,6 +92,7 @@ Command line internet radio player.
  - Station editor (add/edit) with [CJK characters support](#cjk-characters-support)
  - Configuration editor
  - Search function
+ - Customizable key bindings
  - Easy installation / updating
  - Runs on Linux, macOS and Windows
 
@@ -126,9 +129,9 @@ Furthermore, please refrain from using any third-party packaging methods, such a
 
 ```
 # pyradio -h
-Usage: pyradio [-h] [-c CONFIG_DIR] [-p [STATION_NUMBER]] [-u PLAYER] [-l]
-               [-lt] [-sds] [-sd] [-od] [-pc] [-d] [-ul] [-us] [-U] [-R] [-V]
-               [-ls] [-s PLAYLIST] [-tlp] [-t THEME] [--show-themes]
+Usage: pyradio [-h] [-c CONFIG_DIR] [-p [STATION_NUMBER]] [-x] [-u PLAYER]
+               [-l] [-lt] [-sds] [-sd] [-od] [-pc] [-d] [-ul] [-us] [-U] [-R]
+               [-V] [-ls] [-s PLAYLIST] [-tlp] [-t THEME] [--show-themes]
                [--no-themes] [--write-theme IN_THEME OUT_THEME,]
                [--terminal TERMINAL] [--terminal-param TERMINAL_PARAM] [-oc]
                [-sc] [-cc] [-gc] [-r] [-or] [-lr] [-mkv MKV_FILE]
@@ -146,6 +149,9 @@ General options:
   -p [STATION_NUMBER], --play [STATION_NUMBER]
                         Start and play.The value is num station or empty for
                         random.
+  -x, --external-player
+                        Play station in external player. Can be combined with
+                        --play.
   -u PLAYER, --use-player PLAYER
                         Use specified player. A comma-separated list can be
                         used to specify detection order. Supported players:
@@ -247,6 +253,9 @@ Headless operation:
 
 ## Controls
 
+The following list shows a **default** key bindings list used within the program.
+
+
                       Main window                                      Playlists window                   Themes window
     -------------------------------------------------------------------------------------------------------------------------------------
     Up/Down/j/k/
@@ -316,6 +325,12 @@ When focus is on a "*Line editor*", all shortcuts will work when preceded by a "
 
 [2] Function not available in the **RadioBrowser** Search window.
 
+### Customizing key bindings
+
+**PyRadio** provides the possibility to customize the key bindings above.
+
+Just open the configuration window and navigate to **Keyboard Shortcuts**. Please do read the help screen provided therein (press "*?" to get to it).
+
 ## HTML help
 
 While in **PyRadio** main window, one can open the HTML (offline) help using "**\\h**".
@@ -1171,6 +1186,18 @@ If you'd like to set up a "headless" **PyRadio** operation for your linux box, p
 
 For more information, please refer to [the relevant page](client.md).
 
+## Playing a station in the terminal
+
+A user request [Shortcut to quit pyradio and launch standalone player (e.g. mpv) with currently selected station](https://github.com/coderholic/pyradio/issues/252) lead to the possibility to use any player in the terminal.
+
+This action will be triggered by pressing "*X*".
+
+After the player stops, **PyRadio** will stop as well.
+
+**Note:** On Windows, **mpv** and **VLC** will open a new player window instead of executing in the terminal.
+
+In addition, a command line parameter has been added "*-x*" ("*--exteranl-player*") which when used in conjuction with the "*-p*" ("*--play*") command line parameter, will instruct **PyRadio** to play a station and terminate after the playback stops.
+
 ## Debug mode
 
 Adding the "**-d**" option to the command line will instruct **PyRadio** to enter *Debug mode*, which means that it will print debug messages to a file. This file will always reside in the user's home directory and will be named *pyradio.log*.
diff --git a/docs/pyradio.1 b/docs/pyradio.1
index 22ca62c..9c77012 100644
--- a/docs/pyradio.1
+++ b/docs/pyradio.1
@@ -2,7 +2,7 @@
 .\" Copyright (C) 2018-2024 Spiros Georgaras 
 .\" This manual is freely distributable under the terms of the GPL.
 .\"
-.TH pyradio 1 "June 2024" pyradio
+.TH pyradio 1 "December 2024" pyradio
 
 .SH Name
 .PP
@@ -1950,6 +1950,16 @@ If so, a notification message will be displayed, informing the user about it and
 
 \fBpyradio\fR will uninstall all previously installed versions when updated (using the \fB-U\fR command line parameter), so no extra steps are needed any more to house keep your system.
 
+.SH Playing a station in the terminal
+
+A user request \fBShortcut to quit pyradio and launch standalone player (e.g. mpv) with currently selected station\fR (\fIhttps://github.com/coderholic/pyradio/issues/252\fR) lead to the possibility to use any player in the terminal.
+
+This action will be triggered by pressing "\fIX\fR".
+
+After the player stops, \fBpyradio\fR will stop as well.
+
+In addition, a command line parameter has been added "\fI-x\fR" ("\fI--exteranl-player\fR") which when used in conjuction with the "\fI-p\fR" ("\fI--play\fR") command line parameter, will instruct \fBpyradio\fR to play a station and terminate after the playback stops.
+
 .SH Debug Mode
 .PP
 Adding the \fB-d\fR option to the command line will instruct \fBpyradio\fR to enter \fBDebug mode\fR, which means that it will print debug messages to a file. This file will always reside in the user's home directory and will be named \fIpyradio.log\fR.
diff --git a/pyproject.toml b/pyproject.toml
index 8998c9c..661bb48 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "pyradio"
-version = "0.9.3.11.1"
+version = "0.9.3.11.2"
 authors = [
   { name="Ben Dowling", email="ben.m.dowling@gmail.com" },
   { name="Spiros Georgaras", email="sng@hellug.gr" },
diff --git a/pyradio/__init__.py b/pyradio/__init__.py
index abbda89..f266746 100644
--- a/pyradio/__init__.py
+++ b/pyradio/__init__.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 " pyradio -- Console radio player. "
 
-version_info = (0, 9, 3, 11, 1)
+version_info = (0, 9, 3, 11, 2)
 
 # Set it to True if new stations have been
 # added to the package's stations.csv
diff --git a/pyradio/config_window.py b/pyradio/config_window.py
index d7c4c63..f9ed3bb 100644
--- a/pyradio/config_window.py
+++ b/pyradio/config_window.py
@@ -3514,17 +3514,12 @@ def _rename_keyboard_json_file(file_path):
         return new_file_name
 
     def _start_editing(self):
+        self.existing_conflict = None
         self._win.addstr(self._selection - self._start + 2, self.maxX-8, '[edit]', curses.color_pair(6))
         self._win.refresh()
         self._editing = True
         if logger.isEnabledFor(logging.DEBUG):
             logger.debug('editing "{}"'.format(self._list[self._selection]))
-        for i in range(self._selection, -1, -1):
-            if self._list[i][1] is None:
-                self._in_group = i
-                break
-        if logger.isEnabledFor(logging.DEBUG):
-            logger.debug(f'editing in group "{self._list[self._in_group][0]}"')
 
     def _stop_editing(self):
         self._win.addstr(self._selection - self._start + 2, self.maxX-8, '      ', curses.color_pair(6))
@@ -3760,7 +3755,7 @@ def show(self, parent=None):
             self._b_ok, self._b_cancel = self._widget.buttons
             self._b_ok.focused = self._b_cancel.focused = False
 
-        self._win.addstr(1, 2, 'Shortcuts', curses.color_pair(12))
+        self._win.addstr(1, 2, 'Actions', curses.color_pair(12))
         self._win.addstr(1, self._max_length-3, 'Default    User    New', curses.color_pair(12))
 
         for i in range(0, self._number_of_lines):
@@ -3921,6 +3916,7 @@ def _get_available_keys(self):
 
         # Separate characters into categories
         digits = sorted([char for char in available_characters if char.isdigit()])  # Sort digits
+        digits.pop(0)
         letters = sorted([char for char in available_characters if char.isalpha()])  # Sort letters
         function_keys_list = sorted([char for char in available_characters if char.startswith('F')])  # Sort function keys
         if 'F' in function_keys_list:
@@ -3974,6 +3970,7 @@ def keypress(self, char):
             logger.error(f'{the_key = }')
 
             logger.error('\n\n============')
+            self._old_key_value = (self._list[self._selection][3], self._list[self._selection][6])
             self._list[self._selection][3] = char
             self._list[self._selection][6] = ctrl_code_to_string(char)
             ret = self._validate_key()
@@ -3994,8 +3991,8 @@ def keypress(self, char):
                 ''' disable editing '''
                 self._stop_editing()
             else:
-                self._list[self._selection][3] = self._list[self._selection][2]
-                self._list[self._selection][6] = self._list[self._selection][5]
+                # restore previous values on conflict
+                self._list[self._selection][3], self._list[self._selection][6] = self._old_key_value
 
             return ret
         else:
@@ -4008,6 +4005,16 @@ def keypress(self, char):
                 self.keys_string = msg + self._get_available_keys() + '\n\n'
                 return -4
 
+            elif char == kbkey['revert_def']:
+                for i in range(len(self._list)):
+                    self._list[i][3] = self._list[i][1]
+                    self._list[i][6] = self._list[i][4]
+                self._needs_update = True
+            elif char == kbkey['revert_saved']:
+                for i in range(len(self._list)):
+                    self._list[i][3] = self._list[i][2]
+                    self._list[i][6] = self._list[i][5]
+                self._needs_update = True
             elif char == ord('x'):
                 self._list[self._selection][3] = self._list[self._selection][2]
                 self._list[self._selection][6] = self._list[self._selection][5]
@@ -4030,8 +4037,8 @@ def keypress(self, char):
                 self._go_down(step=5)
             elif char == curses.KEY_PPAGE:
                 self._go_up(step=5)
-            elif char in (curses.KEY_EXIT, 27, kbkey['q']):
-                return -1
+            # elif char in (curses.KEY_EXIT, 27, kbkey['q']):
+            #    return -1
             elif char in (curses.KEY_RIGHT, kbkey['l']):
                 if self._focus > 0:
                     self._focus_next()
diff --git a/pyradio/install.py b/pyradio/install.py
index c43cbc2..70da147 100644
--- a/pyradio/install.py
+++ b/pyradio/install.py
@@ -17,7 +17,7 @@
 ''' This is PyRadio version this
     install.py was released for
 '''
-PyRadioInstallPyReleaseVersion = '0.9.3.11.1'
+PyRadioInstallPyReleaseVersion = '0.9.3.11.2'
 
 locale.setlocale(locale.LC_ALL, "")
 
diff --git a/pyradio/keyboard.py b/pyradio/keyboard.py
index fd3a470..15247ef 100644
--- a/pyradio/keyboard.py
+++ b/pyradio/keyboard.py
@@ -436,6 +436,8 @@ def is_valid_char(char, win):
     Returns:
         bool: True if c is valid, False otherwise.
     """
+    if char in (9, ord('\t')):
+        return False
     # if char <= 127:
     if (65 <= char <= 90) or (97 <= char <= 122) or (1 <= char <= 47):
         ''' 1 byte '''
@@ -456,6 +458,9 @@ def is_valid_char(char, win):
     elif char in (
         ord('='), ord('.'), ord('+'),
         ord('`'), ord('-'),
+        ord('1'), ord('2'), ord('3'),
+        ord('4'), ord('5'), ord('6'),
+        ord('7'), ord('8'), ord('9'),
         curses.KEY_F1,
         curses.KEY_F2,
         curses.KEY_F3,
diff --git a/pyradio/keyboard/classes.json b/pyradio/keyboard/classes.json
index abdd89c..bc63160 100644
--- a/pyradio/keyboard/classes.json
+++ b/pyradio/keyboard/classes.json
@@ -1 +1 @@
-{"SimpleCursesString": ["pause", "l", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesDate": ["t", "q", "tab", "l", "?", "stab", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesTime": ["t", "q", "tab", "l", "pause", "?", "stab", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesCounter": ["q", "l", "?", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesWidgetColumns": ["g", "q", "j", "l", "k", "pause", "G", "?", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesMenu": ["edit", "g", "goto_playing", "q", "add", "j", "l", "k", "pause", "G", "?", "del", "screen_middle", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesCheckBox": ["pause", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesPushButton": ["pause", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesLineEdit": ["tab", "paste", "?", "stab", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesBoolean": ["q", "l", "pause", "?", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SelectPlayer": ["q", "j", "l", "k", "s", "pause", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadio": ["F8", "https", "repaint", "g", "rb_vote", "open_enc", "extra_p_pamars", "G", "F9", "F7", "open_online", "jump", "gr_prev", "search", "random", "revert_def", "edit", "open_extra", "add_to_reg", "t", "q", "add", "rec", "search_next", "open_config", "s", "pause", "rb_sort", "transp", "search_prev", "ext_player", "append", "screen_top", "Y", "info", "goto_playing", "gr", "st_dn", "no_show", "paste", "n", "revert_saved", "gr_next", "F10", "st_up", "Reload", "fav", "rb_info", "reload", "j", "y", "k", "rb_server", "l", "?", "screen_bottom", "screen_middle", "del", "open_playlist", "open_regs", "h", "hist_prev", "hist_next", "p_prev", "p_next", "rb_p_first", "rb_p_next", "rb_p_prev", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioThemeSelector": ["edit", "g", "add", "reload", "j", "l", "k", "q", "s", "pause", "G", "watch_theme", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "RadioBrowserConfigWindow": ["revert_def", "q", "tab", "j", "l", "k", "s", "pause", "?", "stab", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "RadioBrowserSearchWindow": ["g", "tab", "G", "rb_h_add", "stab", "rb_h_next", "q", "s", "pause", "rb_h_def", "rb_h_save", "rb_h_0", "j", "l", "k", "?", "rb_h_prev", "h", "rb_h_del", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "RadioBrowserSort": ["g", "q", "j", "l", "k", "pause", "G", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "RadioBrowserServersSelect": ["q", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "RadioBrowserServers": ["g", "q", "j", "l", "k", "pause", "G", "?", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "RadioBrowserTermNavigator": ["g", "next", "q", "l", "prev", "pause", "G", "?", "del", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioMessagesSystem": ["G", "j", "k", "g", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioSimpleScheduleWindow": ["info", "q", "tab", "j", "l", "k", "pause", "?", "stab", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioConfigWindow": ["pause", "l", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "ExtraParametersEditor": ["q", "tab", "s", "?", "stab", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "ExtraParameters": ["q", "l", "s", "pause", "?", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioSelectPlayer": ["tab", "q", "j", "l", "k", "s", "pause", "revert_saved", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioSelectEncodings": ["revert_def", "g", "q", "j", "l", "k", "s", "G", "pause", "revert_saved", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioSelectPlaylist": ["g", "q", "j", "l", "k", "pause", "G", "screen_middle", "revert_saved", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioSelectStation": ["revert_saved", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioKeyboardConfig": ["g", "q", "tab", "j", "l", "k", "G", "pause", "?", "stab", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioEditor": ["q", "tab", "s", "stab", "revert_saved", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioRecordingDir": ["q", "tab", "l", "s", "pause", "?", "stab", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioResourceOpener": ["revert_def", "q", "tab", "s", "?", "stab", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioRenameFile": ["q", "tab", "l", "s", "pause", "stab", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioBuffering": ["no_buffer", "q", "j", "k", "s", "revert_saved", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioConnectionType": ["q", "j", "l", "k", "s", "pause", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioServerWindow": ["revert_def", "j", "l", "k", "s", "pause", "revert_saved", "h", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "InfoWindow": ["info_rename", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "ExtraKeys": ["last_playlist", "open_buffer", "clear_all_reg", "clear_reg", "buffer", "open_dirs", "open_remote_control", "new_playlist", "hist_top", "unnamed", "html_help", "rename_playlist", "change_player"]}
\ No newline at end of file
+{"SelectPlayer": ["q", "h", "k", "pause", "s", "l", "j", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadio": ["screen_middle", "goto_playing", "open_config", "rb_sort", "open_enc", "pause", "rb_info", "extra_p_pamars", "F9", "repaint", "s", "j", "t", "transp", "add", "info", "n", "y", "G", "k", "?", "no_show", "open_regs", "screen_top", "screen_bottom", "revert_saved", "jump", "st_up", "gr", "append", "g", "search_next", "st_dn", "add_to_reg", "gr_next", "F10", "random", "open_online", "F7", "rec", "paste", "del", "l", "reload", "fav", "open_playlist", "search", "Reload", "rb_server", "ext_player", "q", "h", "gr_prev", "Y", "F8", "edit", "search_prev", "rb_vote", "revert_def", "https", "open_extra", "hist_prev", "hist_next", "p_prev", "p_next", "rb_p_first", "rb_p_next", "rb_p_prev", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioConfigWindow": ["h", "l", "pause", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "ExtraParametersEditor": ["tab", "q", "?", "stab", "s", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "ExtraParameters": ["q", "h", "?", "pause", "s", "l", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioSelectPlayer": ["tab", "q", "h", "k", "pause", "revert_saved", "s", "l", "j", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioSelectEncodings": ["G", "h", "q", "k", "pause", "revert_saved", "s", "l", "revert_def", "j", "g", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioSelectPlaylist": ["screen_middle", "q", "h", "G", "k", "pause", "revert_saved", "l", "j", "g", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioSelectStation": ["revert_saved", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioKeyboardConfig": ["tab", "G", "q", "h", "k", "?", "pause", "revert_saved", "stab", "revert_def", "l", "j", "g", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioEditor": ["tab", "q", "revert_saved", "stab", "s", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioRecordingDir": ["tab", "q", "?", "pause", "stab", "s", "l", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioResourceOpener": ["tab", "q", "?", "stab", "s", "revert_def", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioRenameFile": ["tab", "q", "pause", "stab", "s", "l", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioBuffering": ["q", "h", "k", "revert_saved", "s", "j", "no_buffer", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioConnectionType": ["q", "h", "k", "pause", "s", "l", "j", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioServerWindow": ["h", "k", "pause", "revert_saved", "s", "revert_def", "l", "j", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioSimpleScheduleWindow": ["tab", "info", "q", "h", "k", "?", "pause", "stab", "l", "j", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioThemeSelector": ["G", "q", "h", "k", "watch_theme", "edit", "pause", "s", "l", "reload", "j", "g", "add", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesString": ["l", "pause", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesDate": ["tab", "q", "h", "?", "stab", "l", "t", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesTime": ["tab", "q", "h", "?", "pause", "stab", "l", "t", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesCounter": ["q", "h", "?", "l", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesWidgetColumns": ["q", "G", "h", "k", "?", "pause", "l", "j", "g", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesMenu": ["screen_middle", "goto_playing", "q", "h", "G", "k", "?", "edit", "pause", "del", "l", "j", "g", "add", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesCheckBox": ["pause", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesPushButton": ["pause", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesLineEdit": ["tab", "stab", "paste", "?", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "SimpleCursesBoolean": ["q", "h", "?", "pause", "l", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "RadioBrowserConfigWindow": ["tab", "q", "k", "?", "pause", "stab", "s", "revert_def", "l", "j", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "RadioBrowserSearchWindow": ["pause", "rb_h_def", "s", "j", "tab", "G", "k", "rb_h_add", "?", "stab", "rb_h_next", "rb_h_prev", "g", "rb_h_save", "l", "rb_h_del", "q", "h", "rb_h_0", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "RadioBrowserSort": ["q", "h", "G", "k", "pause", "l", "j", "g", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "RadioBrowserServersSelect": ["q", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "RadioBrowserServers": ["q", "h", "G", "k", "?", "pause", "l", "j", "g", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "RadioBrowserTermNavigator": ["prev", "q", "h", "G", "?", "pause", "next", "del", "l", "g", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "PyRadioMessagesSystem": ["G", "k", "j", "g", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "InfoWindow": ["info_rename", "tag", "t_tag", "transp", "v_up1", "v_up2", "v_up3", "v_dn1", "v_dn2", "mute", "s_vol", "t_calc_col", "repaint"], "ExtraKeys": ["open_dirs", "buffer", "open_buffer", "hist_top", "clear_all_reg", "html_help", "unnamed", "change_player", "new_playlist", "open_remote_control", "last_playlist", "rename_playlist", "clear_reg"]}
\ No newline at end of file
diff --git a/pyradio/messages_system.py b/pyradio/messages_system.py
index d172e81..47a72c2 100644
--- a/pyradio/messages_system.py
+++ b/pyradio/messages_system.py
@@ -1467,37 +1467,65 @@ def set_text(self, parent, *args):
 )),
 
     'M_KEYBOARD_HELP':('Keyboard Shortcuts Help',
-kb2str(r'''
-This is the |Keyboard Shortcuts| configuration window help.
-
-The window will display a list of the existing shortcuts:
-The first column will display the action the shortcut
-corresponds to, the second column (|Default|) will print
-the default (hardcoded) shortcut (press |{revert_def}| to activate
-them), the third column (|User|) will print the shortcut
-the user has already set for the action (press |{revert_saved}| to
-activate them), and the last column (|New|) will print the
-latest changes, which have not been saved yet.
-
-Use |Arrow Keys|, |{j}|, |{k}|, |PgUp| and |PgDown| to move, |{g}| and |{G}| to
-go to the beginning or the end of the list, or |[| and |]| to
-navigate the shortcut groups.
-
-To change a |Keyboard Shortcut|, select a shortcut, press
-|Right|, |{l}|, |Enter|, or |{pause}|. Then a "|[edit]|" will appear
-at the right of the line. Press any key to change the
-shortcut, or |Esc| to cancel the operation.
-
-When in navigation mode (not editing a shortcut), press
-"|x|" to remove a |New| shortcut previously inserted. Press
-"|f|" to see a list of "|free|" keys that you can use.
-
-After you have finished customizing the shortcuts, press
-|OK| (use |Tab| or |{tab}| to navigate). Then |PyRadio| will try to
-detect any conflicts, and either help you to resolve
-them or save and activate your new shortcuts.
+r'''
+The |Keyboard Shortcuts| window will display a list of shortcuts in
+four columns:
+__|Actions|  Available actions.
+__|Default|  The default key for the item.
+__|User|___  User custom key for the item (saved value).
+__|New|____  Latest change (not saved yet).
+
+The following action are available:
+''' + self._format_columns(kb2str(
+r'''
+__|Arrow Keys|, |{j}|, |{k}|,            |*|
+__|PgUp|, |PgDown|                       |*| Move up, down, etc.
+__|Tab|, |{tab}| / |Sh-Tab|, |{stab}|    |*| Move to next / previous field.
+__|{g}| / |{G}|                          |*| Go to top / bottom of the list.
+__|[| / |]|                              |*| Move between sections.
+__|{revert_def}| / |{revert_saved}|      |*| Revert to default / saved shortcuts.
+__|x|                                    |*| Revert current item to saved value.
+__|f|                                    |*| Show |free| keys.
+__|Enter|, |{pause}|,                    |*|
+__|Right|, |{l}|                         |*| Enter |editing mode| (insert new shortcut).
+__|Esc|                                  |*|Exit |editing mode|.
+__|0|                                    |*| Switch between |c|ocnflicting items.
+                                         |*|Available in |editing mode| as well.
+__|{h}|                                  |*|Display this help screen.
+''')) + r'''
+
+To change a |Keyboard Shortcut|, just enter the |editing mode|. This will
+be indicated by a "|[edit]|" appearing at the right of the line. Press
+any key to change the shortcut, or |Esc| to cancel the operation.
+
+After you have finished customizing the shortcuts, navigate to the |OK|
+button to save and activate your new shortcuts.
+
+Keep in mind that this is the only window in |PyRadio| that will not be
+closed when "|Esc|" is pressed; you will have to navigate to the |Cancel|
+button and press it, instead.
+
+|Important Notice on Shortcut Customization
+
+As you customize your shortcuts, please be aware that adding a new
+shortcut triggers a |validation| procedure.
+
+The system is designed to be |context-aware|; if the new key you choose
+is already in use, it will check whether it conflicts within the same
+context. In such cases, an error message will be displayed, and the
+change will be rejected.
+
+However, we recognize that there may be instances where conflicting
+keys go undetected by the system. We kindly ask you to keep an eye
+out for any such conflicts. If you encounter a situation where a
+shortcut |seems to be causing issues| without triggering a validation
+error, please |report| this incident to us, at this URL:
+
+____|https://github.com/coderholic/pyradio/issues
+
+Thank you for your cooperation.
 '''
-)),
+),
 
     'M_INVALID_KEY_ERROR':('Invalid Key',
 r'''
@@ -1971,6 +1999,60 @@ def erase(self):
             self._win.erase()
             self._win.refresh()
 
+    def _format_columns(self, help_text):
+        # Step 1: Use the original help text
+        # help_text = self.help_text
+
+        # Step 2: Split lines and process each line
+        lines = help_text.strip().split('\n')
+        formatted_lines = []
+        max_left_length = 0
+
+        for line in lines:
+            # Split by '|*|' and strip whitespace
+            parts = [part.strip() for part in line.split('|*|')]
+            if len(parts) != 2:
+                continue  # Skip lines that don't conform to expected format
+
+            left_part = parts[0].strip()  # Keep '|' in left part
+            right_part = parts[1].strip()  # Right part remains as is
+
+            # Calculate lengths
+            left_length_no_pipe = len(left_part.replace('|', '').strip())
+            left_length_with_pipe = len(left_part.strip())
+
+            # Update max length of left part without pipes
+            max_left_length = max(max_left_length, left_length_no_pipe)
+
+            # Prepare formatted line with original left part
+            formatted_lines.append((left_part, right_part))
+
+        # Total width (max_left_length + 4)
+        total_width = max_left_length + 4
+
+        # Step 3: Create formatted output with proper spacing
+        output_lines = []
+        for left_part, right_part in formatted_lines:
+            # Calculate lengths again for formatting
+            left_length_no_pipe = len(left_part.replace('|', '').strip())
+            left_length_with_pipe = len(left_part.strip())
+
+            # Calculate number of spaces needed for padding after removing '|'
+            padding_spaces = total_width - left_length_no_pipe
+
+            if left_part == "":
+                # If left_part is empty, pad right_part with underscores
+                right_part = '_' * total_width + right_part
+
+            # Create a formatted line with calculated padding
+            formatted_line = left_part.ljust(len(left_part) + padding_spaces) + right_part
+            output_lines.append(formatted_line)
+
+        return '\n'.join(output_lines)
+
+    def get_formatted_help(self):
+        return self._format_columns()
+
     def show(self, parent=None):
         if logger.isEnabledFor(logging.INFO):
             logger.info('>>> Message System: displaying key "{}"'.format(self._last_key))
diff --git a/pyradio/win.py b/pyradio/win.py
index 506f5da..bb9590d 100644
--- a/pyradio/win.py
+++ b/pyradio/win.py
@@ -38,7 +38,7 @@
         https://sourceforge.net/projects/mpv-player-windows/files/latest/download
 '''
 zurl = [
-    'https://sourceforge.com/projects/mpv-player-windows/files/64bit/mpv-x86_64-20240825-git-cb4fdb5.7z/download',
+    'https://sourceforge.com/projects/mpv-player-windows/files/64bit/mpv-x86_64-20241124-git-2d813de.7z/download',
     'https://sourceforge.net/projects/mplayerwin/files/MPlayer-MEncoder/r38151/mplayer-svn-38151-x86_64.7z/download'
 ]