├── .gitignore ├── fman-plugin-statusbarextended-v0.4.0.png ├── fman-plugin-statusbarextended-select-v0.4.0.png ├── fman-plugin-statusbarextended-alignment-v0.4.0.png ├── Key Bindings.json ├── byteconverter └── __init__.py ├── toggleplugin └── __init__.py ├── LICENSE ├── README.md ├── toggleselection └── __init__.py ├── Changelog.md ├── statusbarextended └── __init__.py └── statusbarextended_config └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | README.html 2 | Changelog.html -------------------------------------------------------------------------------- /fman-plugin-statusbarextended-v0.4.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kek91/StatusBarExtended/HEAD/fman-plugin-statusbarextended-v0.4.0.png -------------------------------------------------------------------------------- /fman-plugin-statusbarextended-select-v0.4.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kek91/StatusBarExtended/HEAD/fman-plugin-statusbarextended-select-v0.4.0.png -------------------------------------------------------------------------------- /fman-plugin-statusbarextended-alignment-v0.4.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kek91/StatusBarExtended/HEAD/fman-plugin-statusbarextended-alignment-v0.4.0.png -------------------------------------------------------------------------------- /Key Bindings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": [ "F3"], "command": "toggle_status_bar_extended" }, 3 | { "keys": ["Shift+F3"], "command": "configure_status_bar_extended" } 4 | ] -------------------------------------------------------------------------------- /byteconverter/__init__.py: -------------------------------------------------------------------------------- 1 | # Utility class for converting bytes 2 | 3 | import statusbarextended_config as SBEcfg 4 | 5 | class ByteConverter(): 6 | def __init__(self, n): 7 | self.bytes = n 8 | cfg = SBEcfg.SingletonConfig() 9 | cfgCurrent, exit_status = cfg.loadConfig() 10 | if cfgCurrent is None: 11 | self.sizeDivisor = cfg.Default['SizeDivisor'] 12 | else: 13 | self.sizeDivisor = cfgCurrent['SizeDivisor'] 14 | def calc(self): 15 | for x in ['b', 'k', 'M', 'G', 'T']: 16 | if self.bytes < self.sizeDivisor: 17 | return "%3.1f %s" % (self.bytes, x) if x!='b' else "%5.0f" % (self.bytes) 18 | self.bytes /= self.sizeDivisor 19 | -------------------------------------------------------------------------------- /toggleplugin/__init__.py: -------------------------------------------------------------------------------- 1 | # Class for enabling/disabling StatusBarExtended 2 | 3 | from fman import DirectoryPaneCommand, show_status_message 4 | import statusbarextended as SBE 5 | import statusbarextended_config as SBEcfg 6 | 7 | class ToggleStatusBarExtended(DirectoryPaneCommand): 8 | def __call__(self): 9 | cfg = SBEcfg.SingletonConfig() 10 | cfgCurrent, exit_status = cfg.loadConfig() 11 | if cfgCurrent is None: 12 | return 13 | if cfgCurrent["Enabled"] == True: 14 | cfgCurrent["Enabled"] = False 15 | cfg.saveConfig(cfgCurrent) 16 | show_status_message("Disabled StatusBarExtended", 1) 17 | else: 18 | cfgCurrent["Enabled"] = True 19 | cfg.saveConfig(cfgCurrent) 20 | SBE.StatusBarExtended.refresh(self, cfgCurrent) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Teknix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StatusBarExtended 2 | 3 | Extends the status bar in fman to show additional information. 4 | 5 | Turn the plugin on or off by using a keyboard shortcut, default is F3. 6 | 7 | Configure the plugin by using a keyboard shortcut, default is ShiftF3. 8 | 9 | 10 | 11 | **Features** 12 | 13 | Adds extra information to the status bar. 14 | 15 | - Show the number of directories/files and the total size of files in the current directory for both panes 16 | - Show "Toggle hidden files" status (`◻` shown `◼` hidden) 17 | - Show the number of selected directories/files and the total size of selected files 18 | - Show the currently active pane indicator (`◧` left `◨` right) 19 | 20 | Aligns indicator positions to avoid "jitter" on selection/navigation 21 | 22 | Allows a user to configure all the options via the `configure_status_bar_extended` command aliased as `StatusBarExtended: configure` in the Command Palette: 23 | 24 | | Option | Default | Description | 25 | | :------------- | :--------: | :----------------------------------------- | 26 | | Enabled | `True` | Enable or disable this plugin | 27 | | SizeDivisor | `1024` | File size format: decimal (1k=1000=10³) or binary (1k=1024=2¹⁰) | 28 | | MaxGlob | `5000` | Skip folders with as many items (folders+files) | 29 | | SymbolPane | `◧` `◨` | `Left`/`Right` pane symbol | 30 | | SymbolHiddenF | `◻` `◼` | Hidden files `Shown`/`Hidden` symbol (__tip__: try `👁` `👀👓` `✓✗` `◎◉` `🐵🙈`) | 31 | | HideDotfile | `False` | Treat .dotfiles as hidden files on Windows | 32 | | Justify | `5` `5` `7` | Minimum width of the `Folder`/`File`/`Size` values, e.g.
5,321
   21 | 33 | 34 | 35 | **Preview** 36 | 37 | | Status Bar without selection | Status Bar with selection | 38 | | :--------------------------------------: | :--------------------------------------: | 39 | | ![Screenshot macOS 10 v0.3.0](fman-plugin-statusbarextended-v0.4.0.png) | ![Screenshot macOS 10 v0.3.0-selection](fman-plugin-statusbarextended-select-v0.4.0.png) | 40 | 41 | | Status Bar alignment | 42 | | :------------------------------: | 43 | | ![Screenshot of alignment](fman-plugin-statusbarextended-alignment-v0.4.0.png) | 44 | 45 | __Known issues__ 46 | 47 | - fman raises `ValueError` on the first `Toggle hidden files` if a pane is _launched_ with hidden files _hidden_ (and status bar is not updated this one time) (__tip__: you might be able to conveniently close the error warning window with the same keybind you toggled hidden files with) 48 | - Status bar is NOT updated when _switching panes_ with a _mouse_ since plugins can't notice a pane switch due to a lack of the [necessary APIs](https://github.com/fman-users/fman/issues/292#issuecomment-360036718) 49 | - Alignment of indicators only works for _monospaced_ (fixed-width) fonts since it's currently implemented using regular spaces (__tip__: you can change this font in your `Theme.css` file `.statusbar{font-family:"yourMonospacedFont"}`). And even then fancy icons/emojis might slightly break it 50 | - On launch the right pane is ignored in the status bar udpate to improve performance since fman always activates the left one (and doesn't have an API to let a plugin know which pane is the active one) 51 | -------------------------------------------------------------------------------- /toggleselection/__init__.py: -------------------------------------------------------------------------------- 1 | # Override commands that toggle item selection to automatically compute and instantly display 2 | # combined filesize for selected files and the number of selected folders/files 3 | 4 | from fman import DirectoryPaneCommand, DirectoryPaneListener, load_json, save_json, PLATFORM 5 | from core.commands.util import is_hidden 6 | from fman.url import splitscheme 7 | import statusbarextended as SBE 8 | import statusbarextended_config as SBEcfg 9 | 10 | class _CorePaneCommand(DirectoryPaneCommand): # copy from core/commands/__init__.py 11 | def select_all(self): 12 | self.pane.select_all() 13 | def deselect( self): 14 | self.pane.clear_selection() 15 | 16 | def move_cursor_down( self, toggle_selection=False): 17 | self.pane.move_cursor_down( toggle_selection) 18 | def move_cursor_up( self, toggle_selection=False): 19 | self.pane.move_cursor_up( toggle_selection) 20 | def move_cursor_page_up( self, toggle_selection=False): 21 | self.pane.move_cursor_page_up( toggle_selection) 22 | def move_cursor_page_down(self, toggle_selection=False): 23 | self.pane.move_cursor_page_down(toggle_selection) 24 | def move_cursor_home( self, toggle_selection=False): 25 | self.pane.move_cursor_home( toggle_selection) 26 | def move_cursor_end( self, toggle_selection=False): 27 | self.pane.move_cursor_end( toggle_selection) 28 | 29 | def toggle_hidden_files(self): 30 | _toggle_hidden_files(self.pane, not _is_showing_hidden_files(self.pane)) 31 | def _toggle_hidden_files(pane, value): 32 | if value: 33 | pane._remove_filter(_hidden_file_filter) 34 | else: 35 | pane._add_filter(_hidden_file_filter) 36 | _get_pane_info(pane)['show_hidden_files'] = value 37 | save_json('Panes.json') 38 | def _is_showing_hidden_files(pane): 39 | return _get_pane_info(pane)['show_hidden_files'] 40 | def _get_pane_info(pane): 41 | settings = load_json('Panes.json', default=[]) 42 | default = {'show_hidden_files': False} 43 | pane_index = pane.window.get_panes().index(pane) 44 | for _ in range(pane_index - len(settings) + 1): 45 | settings.append(default.copy()) 46 | return settings[pane_index] 47 | def _hidden_file_filter(url): 48 | if PLATFORM == 'Mac' and url == 'file:///Volumes': 49 | return True 50 | scheme, path = splitscheme(url) 51 | return scheme != 'file://' or not is_hidden(path) 52 | 53 | def _get_opposite_pane(pane): 54 | panes = pane.window.get_panes() 55 | return panes[(panes.index(pane) + 1) % len(panes)] 56 | 57 | 58 | 59 | class CommandEmpty(DirectoryPaneCommand): # to avoid duplicate command execution (and "return '', args" hangs) 60 | def __call__(self): 61 | pass 62 | 63 | class SelectionOverride(DirectoryPaneListener): 64 | def on_command(self, command_name, args): 65 | if command_name in ('switch_panes'): 66 | pane_cur = self.pane 67 | pane_opp = _get_opposite_pane(self.pane) 68 | pane_opp.focus() # doesn't change self.pane, so... 69 | self.pane=pane_opp # ...need to do it manually... 70 | self.show_selected_files() 71 | self.pane=pane_cur # ...and restore after the statusbar update 72 | return 'command_empty', {} 73 | elif command_name in ( 74 | 'select_all', 'deselect', 75 | 'toggle_hidden_files'): 76 | getattr(_CorePaneCommand, command_name)(self) 77 | self.show_selected_files() 78 | return 'command_empty', {} 79 | elif command_name in ( # commands that can pass a 'toggle_selection' argument 80 | 'move_cursor_down' , 'move_cursor_up' , 81 | 'move_cursor_page_down', 'move_cursor_page_up', 82 | 'move_cursor_home' , 'move_cursor_end'): 83 | getattr(_CorePaneCommand, command_name)(self, args) 84 | if 'toggle_selection' in args: 85 | if args['toggle_selection'] == True: 86 | self.show_selected_files() 87 | return 'command_empty', {} 88 | 89 | def show_selected_files(self): 90 | cfg = SBEcfg.SingletonConfig() 91 | cfgCurrent, exit_status = cfg.loadConfig() 92 | if cfgCurrent is None: 93 | return 94 | if cfgCurrent["Enabled"] == True: 95 | SBE.StatusBarExtended.show_selected_files(self, cfgCurrent) 96 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file 3 | 4 | ## [Unreleased] 5 | [ Unreleased]: https://github.com/kek91/StatusBarExtended/compare/v0.4.0...HEAD 6 | 7 | ## [v0.4.0 — 13.08.2021] 8 | [ v0.4.0 — 13.08.2021]: https://github.com/kek91/StatusBarExtended/releases/tag/v0.4.0 9 | - __Added__ 10 | + :sparkles: User-configurable options via the `configure_status_bar_extended` command aliased as `StatusBarExtended: configure` in the Command Palette and bound to ShiftF3 by default 11 | 12 | | Option | Default | Description | 13 | | :------------- | :--------: | :----------------------------------------- | 14 | | Enabled | `True` | Enable/Disable this plugin | 15 | | SizeDivisor | `1024` | File size format: decimal (1k=1000=10³) or binary (1k=1024=2¹⁰) | 16 | | MaxGlob | `5000` | Skip folders with as many items (folders+files) | 17 | | SymbolPane | `◧` `◨` | `Left`/`Right` pane symbol | 18 | | SymbolHiddenF | `◻` `◼` | Hidden files `Shown`/`Hidden` symbol | 19 | | HideDotfile | `False` | Treat .dotfiles as hidden files on Windows | 20 | | Justify | `5` `5` `7` | Minimum width of the `Folder`/`File`/`Size` values | 21 | 22 | + :sparkles: A command to view current configuration (`view_configuration_status_bar_extended` aliased as `StatusBarExtended: view current configuration settings` in Command Palette) 23 | 24 | - __Fixed__ 25 | + :beetle: selection updating on each cursor move even when without a `toggle_selection` argument 26 | + :beetle: update function called 3 times per path change (one extra for each `from statusbarextended import StatusBarExtended`) 27 | + :beetle: each pane calling the status bar update instead of only the left one (currently also the active one) 28 | 29 | ## [v0.3.1 — 07.08.2021] 30 | [ v0.3.1 — 07.08.2021]: https://github.com/kek91/StatusBarExtended/releases/tag/v0.3.1 31 | - __Fixed__ 32 | + :beetle: Hidden non-dotfiles always counted; (Windows) dotfiles (even without the 'hidden' attribute) not counted if option is set to hide 33 | + :beetle: Status bar is NOT updated when a _visible_ hidden file/folder is _selected_ and then _hidden_ via `Toggle hidden files` 34 | 35 | ## [v0.3.0 — 07.08.2021] 36 | [ v0.3.0 — 07.08.2021]: https://github.com/kek91/StatusBarExtended/releases/tag/v0.3.0 37 | - __Fixed__ 38 | + :beetle: Hidden files not counted even when they're shown 39 | + :beetle: Not updating on pane changes via keyboard `switch_panes` command (e.g. with a Tab) 40 | 41 | ## [v0.2.1 — 05.08.2021] 42 | [ v0.2.1 — 05.08.2021]: https://github.com/kek91/StatusBarExtended/releases/tag/v0.2.1 43 | - __Changed__ 44 | + The currently active pane indicator from `Pane: Left`/`Pane: Right` to `◧`/`◨` 45 | + The `Dirs:`/`Files:` indicators' to align with and without selection 46 | 47 | ## [v0.2.0 — 05.08.2021] 48 | [ v0.2.0 — 05.08.2021]: https://github.com/kek91/StatusBarExtended/releases/tag/v0.2.0 49 | - __Fixed__ 50 | + :beetle: Not working with the latest fman version (`1.7.3`) and its new file system API ([blog](https://fman.io/blog/fmans-new-file-system-api/), [API](https://fman.io/docs/api#FileSystem)) 51 | 52 | ## [v0.1.2 — 15.08.2017] 53 | [ v0.1.2 — 15.08.2017]: https://github.com/kek91/StatusBarExtended/releases/tag/v0.1.2 54 | - Shorter size indicators with lower-case for (kilo)bytes: `b, k, M, G, T`. Kibibyte (`2^10`) format is preserved 55 | - Change the status icon of hidden files toggle to `◻`white (hidden files shown) and `◼`black (hidden files hidden) Unicode square symbols 56 | - Align all indicator position to keep it the same regardless of the length of the indicator (file/folder count is consistent up to `9,999`) 57 | - Remove empty folder/file numbers indicators (including labels) 58 | ![Toolbar only screenshot with a custom theme v0.1.1f](fman-plugin-StatusBarExtendedF.png) 59 | - Add thousands separator (`,`) to file/folder numbers (e.g. `Files: 1,000`) 60 | - Change status of selected items to be consistent with the regular view for faster read 61 | 62 | ## [v0.1.1 — 06.04.2017] 63 | [ v0.1.1 — 06.04.2017]: https://github.com/kek91/StatusBarExtended/releases/tag/v0.1.1 64 | - Show binary prefix instead of decimal for file size 65 | 66 | ## [v0.1.0 — 01.03.2017] 67 | [ v0.1.0 — 01.03.2017]: https://github.com/kek91/StatusBarExtended/releases/tag/v0.1.0 68 | - Shows output only for the currently active pane due to layout/resize issues 69 | - Don't display "Files: n" and "Size: n xB" if there are 0 files in current directory 70 | - Hidden status is visualized by checkmark or cross 71 | 72 | 73 | **25.02.2017:** 74 | 75 | - Align text to the left and right for the respective panes 76 | - Reduce text to make the status bar smaller thus allowing resizing window smaller 77 | 78 | 79 | **19.01.2017:** 80 | 81 | - Shows selected directores and files/filesize when selecting files 82 | - Restructured code for readability 83 | 84 | 85 | **16.01.2017:** 86 | 87 | - StatusBar shows immediately when toggled 88 | 89 | 90 | **28.11.2016:** 91 | 92 | - Cleaned code 93 | - Should work in all 3 supported OS (tested on Windows and Linux only) 94 | -------------------------------------------------------------------------------- /statusbarextended/__init__.py: -------------------------------------------------------------------------------- 1 | # Main class for StatusBarExtended 2 | 3 | from fman import DirectoryPaneCommand, DirectoryPaneListener, \ 4 | show_status_message, load_json, PLATFORM 5 | from fman.url import as_url, as_human_readable as as_path 6 | from fman.fs import is_dir, query 7 | from core.commands.util import is_hidden # works on file_paths, not urls 8 | import glob 9 | from byteconverter import ByteConverter 10 | #from PyQt5.QtWidgets import QApplication 11 | import statusbarextended_config as SBEcfg 12 | 13 | 14 | class StatusBarExtended(DirectoryPaneListener): 15 | def __init__(self, *args, **kwargs): 16 | super().__init__(*args, **kwargs) 17 | self.is_first_path_change = True 18 | 19 | def refresh(self, cfg): 20 | 21 | panes = self.pane.window.get_panes() 22 | pane_id = panes.index(self.pane) 23 | statusbar_pane = "" 24 | 25 | cfg_show_hidden_files = load_json('Panes.json')[pane_id]['show_hidden_files'] 26 | pane_show_hidden_files = cfg['SymbolHiddenF'][0] if cfg_show_hidden_files else\ 27 | cfg['SymbolHiddenF'][1] 28 | cur_dir_url = self.pane.get_path() 29 | current_dir = as_path(cur_dir_url) 30 | dir_folders = 0 31 | dir_files = 0 32 | dir_filesize = 0 33 | dir_files_in_dir = glob.glob(current_dir + "/*") 34 | if PLATFORM == 'Windows' and not cfg['HideDotfile']: 35 | # .dotfiles=regular (always shown unless have a 'hidden' attr) 36 | dir_files_in_dir += glob.glob(current_dir + "/.*") 37 | elif cfg_show_hidden_files: # .dotfile=hidden (internal option shows) 38 | dir_files_in_dir += glob.glob(current_dir + "/.*") 39 | f_url = "" 40 | aboveMax = False 41 | 42 | if not dir_files_in_dir: 43 | pass 44 | elif len(dir_files_in_dir) > cfg['MaxGlob']: 45 | aboveMax = True 46 | else: 47 | if cfg_show_hidden_files: 48 | for f in dir_files_in_dir: 49 | f_url = as_url(f) 50 | if is_dir(f_url): 51 | dir_folders += 1 52 | else: 53 | dir_files += 1 54 | try: 55 | dir_filesize += query(f_url, 'size_bytes') 56 | except Exception as e: 57 | continue 58 | else: 59 | for f in dir_files_in_dir: 60 | f_url = as_url(f) 61 | if not is_hidden(f): 62 | if is_dir(f_url): 63 | dir_folders += 1 64 | else: 65 | dir_files += 1 66 | try: 67 | dir_filesize += query(f_url, 'size_bytes') 68 | except Exception as e: 69 | continue 70 | 71 | bc = ByteConverter(dir_filesize) 72 | bcc = str(bc.calc()) 73 | maxG = str("{0:,}".format(cfg['MaxGlob'])) 74 | jFd = cfg['Justify']['folder'] 75 | jFl = cfg['Justify']['file'] 76 | jSz = cfg['Justify']['size'] 77 | dir_foldK = str("{0:,}".format(dir_folders)) # to ','→' ' add .replace(',', ' ') 78 | dir_fileK = str("{0:,}".format(dir_files)) 79 | if(self.pane == panes[0]): 80 | statusbar_pane += cfg['SymbolPane'][0] 81 | else: 82 | statusbar_pane += cfg['SymbolPane'][1] 83 | statusbar_pane += " " + pane_show_hidden_files + " " 84 | if dir_folders > 0: 85 | statusbar_pane += "Dirs: " + dir_foldK.rjust(jFd, ' ') + " " 86 | if dir_folders <= 9999: 87 | statusbar_pane += " " 88 | elif aboveMax: 89 | statusbar_pane += "Dirs " + '+ '.rjust(jFd, ' ') + " " 90 | else: 91 | statusbar_pane += " " + ''.rjust(jFd, ' ') + " " 92 | if dir_files > 0: 93 | statusbar_pane += "Files: " + dir_fileK.rjust(jFl, ' ') + " " 94 | if dir_files <= 9999: 95 | statusbar_pane += " " 96 | elif aboveMax: 97 | statusbar_pane += "Files >" + maxG.rjust(jFl, ' ') + " " 98 | else: 99 | statusbar_pane += " " + ''.rjust(jFl, ' ') + " " 100 | if not aboveMax: 101 | statusbar_pane += " Size: "+ bcc.rjust(jSz, ' ') + " " 102 | # to align with "∑ Size: " 103 | 104 | show_status_message(statusbar_pane, 5000) 105 | 106 | def show_selected_files(self, cfg): 107 | panes = self.pane.window.get_panes() 108 | pane_id = panes.index(self.pane) 109 | cfg_show_hidden_files = load_json('Panes.json')[pane_id]['show_hidden_files'] 110 | selected = self.pane.get_selected_files() 111 | dir_folders = 0 112 | dir_files = 0 113 | dir_filesize = 0 114 | 115 | if selected: 116 | if cfg_show_hidden_files: 117 | for f in selected: 118 | if is_dir(f): 119 | dir_folders += 1 120 | else: 121 | dir_files += 1 122 | dir_filesize += query(f, 'size_bytes') 123 | else: 124 | for f in selected: 125 | if not is_hidden(as_path(f)): 126 | if is_dir(f): 127 | dir_folders += 1 128 | else: 129 | dir_files += 1 130 | dir_filesize += query(f, 'size_bytes') 131 | 132 | bc = ByteConverter(dir_filesize) 133 | bcc = str(bc.calc()) 134 | jFd = cfg['Justify']['folder'] 135 | jFl = cfg['Justify']['file'] 136 | jSz = cfg['Justify']['size'] 137 | dir_foldK = "{0:,}".format(dir_folders) 138 | dir_fileK = "{0:,}".format(dir_files) 139 | statusbar = "Selected* " 140 | if dir_folders > 0: 141 | statusbar += "Dirs: " + dir_foldK.rjust(jFd, ' ') + " " 142 | if dir_folders <= 9999: 143 | statusbar += " " 144 | else: 145 | statusbar += " " + ''.rjust(jFd, ' ') + " " 146 | if dir_files > 0: 147 | statusbar += "Files: " + dir_fileK.rjust(jFl, ' ') + " " 148 | if dir_files <= 9999: 149 | statusbar += " " 150 | else: 151 | statusbar += " " + ''.rjust(jFl, ' ') + " " 152 | statusbar += "∑ Size: " + bcc.rjust(jSz, ' ') + " " 153 | show_status_message(statusbar) 154 | 155 | else: 156 | StatusBarExtended.refresh(self, cfg) 157 | 158 | 159 | def on_path_changed(self): 160 | if self.pane.get_path()=='null://': # ignore strange paths on launch 161 | return 162 | panes = self.pane.window.get_panes() 163 | if self.is_first_path_change: # ignore the right pane on start 164 | self.is_first_path_change = False 165 | if panes.index(self.pane) == 1: 166 | return 167 | cfg = SBEcfg.SingletonConfig() 168 | cfgCurrent, exit_status = cfg.loadConfig() 169 | 170 | if cfgCurrent is None: 171 | return 172 | if cfgCurrent["Enabled"] == True: 173 | StatusBarExtended.refresh(self, cfgCurrent) 174 | -------------------------------------------------------------------------------- /statusbarextended_config/__init__.py: -------------------------------------------------------------------------------- 1 | # Class for configuring StatusBarExtended 2 | 3 | from fman import ApplicationCommand, \ 4 | show_prompt, show_alert, show_status_message, load_json, save_json, \ 5 | PLATFORM, DATA_DIRECTORY, run_application_command 6 | from fman.fs import exists, delete, move_to_trash 7 | from fman.url import as_human_readable as as_path, as_url, join, splitscheme, basename, dirname 8 | from collections import OrderedDict as odict 9 | 10 | class SingletonConfig(object): 11 | def __new__(cls): 12 | """Create the global config singleton object or return the existing one.""" 13 | if not hasattr(cls, 'instance'): 14 | cls.instance = super(SingletonConfig, cls).__new__(cls) 15 | cls.instance.__initialized = False 16 | return cls.instance 17 | 18 | def __init__(self): # set the defaults only once 19 | if(self.__initialized): return 20 | self.__initialized = True 21 | self.setDefault() 22 | 23 | @classmethod 24 | def setDefault(cls): # set the defaults in a dictionary 25 | cls.Default = odict() 26 | cls.Default['Enabled'] = True # enable plugin 27 | cls.Default['SizeDivisor'] = 1024.0 # binary file sizes 28 | cls.Default['MaxGlob'] = 5000 # skip large folders with as many items; 0=∞ 29 | cls.Default['SymbolPane'] = ['◧','◨'] # Left/Right 30 | cls.Default['SymbolHiddenF']= ['◻','◼'] # Show/Hide hidden files 31 | cls.Default['HideDotfile'] = False # hide non-hidden dotfiles on Windows 32 | cls.Default['Justify'] = odict({ # right-justification parameter 33 | 'folder' : 5 , 34 | 'file' : 5 , 35 | 'size' : 7 }) 36 | cls.msgTimeout = 5 # timeout in seconds for show_status_message 37 | 38 | @classmethod 39 | def loadConfig(cls): 40 | """Check StatusBarExtended.json for consistency/completeness, restore defaults on fail""" 41 | msg_t = cls.msgTimeout 42 | if hasattr(cls, 'locked_update'): 43 | show_status_message("StatusBarExtended: waiting for the config files to be updated, try again later...") 44 | return None, 'UpdateInProgress' 45 | cls.locked_update = True # ensure globally unique 'loadConfig' run so that e.g. we don't ask a user multiple times to delete corrupted configs 46 | 47 | cfgCurrent = load_json('StatusBarExtended.json') # within one fman session, it is guaranteed that multiple calls to load_json(...) with the same JSON name always return the same object, so save {} when deleting the files to force reload 48 | if type(cfgCurrent) not in (type(dict()), type(None)): 49 | # delete config files, fman's save_json can't replace types 50 | config_files = ['StatusBarExtended.json'] 51 | config_files.append( 'StatusBarExtended ('+PLATFORM+').json') 52 | user_settings_url = join(as_url(DATA_DIRECTORY),'Plugins','User','Settings') 53 | user_input_allow = '' 54 | prompt_msg_full = '' 55 | corrupt_config = [] 56 | for f in config_files: 57 | f_url = join(user_settings_url,f) 58 | f_path = as_path(f_url) 59 | 60 | if not exists(f_url): 61 | continue 62 | 63 | excerpt = str(load_json(f_path))[:100] 64 | prompt_msg = f_path \ 65 | + "\n that begins with:"\ 66 | + "\n " + excerpt 67 | corrupt_config.append({}) 68 | corrupt_config[-1]['url' ] = f_url 69 | corrupt_config[-1]['path'] = f_path 70 | corrupt_config[-1]['prompt_msg'] = prompt_msg 71 | 72 | corrupt_count = len(corrupt_config) 73 | if corrupt_count: # delete corrupt config files with the user's permission 74 | prompt_msg_full += "Please enter 'y' or 'yes' or '1' (without the quotes) to delete " + str(corrupt_count) + " corrupt plugin config file" \ 75 | + ("\n" if corrupt_count==1 else "s\n") \ 76 | + "with incompatible data type " + str(type(cfgCurrent)) + '\n'\ 77 | + "(all settings will be reset to their defaults)\n" 78 | for corrupt_file_dict in corrupt_config: 79 | prompt_msg_full += '\n' + corrupt_file_dict['prompt_msg'] + '\n' 80 | user_input_allow, ok = show_prompt(prompt_msg_full, default=user_input_allow) 81 | if ok and user_input_allow in ('y', 'yes', '1'): 82 | _reset = False 83 | for corrupt_file_dict in corrupt_config: 84 | f_url = corrupt_file_dict['url' ] 85 | f_path = corrupt_file_dict['path'] 86 | try: 87 | move_to_trash(f_url) 88 | except Exception as e: 89 | show_status_message("StatusBarExtended: failed to move to trash — " + f_path + " — with exception " + repr(e), msg_t) 90 | pass 91 | 92 | if not exists(f_url): 93 | show_status_message("StatusBarExtended: moved to trash — " + f_path, msg_t) 94 | _reset = True 95 | else: 96 | show_status_message("StatusBarExtended: failed to move to trash, deleting — " + f_path, msg_t) 97 | try: 98 | delete(f_url) 99 | except Exception as e: 100 | show_status_message("StatusBarExtended: failed to delete — " + f_path + " — with exception " + repr(e), msg_t) 101 | pass 102 | 103 | if not exists(f_url): 104 | show_status_message("StatusBarExtended: deleted — " + f_path, msg_t) 105 | _reset = True 106 | else: 107 | show_alert("StatusBarExtended: failed to move to trash or delete — " + f_path + "\nPlease, delete it manually") 108 | del cls.locked_update 109 | return None, 'ConfigDeleteFail' 110 | if _reset == True: # can save_json only when both files are deleted, otherwise there is a format mismatch ValueError 111 | cls.saveConfig({}) 112 | cfgCurrent = load_json('StatusBarExtended.json') 113 | else: # user canceled or didn't enter y/1 114 | show_status_message("StatusBarExtended: you canceled the deletion of the corrupted config files", msg_t) 115 | del cls.locked_update 116 | return None, 'Canceled' 117 | else: # can't find the config files 118 | show_alert("StatusBarExtended: can't find the corrupt config files:\n" \ 119 | + str(config_files) + "\n @ " + as_path(user_settings_url) \ 120 | + "\nMaybe their default location changed, please, delete them manually") 121 | del cls.locked_update 122 | return None, 'CorruptConfigNotFound' 123 | 124 | reload = False 125 | if (cfgCurrent is None) or (cfgCurrent == {}): # empty file or empty dictionary (corrupt configs deleted and replaced with {}), save defaults to the config file 126 | cfgCurrent = dict() 127 | for key in cls.Default: 128 | cfgCurrent[key] = cls.Default[key] 129 | reload = True 130 | 131 | if type(cfgCurrent) is dict: 132 | for key in cls.Default: # Fill in missing default values (e.g. in older installs) 133 | if key not in cfgCurrent: 134 | cfgCurrent[key] = cls.Default[key] 135 | reload = True 136 | if reload: 137 | cls.saveConfig(cfgCurrent) 138 | cfgCurrent = load_json('StatusBarExtended.json') 139 | 140 | if type(cfgCurrent) is dict: # check for still missing keys 141 | missing_keys=[] 142 | for key in cls.Default: 143 | if key in cfgCurrent: 144 | continue 145 | missing_keys.append(key) 146 | if len(missing_keys): 147 | show_status_message("StatusBarExtended: config files are missing some required keys:" + str(missing_keys) + " Maybe try to reload?", msg_t) 148 | del cls.locked_update 149 | return None, 'MissingKeys' 150 | else: 151 | del cls.locked_update 152 | return cfgCurrent, 'Success' 153 | else: 154 | show_status_message("StatusBarExtended: couldn't fix config files, maybe try to reload?", msg_t) 155 | del cls.locked_update 156 | return None, 'UnknownError' 157 | 158 | @classmethod 159 | def saveConfig(cls, save_data): 160 | save_json('StatusBarExtended.json', save_data) 161 | 162 | class ConfigureStatusBarExtended(ApplicationCommand): 163 | aliases = ('StatusBarExtended: configure',) 164 | 165 | def __call__(self): 166 | cfg = SingletonConfig() 167 | self.cfgCurrent, exit_status = cfg.loadConfig() 168 | if self.cfgCurrent is None: 169 | return 170 | 171 | prompt_msg = "Please enter any combination of the the &u&n&d&e&r&l&i&n&e&d numbers/letters" +'\n'\ 172 | + "to configure the corresponding option(s)" +'\n'\ 173 | + "# \tOption \t\tDescription" +'\n'\ 174 | + "&0. \t&a&l&l\t\t" + "Configure all the options" +'\n'\ 175 | + "&1. \t&Enabled\t\t" + "Enable/Disable this plugin" +'\n'\ 176 | + "&2. \tSize&Divisor\t" + "File size format: decimal or binary" +'\n'\ 177 | + "&3. \tMax&Glob\t\t" + "Skip folders with as many items" +'\n'\ 178 | + "&4. \tSymbol&Pane\t" + "Left/Right pane symbol" +'\n'\ 179 | + "&5. \tSymbol&HiddenF\t" + "Hidden files Shown/Hidden symbol" +'\n'\ 180 | + "&6. \tHideD&otfile\t" + "Treat .dotfiles as hidden files on Windows" +'\n'\ 181 | + "&7. \t&Justify\t\t" + "Minimum width of the Folder/File/Size values" +'\n'\ 182 | + '\n' 183 | value_new, ok = show_prompt(prompt_msg) 184 | if not ok: 185 | show_status_message("StatusBarExtended: setup canceled") 186 | return 187 | if any(x in value_new.casefold() for x in ('1','e','0','all')): 188 | self.setEnabled( cfg.Default['Enabled']) 189 | if any(x in value_new.casefold() for x in ('2','d','0','all')): 190 | self.setSizeDivisor( cfg.Default['SizeDivisor']) 191 | if any(x in value_new.casefold() for x in ('3','g','0','all')): 192 | self.setMaxGlob( cfg.Default['MaxGlob']) 193 | if any(x in value_new.casefold() for x in ('4','p','0','all')): 194 | self.setSymbolPane( cfg.Default['SymbolPane']) 195 | if any(x in value_new.casefold() for x in ('5','h','0','all')): 196 | self.setSymbolHiddenF(cfg.Default['SymbolHiddenF']) 197 | if any(x in value_new.casefold() for x in ('6','o','0','all')): 198 | self.setHideDotfile( cfg.Default['HideDotfile']) 199 | if any(x in value_new.casefold() for x in ('7','j','0','all')): 200 | self.setJustify( cfg.Default['Justify']) 201 | cfg.saveConfig(self.cfgCurrent) 202 | run_application_command('view_configuration_status_bar_extended') 203 | 204 | def setEnabled(self, value_default): 205 | _t = ('1', 't', 'true') 206 | _f = ('0', 'f', 'false') 207 | _tsep = "'" + "' or '".join(_t) + "'" 208 | _fsep = "'" + "' or '".join(_f) + "'" 209 | _accept = (_t + _f) 210 | value_cfg = str(self.cfgCurrent['Enabled']) 211 | prompt_msg = "Please enter " +_tsep+ " to enable this plugin" +'\n'\ 212 | + "or " +_fsep+ " to disable it" +'\n'\ 213 | + "or leave the field empty to restore the default ("+str(value_default) +'):' 214 | selection_start = 0 215 | value_new = '' 216 | value_new_fmt = value_new.casefold() 217 | while value_new_fmt not in _accept: 218 | value_new, ok = show_prompt(prompt_msg, value_cfg, selection_start) 219 | value_cfg = value_new # preserve user input on multiple edits 220 | if not ok: 221 | show_status_message("StatusBarExtended: setup canceled") 222 | return 223 | if value_new.strip(' ') == '': 224 | self.cfgCurrent['Enabled'] = value_default 225 | return 226 | value_new_fmt = value_new.casefold() 227 | if value_new_fmt not in _accept: 228 | show_alert("You entered\n" + value_new +'\n'\ 229 | + "I parsed it as " + value_new_fmt +'\n'\ 230 | + "but the only acceptable values are:\n" +_tsep+ "\n" +_fsep) 231 | self.cfgCurrent['Enabled'] = True if value_new_fmt in _t else False 232 | 233 | def setSizeDivisor(self, value_default): 234 | _accept = ('1000', '1024') 235 | value_cfg = str(int(self.cfgCurrent['SizeDivisor'])) 236 | prompt_msg = "Please enter the file size divisor ('1000' or '1024') to display file size\nin a decimal (1k=1000=10³) or binary (1k=1024=2¹⁰) format" + '\n'\ 237 | + "or leave the field empty to restore the default ("+str(value_default) +'):' 238 | selection_start = 2 239 | value_new = '' 240 | while value_new not in _accept: 241 | value_new, ok = show_prompt(prompt_msg, value_cfg, selection_start) 242 | value_cfg = value_new # preserve user input on multiple edits 243 | if not ok: 244 | show_status_message("StatusBarExtended: setup canceled") 245 | return 246 | if value_new.strip(' ') == '': 247 | self.cfgCurrent['SizeDivisor'] = value_default 248 | return 249 | elif value_new not in _accept: 250 | show_alert("You entered\n" + value_new +'\n'\ 251 | + "but the only acceptable values are:\n1000\n1024") 252 | self.cfgCurrent['SizeDivisor'] = float(value_new) 253 | 254 | def setMaxGlob(self, value_default): 255 | value_cfg = str(self.cfgCurrent['MaxGlob']) 256 | prompt_msg = "Please enter a natural number to set the threshold of the number of folders+files in a pane," +'\n'\ 257 | + "above which the status bar for such a pane will not be updated to improve performance" +'\n'\ 258 | + "or enter '0' to disable" +'\n'\ 259 | + "or leave the field empty to restore the default ("+str(value_default)+"):" 260 | selection_start = 0 261 | value_new = '' 262 | while not isNat0(value_new): 263 | value_new, ok = show_prompt(prompt_msg, value_cfg, selection_start) 264 | value_cfg = value_new # preserve user input on multiple edits 265 | if not ok: 266 | show_status_message("StatusBarExtended: setup canceled") 267 | return 268 | if value_new.strip(' ') == '': 269 | self.cfgCurrent['MaxGlob'] = value_default 270 | return 271 | if value_new.strip(' ') == '0': 272 | self.cfgCurrent['MaxGlob'] = 0 273 | return 274 | if not isInt(value_new): 275 | show_alert("You entered\n" + value_new +'\n'\ 276 | + "but I couldn't parse it as an integer") 277 | elif not isNat0(value_new): 278 | show_alert("You entered\n" + value_new +'\n'\ 279 | + "but I was expecting a non-negative integer 0,1,2,3–∞") 280 | self.cfgCurrent['MaxGlob'] = int(value_new) 281 | 282 | def setSymbolPane(self, value_default): 283 | value_cfg = " ".join(self.cfgCurrent['SymbolPane']) 284 | prompt_msg = "Please enter two symbols, separated by space, to indicate Left/Right pane" +'\n'\ 285 | + "or leave the field empty to restore the default ("+str(value_default)+"):" 286 | selection_start = 0 287 | selection_end = 0 288 | value_new = '' 289 | value_new_list = [] 290 | _len = len(value_new_list) 291 | len_def = len(value_default) 292 | while _len != len_def: 293 | value_new, ok = show_prompt(prompt_msg, value_cfg, selection_start, selection_end) 294 | value_cfg = value_new # preserve user input on multiple edits 295 | if not ok: 296 | show_status_message("StatusBarExtended: setup canceled") 297 | return 298 | if value_new.strip(' ') == '': 299 | self.cfgCurrent['SymbolPane'] = value_default 300 | return 301 | value_new_nosp = ' '.join(value_new.split()) # replace multiple spaces with 1 302 | value_new_list = value_new_nosp.split(' ') # split by space 303 | _len = len(value_new_list) 304 | if _len != len_def: 305 | show_alert("You entered\n" + value_new +'\n'\ 306 | + "I parsed it as " + str(value_new_list) + " with " + str(_len) + " element" + ("" if _len==1 else "s") +'\n'\ 307 | + "but was expecting " + str(len_def) + " elements") 308 | self.cfgCurrent['SymbolPane'] = value_new_list 309 | 310 | def setSymbolHiddenF(self, value_default): 311 | value_cfg = " ".join(self.cfgCurrent['SymbolHiddenF']) 312 | prompt_msg = "Please enter two symbols, separated by space, to indicate whether hidden files are Shown/Hidden" +'\n'\ 313 | + "or leave the field empty to restore the default ("+str(value_default)+"):" 314 | selection_start = 0 315 | selection_end = 0 316 | value_new = '' 317 | value_new_list = [] 318 | _len = len(value_new_list) 319 | len_def = len(value_default) 320 | while _len != len_def: 321 | value_new, ok = show_prompt(prompt_msg, value_cfg, selection_start, selection_end) 322 | value_cfg = value_new # preserve user input on multiple edits 323 | if not ok: 324 | show_status_message("StatusBarExtended: setup canceled") 325 | return 326 | if value_new.strip(' ') == '': 327 | self.cfgCurrent['SymbolHiddenF'] = value_default 328 | return 329 | value_new_nosp = ' '.join(value_new.split()) # replace multiple spaces with 1 330 | value_new_list = value_new_nosp.split(' ') # split by space 331 | _len = len(value_new_list) 332 | if _len != 2: 333 | show_alert("You entered\n" + value_new +'\n'\ 334 | + "I parsed it as " + str(value_new_list) + " with " + str(_len) + " element" + ("" if _len==1 else "s") +'\n'\ 335 | + "but was expecting " + str(len_def) + " elements") 336 | self.cfgCurrent['SymbolHiddenF'] = value_new_list 337 | 338 | def setHideDotfile(self, value_default): 339 | _t = ('1', 't', 'true') 340 | _f = ('0', 'f', 'false') 341 | _tsep = "'" + "' or '".join(_t) + "'" 342 | _fsep = "'" + "' or '".join(_f) + "'" 343 | _accept = (_t + _f) 344 | value_cfg = str(self.cfgCurrent['HideDotfile']) 345 | prompt_msg = "Please enter " +_tsep+ " to treat all .dotfiles on Windows as hidden files\n even if they don't have a 'hidden' attribute" +'\n'\ 346 | + "or " +_fsep+ " to treat them as regular files" +'\n'\ 347 | + "or leave the field empty to restore the default ("+str(value_default) +'):' 348 | selection_start = 0 349 | value_new = '' 350 | value_new_fmt = value_new.casefold() 351 | while value_new_fmt not in _accept: 352 | value_new, ok = show_prompt(prompt_msg, value_cfg, selection_start) 353 | value_cfg = value_new # preserve user input on multiple edits 354 | if not ok: 355 | show_status_message("StatusBarExtended: setup canceled") 356 | return 357 | if value_new.strip(' ') == '': 358 | self.cfgCurrent['HideDotfile'] = value_default 359 | return 360 | value_new_fmt = value_new.casefold() 361 | if value_new_fmt not in _accept: 362 | show_alert("You entered\n" + value_new +'\n'\ 363 | + "I parsed it as " + value_new_fmt +'\n'\ 364 | + "but the only acceptable values are:\n" +_tsep+ "\n" +_fsep) 365 | self.cfgCurrent['HideDotfile'] = True if value_new_fmt in _t else False 366 | 367 | def setJustify(self, value_default): 368 | value_cfg_in = self.cfgCurrent['Justify'] 369 | value_cfg = " ".join(str(val) for val in value_cfg_in.values()) 370 | val_def_fmt = " ".join(str(val) for val in value_default.values()) 371 | max_val = 11 372 | prompt_msg = "Please enter three natural numbers, each <= "+str(max_val)+", separated by space, to set the minimum width of" +'\n'\ 373 | + "the folder, file, and size indicators respectively." +'\n'\ 374 | + "e.g. with a min width=2, 1 in 1 and 21 will align, but not in 321:" +'\n'\ 375 | + " 1" +'\n'\ 376 | + "21" +'\n'\ 377 | + "321" +'\n'\ 378 | + "or enter '0' to restore an individual default" +'\n'\ 379 | + "or leave the field empty to restore all the defaults ("+val_def_fmt+"):" 380 | selection_start = 0 381 | selection_end = 1 382 | value_new = '' 383 | value_new_list = [] 384 | _len = len(value_new_list) 385 | len_def = len(value_default) 386 | above_max = False 387 | while (not all([isNat0(v) for v in value_new_list])) \ 388 | or above_max \ 389 | or _len != len_def: 390 | value_new, ok = show_prompt(prompt_msg, value_cfg, selection_start, selection_end) 391 | value_cfg = value_new # preserve user input on multiple edits 392 | if not ok: 393 | show_status_message("StatusBarExtended: setup canceled") 394 | return 395 | if value_new.strip(' ') == '': 396 | self.cfgCurrent['Justify'] = value_default 397 | return 398 | value_new_nosp = ' '.join(value_new.split()) # replace multiple spaces with 1 399 | value_new_list = value_new_nosp.split(' ') # split by space 400 | _len = len(value_new_list) 401 | if not all([isInt(v) for v in value_new_list]): 402 | show_alert("You entered\n" + value_new +'\n'\ 403 | + "I parsed it as " + str(value_new_list) + " with " + str(_len) + " element" + ("" if _len==1 else "s") +'\n'\ 404 | + "but I couldn't parse all elements as integers") 405 | elif not all([isNat0(v) for v in value_new_list]): 406 | show_alert("You entered\n" + value_new +'\n'\ 407 | + "I parsed it as " + str(value_new_list) + " with " + str(_len) + " element" + ("" if _len==1 else "s") +'\n'\ 408 | + "but couldn't parse all elements as non-negative integers 0,1,2,3–∞") 409 | elif True in [int(v) > max_val for v in value_new_list]: 410 | above_max = True 411 | show_alert("You entered\n" + value_new +'\n'\ 412 | + "I parsed it as " + str(value_new_list) + " with " + str(_len) + " element" + ("" if _len==1 else "s") +'\n'\ 413 | + "but the maximum value of each number is " + str(max_val)) 414 | continue 415 | elif all([int(v) <= max_val for v in value_new_list]): 416 | above_max = False 417 | if _len != len_def: 418 | show_alert("You entered\n" + value_new +'\n'\ 419 | + "I parsed it as " + str(value_new_list) + " with " + str(_len) + " element" + ("" if _len==1 else "s") +'\n'\ 420 | + "but was expecting " + str(len_def) + " elements") 421 | for i, key in enumerate(value_default): 422 | if value_new_list[i] == '0': 423 | self.cfgCurrent['Justify'][key] = value_default[key] 424 | else: 425 | self.cfgCurrent['Justify'][key] = int(value_new_list[i]) 426 | 427 | 428 | 429 | class ViewConfigurationStatusBarExtended(ApplicationCommand): 430 | aliases = ('StatusBarExtended: view current configuration settings',) 431 | 432 | def __call__(self): 433 | cfg = SingletonConfig() 434 | cfgCurrent, exit_status = cfg.loadConfig() 435 | if cfgCurrent is None: 436 | return 437 | cfg_fmt = "StatusBarExtended's configuration:" +'\n\n'\ 438 | + "Option" + '\t ' + 'Current' + '\t' + 'Default' +'\n' 439 | for key in cfg.Default: # Default is odict, preserving the key order 440 | if key in ('SizeDivisor'): 441 | cfg_fmt += key +'\t = '+ str(int(cfgCurrent[key])) + "\t"+str(int(cfg.Default[key])) +'\n' 442 | elif key in ('SymbolPane'): 443 | cfg_fmt += key +'\t = '+ " ".join(cfgCurrent[key]) + "\t"+" ".join(cfg.Default[key]) +'\n' 444 | elif key in ('SymbolHiddenF'): 445 | cfg_fmt += key + '=' + " ".join(cfgCurrent[key]) + "\t"+" ".join(cfg.Default[key]) +'\n' 446 | elif key in ('Justify'): 447 | cfg_fmt += key +'\t = '+ " ".join(str(v) for v in cfgCurrent['Justify'].values()) + "\t"+" ".join(str(v) for v in cfg.Default['Justify'].values()) +'\n' 448 | else: 449 | cfg_fmt += key +'\t = '+ str(cfgCurrent[key]) + "\t"+str(cfg.Default[key]) +'\n' 450 | show_alert(cfg_fmt) 451 | 452 | def isInt(s: str) -> bool: 453 | try: 454 | int(s) 455 | return True 456 | except ValueError: 457 | return False 458 | def isNat0(s: str) -> bool: 459 | try: 460 | int(s) 461 | return int(s) >= 0 462 | except ValueError: 463 | return False 464 | def isNat1(s: str) -> bool: 465 | try: 466 | int(s) 467 | return int(s) > 0 468 | except ValueError: 469 | return False 470 | --------------------------------------------------------------------------------