├── .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 | |  |  |
40 |
41 | | Status Bar alignment |
42 | | :------------------------------: |
43 | |  |
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 | 
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 |
--------------------------------------------------------------------------------