├── scum ├── resources │ ├── __init__.py │ ├── tabs.dat │ ├── help.txt │ ├── config.txt │ └── start_up.txt ├── docs │ ├── scum.gif │ ├── screenshot.png │ └── TODO.txt ├── __init__.py ├── modules │ ├── __init__.py │ ├── term.py │ ├── popup.py │ └── browse.py ├── scum.py └── main.py ├── setup.cfg ├── MANIFEST ├── setup.py ├── README.md ├── README.rst └── LICENSE /scum/resources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scum/resources/tabs.dat: -------------------------------------------------------------------------------- 1 | False -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /scum/docs/scum.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCareaga/scum/HEAD/scum/docs/scum.gif -------------------------------------------------------------------------------- /scum/docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCareaga/scum/HEAD/scum/docs/screenshot.png -------------------------------------------------------------------------------- /scum/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from scum.main import MainGUI 3 | except: 4 | from main import MainGUI 5 | -------------------------------------------------------------------------------- /scum/resources/help.txt: -------------------------------------------------------------------------------- 1 | Keybindings can be customized in the config file. To open said file press F5. To update the 2 | changes just save the file. The changes will then be in effect. 3 | 4 | Colors can also be edited in the config file and update without having to restart the editor. 5 | 6 | Happy editing! 7 | -------------------------------------------------------------------------------- /scum/modules/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | try: 3 | from scum.modules.browse import DirectoryNode 4 | from scum.modules.term import ToggleTerm 5 | from scum.modules.popup import * 6 | 7 | except: 8 | from modules.browse import DirectoryNode 9 | from modules.term import ToggleTerm 10 | from modules.popup import * 11 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | README.rst 3 | scum.py 4 | setup.cfg 5 | setup.py 6 | src/__init__.py 7 | src/main.py 8 | src/modules/__init__.py 9 | src/modules/browse.py 10 | src/modules/popup.py 11 | src/modules/term.py 12 | src/resources/__init__.py 13 | src/resources/config.txt 14 | src/resources/help.txt 15 | src/resources/start_up.txt 16 | src/resources/tabs.dat 17 | -------------------------------------------------------------------------------- /scum/modules/term.py: -------------------------------------------------------------------------------- 1 | import urwid 2 | 3 | class ToggleTerm(urwid.Terminal): 4 | def __init__(self, display): 5 | super().__init__(None) 6 | self.display = display 7 | 8 | def keypress(self, size, key): 9 | ret = super().keypress(size, key) 10 | if key == self.display.config['terminal']: 11 | self.display.toggle_term() 12 | return ret 13 | -------------------------------------------------------------------------------- /scum/scum.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # from scum.main import MainGUI, START_PATH, TABS_PATH 4 | from main import MainGUI, START_PATH, TABS_PATH 5 | import os 6 | import signal 7 | 8 | def main(): 9 | os.system('stty -ixon') # disable XOFF to accept Ctrl-S 10 | # instantiate it! 11 | main = MainGUI() 12 | signal.signal(signal.SIGTSTP, signal.SIG_IGN) 13 | signal.signal(signal.SIGINT, signal.SIG_IGN) 14 | status = main.display() 15 | os.system('stty ixon') # re-enable XOFF! 16 | if status == 'failure': 17 | with open(TABS_PATH, 'w') as f: 18 | f.write(START_PATH + '\n') 19 | f.write('False') 20 | 21 | if __name__=="__main__": 22 | main() 23 | 24 | 25 | -------------------------------------------------------------------------------- /scum/modules/popup.py: -------------------------------------------------------------------------------- 1 | import urwid 2 | 3 | class PopUpDialog(urwid.WidgetWrap): 4 | """A dialog that appears with nothing but a close button """ 5 | signals = ['close'] 6 | def __init__(self): 7 | close_button = urwid.Button("that's pretty cool") 8 | urwid.connect_signal(close_button, 'click', 9 | lambda button:self._emit("close")) 10 | pile = urwid.Pile([urwid.Text( 11 | "^^ I'm attached to the widget that opened me. " 12 | "Try resizing the window!\n"), close_button]) 13 | fill = urwid.Filler(pile) 14 | self.__super.__init__(urwid.AttrWrap(fill, 'popbg')) 15 | 16 | 17 | class ThingWithAPopUp(urwid.PopUpLauncher): 18 | def __init__(self): 19 | self.__super.__init__(urwid.Button("click-me")) 20 | urwid.connect_signal(self.original_widget, 'click', 21 | lambda button: self.open_pop_up()) 22 | 23 | def create_pop_up(self): 24 | pop_up = PopUpDialog() 25 | urwid.connect_signal(pop_up, 'close', 26 | lambda button: self.close_pop_up()) 27 | return pop_up 28 | 29 | def get_pop_up_parameters(self): 30 | return {'left':0, 'top':1, 'overlay_width':32, 'overlay_height':7} 31 | 32 | -------------------------------------------------------------------------------- /scum/docs/TODO.txt: -------------------------------------------------------------------------------- 1 | - new file creation: 2 | need a file browser that selects directories rather than files 3 | then prompt the user for a name for the file, create it and open it 4 | 5 | - ctrl + arrow keys needs improvement, it doesn't seem quite right. 6 | 7 | - make tabsize customizable 8 | 9 | - when user presses enter, a new line should start at the current indentation level 10 | 11 | - somehow making tab a single character (\t) that can be backspaced 12 | 13 | - fix finding to make it easier to use and more straight forward, then add replace 14 | - when the user presses ctrl+f, they can type in a new search string. when 15 | they press enter the search string will be saved. 16 | - then the user can move to the next/previous instance of this search string 17 | by pressing a hot key (not sure what key) 18 | - once this is done, I should add a replace that works the same way except it replaces 19 | the next instance of the search string 20 | 21 | - auto-complete 22 | - I believe this is do-able, I could use an urwid pop-up with a small listbox of suggestions 23 | 24 | - add line numbers, coudl be done with a seperate listbox next to the main text listbox 25 | - could be turned on and off by hotkey 26 | -------------------------------------------------------------------------------- /scum/resources/config.txt: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # 3 | # This is the scum configuration file 4 | # key bindings, colors and syntax-highlighting themes can be defined here 5 | # when you are done editing save the file and then press ctrl+t to 6 | # update the changes! 7 | # 8 | # 9 | # GUI widget options: 10 | # header - the attributes of the top bar 11 | # footer - the attributes of the bottom bar 12 | # focus - the attributes of the current focused item in openfile window 13 | # key - the attrbutes of the highlighted parts of the top bar 14 | # selected - attributes of the currently opened tab indicator 15 | ################################################################## 16 | 17 | # Key-bindings 18 | open: ctrl o 19 | save: ctrl s 20 | find: ctrl f 21 | undo: ctrl q 22 | delline: ctrl d 23 | prevtab: meta page up 24 | nexttab: meta page down 25 | closetab: ctrl w 26 | terminal: ctrl g 27 | linenum: ctrl n 28 | layout: f1 29 | config: f5 30 | exit: ctrl x 31 | 32 | # syntax style 33 | style: emacs 34 | 35 | # gui colors 36 | # format -> widget:foreground,background,extra 37 | 38 | header:white,dark gray,bold 39 | footer:white,dark gray,bold 40 | focus:light gray,dark blue,bold 41 | key:white,dark blue,bold 42 | selected:white,dark blue,bold 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from setuptools import setup 4 | 5 | if sys.version_info[0] < 3: 6 | sys.exit("Scum requires Python 3.") 7 | 8 | VERSION = 'v0.2.8' 9 | 10 | setup_kwargs = { 11 | "version": VERSION, 12 | "description": 'A scummy editor for your terminal!', 13 | "author": 'Christian Careaga', 14 | "author_email": 'christian.careaga7@gmail.com', 15 | "url": 'https://github.com/CCareaga/scum', 16 | "download_url": "https://github.com/CCareaga/scum/zipball/" + VERSION, 17 | "install_requires":['urwid', 'Pygments'], 18 | "classifiers": [ 19 | "License :: OSI Approved :: MIT License", 20 | "Intended Audience :: Developers", 21 | "Programming Language :: Python :: 3", 22 | "Topic :: Utilities", 23 | "Topic :: Text Editors", 24 | ] 25 | } 26 | 27 | 28 | if __name__ == '__main__': 29 | setup( 30 | name='scum', 31 | entry_points={ 32 | 'console_scripts': [ 33 | 'scum = scum.scum:main' 34 | ] 35 | }, 36 | data_files=[('', ['README.rst'])], 37 | packages = ['scum.modules', 'scum', 'scum.resources'], 38 | package_data = {'scum.resources': ['config.txt', 'start_up.txt', 'tabs.dat', 'help.txt']}, 39 | long_description=open('README.rst').read(), 40 | **setup_kwargs 41 | ) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SCUM 2 | ------------ 3 | Scum is a terminal-based, multi-tab text editor written in Python. 4 | 5 | ![](https://github.com/CCareaga/scum/blob/master/scum/docs/scum.gif?raw=true "Scum in urxvt!") 6 | 7 | ### About 8 | ------------ 9 | First of all, Scum is really just a novelty editor I wrote mostly for learning and fun. It was not made to compete with 10 | seasoned text editors such as Emacs or Vim (hence the name "Scum"). With that being said, it is still a fully-functional 11 | terminal based editor! It can be used for both quick editing and full on development. Overall it was extremely fun to write 12 | and was a wonderful learning experience. Enjoy! (at your own risk) 13 | 14 | ### Installation 15 | ------------ 16 | To install scum simply use pip: 17 | 18 | ``` 19 | pip install scum 20 | ``` 21 | 22 | ### Features 23 | ------------ 24 | - Syntax-highlighting 25 | - Multiple Tabs 26 | - Undo and find functions 27 | - Open and save files (complete with text-based file-browser) 28 | - Fully customizable 29 | - On-the-go configuration changing 30 | - Tab saving 31 | - Togglable line numbers and terminal 32 | 33 | ### Dependencies 34 | ------------ 35 | - Python 3.x 36 | - Urwid 37 | - Pygments 38 | 39 | ### To-Do 40 | ------------ 41 | - Find and replace. 42 | - Creating a new file. 43 | 44 | ### Key Bindings 45 | ------------ 46 | | Key: | Command: | 47 | | ------------- |:---------------------:| 48 | | Ctrl+O | Open file(s) | 49 | | Ctrl+S | Save file | 50 | | Ctrl+W | Close current tab | 51 | | Ctrl+page up | Move to the next tab | 52 | | Ctrl+page down| Move to the prev tab | 53 | | F5 | Edit the config file | 54 | | F1 | Change GUI layout | 55 | | Ctrl+F | Find | 56 | | Ctrl+Q | Undo last action | 57 | | Ctrl+D | Delete current line | 58 | | Ctrl+X | Exit | 59 | 60 | -------------------------------------------------------------------------------- /scum/resources/start_up.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ::: ::: :::::::::: ::: :::::::: :::::::: :::: :::: :::::::::: 5 | :+: :+: :+: :+: :+: :+: :+: :+: +:+:+: :+:+:+ :+: 6 | +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+:+ +:+ +:+ 7 | +#+ +:+ +#+ +#++:++# +#+ +#+ +#+ +:+ +#+ +:+ +#+ +#++:++# 8 | +#+ +#+#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ 9 | #+#+# #+#+# #+# #+# #+# #+# #+# #+# #+# #+# #+# 10 | ### ### ########## ########## ######## ######## ### ### ########## 11 | 12 | 13 | ::::::::::: :::::::: 14 | :+: :+: :+: 15 | +:+ +:+ +:+ 16 | +#+ +#+ +:+ 17 | +#+ +#+ +#+ 18 | #+# #+# #+# 19 | ### ######## 20 | 21 | 22 | :::::::: :::::::: ::: ::: :::: :::: 23 | :+: :+: :+: :+: :+: :+: +:+:+: :+:+:+ 24 | +:+ +:+ +:+ +:+ +:+ +:+:+ +:+ 25 | +#++:++#++ +#+ +#+ +:+ +#+ +:+ +#+ 26 | +#+ +#+ +#+ +#+ +#+ +#+ 27 | #+# #+# #+# #+# #+# #+# #+# #+# 28 | ######## ######## ######## ### ### 29 | 30 | 31 | DEFAULT KEY BINDINGS: 32 | 33 | ctrl+o to open a file(s) 34 | ctrl+s to save current file 35 | ctrl+f to find occurences of a string 36 | crtl+d to delete current line 37 | ctrl+q to undo last action 38 | ctrl+w to close the current tab 39 | ctrl+page up to move to the next tab 40 | ctrl+page down to move to the previous tab 41 | F1 to toggle the GUI layout 42 | F5 to open config file 43 | F6 to update configuration 44 | ctrl+g to open/close the terminal 45 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | SCUM 2 | ------ 3 | 4 | Scum is a terminal-based, multi-tab text editor written in Python. 5 | 6 | .. figure:: https://github.com/CCareaga/scum/blob/master/scum/docs/scum.gif?raw=true 7 | :alt: Scum in urxvt! 8 | 9 | About 10 | --------- 11 | 12 | First of all, Scum is really just a novelty editor I wrote mostly for 13 | learning and fun. It was not made to compete with seasoned text editors 14 | such as Emacs or Vim (hence the name "Scum"). With that being said, it 15 | is still a fully-functional terminal based editor! It can be used for 16 | both quick editing and full on development. Overall it was extremely fun 17 | to write and was a wonderful learning experience. Enjoy! (at your own 18 | risk) 19 | 20 | Features 21 | ------------ 22 | 23 | - Syntax-highlighting 24 | - Multiple Tabs 25 | - Undo and find functions 26 | - Open and save files (complete with text-based file-browser) 27 | - Fully customizable 28 | - On-the-go configuration changing 29 | - Tab saving 30 | - Togglable line numbers and terminal 31 | 32 | Dependencies 33 | ---------------- 34 | 35 | - Python 3.x 36 | - Urwid 37 | - Pygments 38 | 39 | To-Do 40 | --------- 41 | 42 | - Find and replace. 43 | - Creating a new file. 44 | 45 | Key Bindings 46 | ---------------- 47 | 48 | +------------------+------------------------+ 49 | | Key: | Command: | 50 | +==================+========================+ 51 | | Ctrl+O | Open file(s) | 52 | +------------------+------------------------+ 53 | | Ctrl+S | Save file | 54 | +------------------+------------------------+ 55 | | Ctrl+W | Close current tab | 56 | +------------------+------------------------+ 57 | | Ctrl+page up | Move to the next tab | 58 | +------------------+------------------------+ 59 | | Ctrl+page down | Move to the prev tab | 60 | +------------------+------------------------+ 61 | | F5 | Edit the config file | 62 | +------------------+------------------------+ 63 | | F1 | Change GUI layout | 64 | +------------------+------------------------+ 65 | | Ctrl+F | Find | 66 | +------------------+------------------------+ 67 | | Ctrl+Q | Undo last action | 68 | +------------------+------------------------+ 69 | | Ctrl+D | Delete current line | 70 | +------------------+------------------------+ 71 | | Ctrl+X | Exit | 72 | +------------------+------------------------+ 73 | -------------------------------------------------------------------------------- /scum/modules/browse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Urwid example lazy directory browser / tree view 4 | # Copyright (C) 2004-2011 Ian Ward 5 | # Copyright (C) 2010 Kirk McDonald 6 | # Copyright (C) 2010 Rob Lanphier 7 | # 8 | # This library is free software; you can redistribute it and/or 9 | # modify it under the terms of the GNU Lesser General Public 10 | # License as published by the Free Software Foundation; either 11 | # version 2.1 of the License, or (at your option) any later version. 12 | # 13 | # This library is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 | # Lesser General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Lesser General Public 19 | # License along with this library; if not, write to the Free Software 20 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 21 | # 22 | # Urwid web site: http://excess.org/urwid/ 23 | 24 | """ 25 | Urwid example lazy directory browser / tree view 26 | Features: 27 | - custom selectable widgets for files and directories 28 | - custom message widgets to identify access errors and empty directories 29 | - custom list walker for displaying widgets in a tree fashion 30 | - outputs a quoted list of files and directories "selected" on exit 31 | """ 32 | 33 | import itertools 34 | import re 35 | import os 36 | 37 | import urwid 38 | 39 | 40 | class FlagFileWidget(urwid.TreeWidget): 41 | # apply an attribute to the expand/unexpand icons 42 | unexpanded_icon = urwid.AttrMap(urwid.TreeWidget.unexpanded_icon, 43 | 'dirmark') 44 | expanded_icon = urwid.AttrMap(urwid.TreeWidget.expanded_icon, 45 | 'dirmark') 46 | 47 | def __init__(self, node, display): 48 | self.__super.__init__(node) 49 | self.display= display 50 | # insert an extra AttrWrap for our own use 51 | self._w = urwid.AttrWrap(self._w, None) 52 | self.flagged = False 53 | self.update_w() 54 | 55 | def selectable(self): 56 | return True 57 | 58 | def keypress(self, size, key): 59 | """allow subclasses to intercept keystrokes""" 60 | key = self.__super.keypress(size, key) 61 | if key: 62 | key = self.unhandled_keys(size, key) 63 | return key 64 | 65 | def unhandled_keys(self, size, key): 66 | """ 67 | Override this method to intercept keystrokes in subclasses. 68 | Default behavior: Toggle flagged on space, ignore other keys. 69 | """ 70 | if key == " ": 71 | if not self.flagged: 72 | self.display.new_files.append(self.get_node().get_value()) 73 | else: 74 | self.display.new_files.remove(self.get_node().get_value()) 75 | self.flagged = not self.flagged 76 | self.update_w() 77 | self.display.update_status() 78 | else: 79 | return key 80 | 81 | def update_w(self): 82 | """Update the attributes of self.widget based on self.flagged. 83 | """ 84 | if self.flagged: 85 | self._w.attr = 'flagged' 86 | self._w.focus_attr = 'flagged focus' 87 | else: 88 | self._w.attr = 'body' 89 | self._w.focus_attr = 'focus' 90 | 91 | 92 | class FileTreeWidget(FlagFileWidget): 93 | """Widget for individual files.""" 94 | def __init__(self, node, display): 95 | self.__super.__init__(node, display) 96 | path = node.get_value() 97 | add_widget(path, self) 98 | 99 | def get_display_text(self): 100 | return self.get_node().get_key() 101 | 102 | 103 | 104 | class EmptyWidget(urwid.TreeWidget): 105 | """A marker for expanded directories with no contents.""" 106 | def get_display_text(self): 107 | return ('flag', '(empty directory)') 108 | 109 | 110 | class ErrorWidget(urwid.TreeWidget): 111 | """A marker for errors reading directories.""" 112 | 113 | def get_display_text(self): 114 | return ('error', "(error/permission denied)") 115 | 116 | 117 | class DirectoryWidget(urwid.TreeWidget): 118 | """Widget for a directory.""" 119 | def __init__(self, node): 120 | self.__super.__init__(node) 121 | path = node.get_value() 122 | add_widget(path, self) 123 | self.expanded = starts_expanded(path) 124 | self.update_expanded_icon() 125 | 126 | def get_display_text(self): 127 | node = self.get_node() 128 | if node.get_depth() == 0: 129 | return "/" 130 | else: 131 | return node.get_key() 132 | 133 | 134 | class FileNode(urwid.TreeNode): 135 | """Metadata storage for individual files""" 136 | 137 | def __init__(self, path, display, parent=None): 138 | self.display = display 139 | depth = path.count(dir_sep()) 140 | key = os.path.basename(path) 141 | urwid.TreeNode.__init__(self, path, key=key, parent=parent, depth=depth) 142 | 143 | def load_parent(self): 144 | parentname, myname = os.path.split(self.get_value()) 145 | parent = DirectoryNode(parentname, display) 146 | parent.set_child_node(self.get_key(), self) 147 | return parent 148 | 149 | def load_widget(self): 150 | return FileTreeWidget(self, self.display) 151 | 152 | 153 | class EmptyNode(urwid.TreeNode): 154 | def load_widget(self): 155 | return EmptyWidget(self) 156 | 157 | 158 | class ErrorNode(urwid.TreeNode): 159 | def load_widget(self): 160 | return ErrorWidget(self) 161 | 162 | 163 | class DirectoryNode(urwid.ParentNode): 164 | """Metadata storage for directories""" 165 | 166 | def __init__(self, path, display, parent=None): 167 | self.display = display 168 | if path == dir_sep(): 169 | depth = 0 170 | key = None 171 | else: 172 | depth = path.count(dir_sep()) 173 | key = os.path.basename(path) 174 | urwid.ParentNode.__init__(self, path, key=key, parent=parent, 175 | depth=depth) 176 | 177 | def load_parent(self): 178 | parentname, myname = os.path.split(self.get_value()) 179 | parent = DirectoryNode(parentname, self.display) 180 | parent.set_child_node(self.get_key(), self) 181 | return parent 182 | 183 | def load_child_keys(self): 184 | dirs = [] 185 | files = [] 186 | try: 187 | path = self.get_value() 188 | # separate dirs and files 189 | for a in os.listdir(path): 190 | if os.path.isdir(os.path.join(path,a)): 191 | dirs.append(a) 192 | else: 193 | files.append(a) 194 | except OSError as e: 195 | depth = self.get_depth() + 1 196 | self._children[None] = ErrorNode(self, parent=self, key=None, 197 | depth=depth) 198 | return [None] 199 | 200 | # sort dirs and files 201 | dirs.sort(key=alphabetize) 202 | files.sort(key=alphabetize) 203 | # store where the first file starts 204 | self.dir_count = len(dirs) 205 | # collect dirs and files together again 206 | keys = dirs + files 207 | if len(keys) == 0: 208 | depth=self.get_depth() + 1 209 | self._children[None] = EmptyNode(self, parent=self, key=None, 210 | depth=depth) 211 | keys = [None] 212 | return keys 213 | 214 | def load_child_node(self, key): 215 | """Return either a FileNode or DirectoryNode""" 216 | index = self.get_child_index(key) 217 | if key is None: 218 | return EmptyNode(None) 219 | else: 220 | path = os.path.join(self.get_value(), key) 221 | if index < self.dir_count: 222 | return DirectoryNode(path, self.display, parent=self) 223 | else: 224 | path = os.path.join(self.get_value(), key) 225 | return FileNode(path, self.display, parent=self) 226 | 227 | def load_widget(self): 228 | return DirectoryWidget(self) 229 | 230 | ####### 231 | # global cache of widgets 232 | _widget_cache = {} 233 | 234 | def add_widget(path, widget): 235 | """Add the widget for a given path""" 236 | 237 | _widget_cache[path] = widget 238 | 239 | def get_flagged_names(): 240 | """Return a list of all filenames marked as flagged.""" 241 | 242 | l = [] 243 | for w in _widget_cache.values(): 244 | if w.flagged: 245 | l.append(w.get_node().get_value()) 246 | return l 247 | 248 | 249 | 250 | ###### 251 | # store path components of initial current working directory 252 | _initial_cwd = [] 253 | 254 | def store_initial_cwd(name): 255 | """Store the initial current working directory path components.""" 256 | 257 | global _initial_cwd 258 | _initial_cwd = name.split(dir_sep()) 259 | 260 | def starts_expanded(name): 261 | """Return True if directory is a parent of initial cwd.""" 262 | 263 | if name is '/': 264 | return True 265 | 266 | l = name.split(dir_sep()) 267 | if len(l) > len(_initial_cwd): 268 | return False 269 | 270 | if l != _initial_cwd[:len(l)]: 271 | return False 272 | 273 | return True 274 | 275 | 276 | def escape_filename_sh(name): 277 | """Return a hopefully safe shell-escaped version of a filename.""" 278 | 279 | # check whether we have unprintable characters 280 | for ch in name: 281 | if ord(ch) < 32: 282 | # found one so use the ansi-c escaping 283 | return escape_filename_sh_ansic(name) 284 | 285 | # all printable characters, so return a double-quoted version 286 | name.replace('\\','\\\\') 287 | name.replace('"','\\"') 288 | name.replace('`','\\`') 289 | name.replace('$','\\$') 290 | return '"'+name+'"' 291 | 292 | 293 | def escape_filename_sh_ansic(name): 294 | """Return an ansi-c shell-escaped version of a filename.""" 295 | 296 | out =[] 297 | # gather the escaped characters into a list 298 | for ch in name: 299 | if ord(ch) < 32: 300 | out.append("\\x%02x"% ord(ch)) 301 | elif ch == '\\': 302 | out.append('\\\\') 303 | else: 304 | out.append(ch) 305 | 306 | # slap them back together in an ansi-c quote $'...' 307 | return "$'" + "".join(out) + "'" 308 | 309 | SPLIT_RE = re.compile(r'[a-zA-Z]+|\d+') 310 | def alphabetize(s): 311 | L = [] 312 | for isdigit, group in itertools.groupby(SPLIT_RE.findall(s), key=lambda x: x.isdigit()): 313 | if isdigit: 314 | for n in group: 315 | L.append(('', int(n))) 316 | else: 317 | L.append((''.join(group).lower(), 0)) 318 | return L 319 | 320 | def dir_sep(): 321 | """Return the separator used in this os.""" 322 | return getattr(os.path,'sep','/') 323 | 324 | 325 | if __name__=="__main__": 326 | main() 327 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /scum/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import urwid 4 | import os 5 | import re 6 | import signal 7 | import string 8 | #from scum.modules import * 9 | from modules import * 10 | 11 | import pygments.util 12 | from pygments.lexers import guess_lexer_for_filename, get_lexer_for_filename 13 | from pygments.lexers.special import TextLexer 14 | from pygments.lexers.python import PythonLexer, Python3Lexer 15 | from pygments.token import Token 16 | from pygments.filter import Filter 17 | from pygments.styles import get_style_by_name 18 | 19 | from pkg_resources import resource_filename 20 | 21 | RE_WORD = re.compile(r'\w+') 22 | RE_NOT_WORD = re.compile(r'\W+') 23 | 24 | 25 | try: 26 | CONFIG_PATH = os.path.abspath(resource_filename('scum.resources', 'config.txt')) 27 | HELP_PATH = os.path.abspath(resource_filename('scum.resources', 'help.txt')) 28 | TABS_PATH = os.path.abspath(resource_filename('scum.resources', 'tabs.dat')) 29 | START_PATH = os.path.abspath(resource_filename('scum.resources', 'start_up.txt')) 30 | 31 | except: 32 | CONFIG_PATH = os.path.abspath(resource_filename('resources', 'config.txt')) 33 | HELP_PATH = os.path.abspath(resource_filename('resources', 'help.txt')) 34 | TABS_PATH = os.path.abspath(resource_filename('resources', 'tabs.dat')) 35 | START_PATH = os.path.abspath(resource_filename('resources', 'start_up.txt')) 36 | 37 | CONFIG = { 38 | 'short_to_rgb': { # color look-up table for 8-bit to RGB hex 39 | # Primary 3-bit (8 colors). Unique representation! 40 | '00': '000000', '01': '800000', '02': '008000', '03': '808000', '04': '000080', 41 | '05': '800080', '06': '008080', '07': 'c0c0c0', 42 | # Equivalent "bright" versions of original 8 colors. 43 | '08': '808080', '09': 'ff0000', '10': '00ff00', '11': 'ffff00', '12': '0000ff', 44 | '13': 'ff00ff', '14': '00ffff', '15': 'ffffff', 45 | # Strictly ascending. 46 | '16': '000000', '17': '00005f', '18': '000087', '19': '0000af', '20': '0000d7', 47 | '21': '0000ff', '22': '005f00', '23': '005f5f', '24': '005f87', '25': '005faf', 48 | '26': '005fd7', '27': '005fff', '28': '008700', '29': '00875f', '30': '008787', 49 | '31': '0087af', '32': '0087d7', '33': '0087ff', '34': '00af00', '35': '00af5f', 50 | '36': '00af87', '37': '00afaf', '38': '00afd7', '39': '00afff', '40': '00d700', 51 | '41': '00d75f', '42': '00d787', '43': '00d7af', '44': '00d7d7', '45': '00d7ff', 52 | '46': '00ff00', '47': '00ff5f', '48': '00ff87', '49': '00ffaf', '50': '00ffd7', 53 | '51': '00ffff', '52': '5f0000', '53': '5f005f', '54': '5f0087', '55': '5f00af', 54 | '56': '5f00d7', '57': '5f00ff','58': '5f5f00', '59': '5f5f5f', '60': '5f5f87', 55 | '61': '5f5faf', '62': '5f5fd7', '63': '5f5fff', '64': '5f8700', '65': '5f875f', 56 | '66': '5f8787', '67': '5f87af', '68': '5f87d7', '69': '5f87ff', '70': '5faf00', 57 | '71': '5faf5f', '72': '5faf87', '73': '5fafaf', '74': '5fafd7', '75': '5fafff', 58 | '76': '5fd700', '77': '5fd75f', '78': '5fd787', '79': '5fd7af', '80': '5fd7d7', 59 | '81': '5fd7ff', '82': '5fff00', '83': '5fff5f', '84': '5fff87', '85': '5fffaf', 60 | '86': '5fffd7', '87': '5fffff', '88': '870000', '89': '87005f', '90': '870087', 61 | '91': '8700af', '92': '8700d7', '93': '8700ff', '94': '875f00', '95': '875f5f', 62 | '96': '875f87', '97': '875faf', '98': '875fd7', '99': '875fff', '100': '878700', 63 | '101': '87875f', '102': '878787', '103': '8787af', '104': '8787d7', 64 | '105': '8787ff', '106': '87af00', '107': '87af5f', '108': '87af87', 65 | '109': '87afaf', '110': '87afd7', '111': '87afff', '112': '87d700', 66 | '113': '87d75f', '114': '87d787', '115': '87d7af', '116': '87d7d7', 67 | '117': '87d7ff', '118': '87ff00', '119': '87ff5f', '120': '87ff87', 68 | '121': '87ffaf', '122': '87ffd7', '123': '87ffff', '124': 'af0000', 69 | '125': 'af005f', '126': 'af0087', '127': 'af00af', '128': 'af00d7', 70 | '129': 'af00ff', '130': 'af5f00', '131': 'af5f5f', '132': 'af5f87', 71 | '133': 'af5faf', '134': 'af5fd7', '135': 'af5fff', '136': 'af8700', 72 | '137': 'af875f', '138': 'af8787', '139': 'af87af', '140': 'af87d7', 73 | '141': 'af87ff', '142': 'afaf00', '143': 'afaf5f', '144': 'afaf87', 74 | '145': 'afafaf', '146': 'afafd7', '147': 'afafff', '148': 'afd700', 75 | '149': 'afd75f', '150': 'afd787', '151': 'afd7af', '152': 'afd7d7', 76 | '153': 'afd7ff', '154': 'afff00', '155': 'afff5f', '156': 'afff87', 77 | '157': 'afffaf', '158': 'afffd7', '159': 'afffff', '160': 'd70000', 78 | '161': 'd7005f', '162': 'd70087', '163': 'd700af', '164': 'd700d7', 79 | '165': 'd700ff', '166': 'd75f00', '167': 'd75f5f', '168': 'd75f87', 80 | '169': 'd75faf', '170': 'd75fd7', '171': 'd75fff', '172': 'd78700', 81 | '173': 'd7875f', '174': 'd78787', '175': 'd787af', '176': 'd787d7', 82 | '177': 'd787ff', '178': 'd7af00', '179': 'd7af5f', '180': 'd7af87', 83 | '181': 'd7afaf', '182': 'd7afd7', '183': 'd7afff', '184': 'd7d700', 84 | '185': 'd7d75f', '186': 'd7d787', '187': 'd7d7af', '188': 'd7d7d7', 85 | '189': 'd7d7ff', '190': 'd7ff00', '191': 'd7ff5f', '192': 'd7ff87', 86 | '193': 'd7ffaf', '194': 'd7ffd7', '195': 'd7ffff', '196': 'ff0000', 87 | '197': 'ff005f', '198': 'ff0087', '199': 'ff00af', '200': 'ff00d7', 88 | '201': 'ff00ff', '202': 'ff5f00', '203': 'ff5f5f', '204': 'ff5f87', 89 | '205': 'ff5faf', '206': 'ff5fd7', '207': 'ff5fff', '208': 'ff8700', 90 | '209': 'ff875f', '210': 'ff8787', '211': 'ff87af', '212': 'ff87d7', 91 | '213': 'ff87ff', '214': 'ffaf00', '215': 'ffaf5f', '216': 'ffaf87', 92 | '217': 'ffafaf', '218': 'ffafd7', '219': 'ffafff', '220': 'ffd700', 93 | '221': 'ffd75f', '222': 'ffd787', '223': 'ffd7af', '224': 'ffd7d7', 94 | '225': 'ffd7ff', '226': 'ffff00', '227': 'ffff5f', '228': 'ffff87', 95 | '229': 'ffffaf', '230': 'ffffd7', '231': 'ffffff', 96 | # Gray-scale range. 97 | '232': '080808', '233': '121212', '234': '1c1c1c', '235': '262626', 98 | '236': '303030', '237': '3a3a3a', '238': '444444', '239': '4e4e4e', 99 | '240': '585858', '241': '626262', '242': '6c6c6c', '243': '767676', 100 | '244': '808080', '245': '8a8a8a', '246': '949494', '247': '9e9e9e', 101 | '248': 'a8a8a8', '249': 'b2b2b2', '250': 'bcbcbc', '251': 'c6c6c6', 102 | '252': 'd0d0d0', '253': 'dadada', '254': 'e4e4e4', '255': 'eeeeee', 103 | }, 104 | 105 | 'header':['header', 'white', 'dark gray', 'bold'], 106 | 'browse':['browse', 'black', 'light gray'], 107 | 'footer':['footer', 'white', 'dark gray', 'bold'], 108 | 'key':['key', 'white', 'dark blue', 'default'], 109 | 'selected':['selected', 'white', 'dark blue', 'bold'], 110 | 'flagged':['flagged', 'black', 'dark green', 'bold'], 111 | 'focus':['focus', 'light gray', 'dark blue', 'standout'], 112 | 'flagged focus':['flagged focus', 'yellow', 'dark cyan', ('bold','standout','underline')], 113 | 114 | 'style':'monokai', 115 | 116 | 'open':'ctrl o', 117 | 'save':'ctrl s', 118 | 'find':'ctrl f', 119 | 'undo':'ctrl q', 120 | 'delline':'ctrl d', 121 | 'prevtab':'meta page up', 122 | 'nexttab':'meta page down', 123 | 'closetab':'ctrl w', 124 | 'terminal':'ctrl g', 125 | 'linenum':'ctrl n', 126 | 'layout':'f1', 127 | 'config':'f5', 128 | 'exit':'ctrl x' 129 | } 130 | 131 | CONFIG['rgb_to_short'] = {v: k for k, v in CONFIG['short_to_rgb'].items()} 132 | 133 | # all the possible widgets that can be defined in the config 134 | palette_items = ['header', 'flagged focus', 'key', 'footer', 'focus', 'selected', 'flagged', 'browse'] 135 | text_options = ['bold', 'underline', 'standout'] 136 | 137 | def read_config(): 138 | counter = 0 139 | new_config = CONFIG # make a copy of the default config 140 | with open(CONFIG_PATH, 'r') as f: 141 | lines = [x.strip('\n') for x in f.readlines()] # strip any unempty lines 142 | 143 | for line in lines: 144 | counter += 1 145 | if line.strip() and line.lstrip()[0] != '#': # skip lines with '#' at beginning 146 | split = line.split(':') # break the line into two parts item and attributes 147 | item = split[0].strip() 148 | if item in palette_items: # if this line is a palette line 149 | attribs = split[1].strip().split(",") 150 | try: # try creating an urwid attr spec 151 | a = urwid.AttrSpec(attribs[0].strip(), attribs[1].strip(), colors=256) 152 | if attribs[2] not in text_options: 153 | attribs[2] = '' 154 | new_config[item] = [item]+[a.foreground, a.background, attribs[2]] # add this to the new config 155 | except: 156 | print("error on line" + str(counter)) 157 | else: # this line isn't a palette lime 158 | if item in new_config: # if this item exists in config dict 159 | new_config[item] = split[1].strip() # redefine it in the dict 160 | 161 | return new_config 162 | 163 | def strip_fname(fname): 164 | # This function gets only the name of the file, without the path 165 | peices = fname.split("/") 166 | return peices[-1] 167 | 168 | def strip_path(fname): 169 | peices = os.path.abspath(fname).split("/") 170 | return "/".join(peices[0:-1]) 171 | 172 | def rgb_to_short(rgb, mapping): 173 | """Find the closest xterm-256 approximation to the given RGB value.""" 174 | # Thanks to Micah Elliott (http://MicahElliott.com) for colortrans.py 175 | rgb = rgb.lstrip('#') if rgb.startswith('#') else rgb 176 | incs = (0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff) 177 | # Break 6-char RGB code into 3 integer vals. 178 | parts = [int(h, 16) for h in re.split(r'(..)(..)(..)', rgb)[1:4]] 179 | res = [] 180 | for part in parts: 181 | i = 0 182 | while i < len(incs)-1: 183 | s, b = incs[i], incs[i+1] # smaller, bigger 184 | if s <= part <= b: 185 | s1 = abs(s - part) 186 | b1 = abs(b - part) 187 | if s1 < b1: closest = s 188 | else: closest = b 189 | res.append(closest) 190 | break 191 | i += 1 192 | res = ''.join([ ('%02.x' % i) for i in res ]) 193 | equiv = mapping[res] 194 | return equiv, res 195 | 196 | # since this is a multi-tab editor, there are a lot of peices of info that need to be stored for each tab 197 | # instead of keeping a bunch of dicts to key the data we need, we can just have one dict and key up an 198 | # object that has all the data we need 199 | # 200 | # DATA: 201 | # file content 202 | # cursor pos 203 | # undo stack 204 | # 205 | class TabInfo(object): 206 | def __init__(self, display): 207 | self.display = display 208 | self.lines = [] 209 | self.cursor = [0, 0] 210 | self.undo = UndoStack(self.display) 211 | 212 | class UndoStack(object): 213 | def __init__(self, display): 214 | self.items = [] 215 | self.display = display 216 | 217 | def undo(self): 218 | # this method undoes the last action from the undo stack. This could be a character added, deleted, a line 219 | # added, a line combined with the previous or the next. It is pretty ugly becuase there are quite a bit of special 220 | # cases to take into consideration like pressing delete at the end of a line, or backspace at the beginning of 221 | # a line. It works though! 222 | if self.items == []: 223 | return 224 | item = self.items.pop(-1) 225 | # item[0] = keys, item[1] = position, item[2] = change 226 | 227 | self.display.listbox.set_focus(item[1][0]) 228 | self.display.listbox.focus.set_edit_pos(item[1][1]) 229 | 230 | if item[2]: # something deleted, so add 231 | if item[0] == 'backspace': # backspace at beginning of line or del at end 232 | self.display.listbox.set_focus(item[1][0]) 233 | self.display.listbox.focus.set_edit_pos(item[1][1]) 234 | self.display.listbox.do_enter() 235 | self.items.pop(-1) 236 | return 237 | elif item[0][-1] == '\n': # if ctrl d is pressed (delete line) 238 | text = TextLine(item[0][:-1], self.display) 239 | self.display.listbox.lines.insert(item[1][0], text) 240 | self.display.line_nums.add() 241 | else: # this is just a normal backspace 242 | self.display.listbox.focus.insert_text(item[0]) 243 | 244 | else: # something added so delete! 245 | if item[0] == 'enter': # undo an enter press 246 | self.display.listbox.set_focus(item[1][0]+1) 247 | self.display.listbox.combine_previous() 248 | return 249 | # otherwise undo a normal character deletion 250 | old_text = self.display.listbox.focus.edit_text 251 | index = int(item[0]) 252 | #self.display.loop.process_input(['backspace']) 253 | if index != len(old_text): 254 | new_text = old_text[:index-1] + old_text[index:] 255 | else: 256 | new_text = old_text[:index-1] 257 | 258 | self.display.listbox.focus.set_edit_text(new_text) 259 | 260 | def log(self, keys, position, change): 261 | #keys: text to insert, or un-insert 262 | #position: where to place or take out text 263 | #change: if removed this is 1 (backspace, delete), if added this is 0 (keypress) 264 | new_item = [keys, position, change] 265 | self.items.append(new_item) 266 | 267 | class FindField(urwid.Edit): 268 | def __init__(self, display, **kwargs): 269 | super().__init__("find: ", **kwargs) 270 | self.display = display 271 | self.index = 0 272 | self.line = 0 273 | self.last = 0 274 | self.on_line = [] 275 | self.history = [] 276 | self.searches = [] 277 | self.search_pos = -1 278 | 279 | def goto(self, word, current): 280 | # this method is some crazy sh*t, I wrote it and I still am not quite sure how it works... 281 | # but since it works, I will not mess with it :-) 282 | found = False 283 | if self.history == [] or self.history[-1] != (self.line, self.index): 284 | self.history.append((self.line, self.index)) 285 | 286 | if len(self.on_line) > 0: 287 | self.index = self.on_line.pop(0) 288 | 289 | else: 290 | for line in self.display.listbox.lines[current[0]:]: 291 | if word in line.edit_text: 292 | for m in re.finditer(word, line.edit_text): 293 | self.on_line.append(m.start()) 294 | 295 | self.index = self.on_line.pop(0) 296 | 297 | self.last = self.line 298 | self.line = self.display.listbox.lines.index(line) 299 | found = True 300 | break 301 | if not found: 302 | self.history.pop(-1) 303 | return 304 | 305 | self.display.top.set_focus('body') 306 | self.display.listbox.lines[self.line].set_edit_pos(self.index) 307 | self.display.listbox.set_focus(self.line) 308 | self.display.update_line_numbers() 309 | 310 | def handle_key(self, key): 311 | # this is where all the keypress for the find bar happen, I don't use the keypress method 312 | # becuase we are never actually focused on the find bar, we are always focused on the text 313 | # editor. We grab the key presses from the text editor and manually feed them into the find 314 | # bar, and running each function according to the keypress we get 315 | 316 | if key in string.printable: # if the key press was a printable character 317 | # we are searching for a new string so we clear everyhting and find the first occurence 318 | self.on_line = [] 319 | self.insert_text(key) 320 | self.goto(self.edit_text, (0,0)) 321 | self.history = [] 322 | 323 | if key == self.display.config['find']: # if ctrl+f is pressed again stop finding 324 | self.display.top.contents['footer'] = (self.display.foot_col, None) 325 | self.searches.insert(0, self.edit_text) 326 | self.set_edit_text("") 327 | self.on_line = [] 328 | if self.display.layout: 329 | self.display.top.contents['footer'] = (self.display.status, None) 330 | else: 331 | for file in self.display.file_names: 332 | # since these files are already open, the list box won't repopulate 333 | # but the tabs will be re-drawn! 334 | self.display.listbox.populate(file) 335 | 336 | self.display.listbox.set_focus(self.line) 337 | self.display.finding = False 338 | #self.display.update_line_numbers() 339 | return 340 | 341 | if key == 'right': # go to the next occurence of the search string 342 | if len(self.edit_text) > 0: 343 | self.goto(self.edit_text, (self.line+1, self.index+1)) 344 | self.display.update_line_numbers(cfrom='above') 345 | 346 | if key == 'left': # go to the previous occurence of the search string 347 | if len(self.history) > 0: # if there is a previous one to go to 348 | previous = self.history.pop(-1) 349 | self.last = self.line 350 | self.line, self.index = previous[0], previous[1] 351 | self.display.listbox.lines[self.line].set_edit_pos(self.index) 352 | self.display.listbox.set_focus(self.line) 353 | self.display.update_line_numbers(cfrom='below') 354 | else: # we are at the first occurence so ensure that nothing wonky happens 355 | self.on_line = [] 356 | self.goto(self.edit_text, (0,0)) 357 | self.history = [] 358 | return 359 | 360 | if key == 'backspace': # this means we are starting a new search 361 | self.on_line = [] 362 | self.set_edit_text(self.edit_text[:-1]) # get the string wihtout the last letter 363 | self.goto(self.edit_text, (0, 0)) 364 | self.history = [] 365 | 366 | #if key == 'up': 367 | #self.search_pos += 1 368 | #if self.search_pos <= len(self.searches) -1: 369 | #self.set_edit_text(self.searches[search_pos]) 370 | 371 | #if key == 'down': 372 | #self.search_pos += 1 373 | #if self.search_pos >= 0: 374 | #self.set_edit_text(self.searches[search_pos]) 375 | #else: 376 | #self.set_edit_text("") 377 | 378 | self.display.listbox.lines[self.line].set_edit_pos(self.index) 379 | self.display.listbox.set_focus(self.line) 380 | self.display.update_line_numbers() 381 | 382 | class TextLine(urwid.Edit): 383 | def __init__(self, text, display, tabsize=4, **kwargs): 384 | super().__init__(edit_text=text.expandtabs(4), wrap='clip', **kwargs) 385 | self.display = display 386 | self.tab = tabsize 387 | self.tokens = [] 388 | self.attribs = [] 389 | self.parsed = False 390 | self.original = text 391 | 392 | def get_text(self): 393 | etext = self.get_edit_text() 394 | # this is done to ensure that parsing is only done if the line is selected and altered 395 | if not self.parsed or (self.display.listbox.focus == self and self.edit_text != self.original): 396 | self.parsed = True 397 | # get the new tokens and return them 398 | self.tokens = self.display.listbox.get_tokens(self.edit_text) 399 | self.attribs = [(tok, len(s)) for tok, s in self.tokens] 400 | self.original = self.edit_text 401 | 402 | return etext, self.attribs 403 | 404 | def get_tabsize(self, pos): 405 | mod = pos % self.tab 406 | size = self.tab - mod 407 | if size == 0: 408 | return self.tab 409 | return size 410 | 411 | def keypress(self, size, key): 412 | # this function updates the status bar and implements tab behaviour 413 | if self.display.finding: 414 | ret = self.display.finder.handle_key(key) 415 | return 416 | 417 | ret = super().keypress(size, key) 418 | 419 | if key == 'tab': 420 | lb = self.display.listbox 421 | pos = lb.focus.edit_pos 422 | tabsize = self.get_tabsize(pos) 423 | for i in range(1, tabsize+1): 424 | cur_tab = self.display.tab_info[self.display.listbox.fname] 425 | cur_tab.undo.log(pos+i, [lb.focus_position, lb.focus.edit_pos], 0) 426 | self.insert_text(' ' * tabsize) 427 | 428 | return ret 429 | 430 | # the line numbers to the left of the editor window is actually a listbox of its own 431 | # it is placed next to the main editor listbox in a column container like so: 432 | # __________________________________ 433 | # | Urwid Column container | 434 | # ---------------------------------- 435 | # |1| text | 436 | # |2| text | 437 | # 438 | # to simulate scrolling I simply set the focus of the line number list box 439 | # to the same focus position as the main editor window. Unfortunately the 440 | # line numbers are copyable so if you try to copy multiple lines they will be 441 | # copied as well :/ so I made them togglable! 442 | 443 | class LineNumbers(urwid.ListBox): 444 | def __init__(self, display): 445 | self.display = display 446 | self.numbers = [] # this is the list the listbox uses 447 | super().__init__(self.numbers) # instantiate the parent class with the list 448 | self._selectable = False # can't be selectable! 449 | self.width = 1 # this tells how wide to make the line num column 450 | # since the line numbers aren't selectable we simulate them being selected 451 | # by changing their color. this field is used to save the last number that 452 | # was highlighted in order to unhighlight it 453 | self.previous = 1 454 | 455 | # each time the user switches tabs the line numbers need to be cleared and 456 | # repopulated. For some reason this is the way this has to be done... 457 | def clear(self): 458 | self.previous = 1 459 | for _ in range(0, len(self.numbers)): 460 | self.sub() 461 | 462 | # this populates the line number listbox , it takes a list as an arg, 463 | # but it really only needs a length.. it calls clear then 464 | # it calls add a bunch to create the new line num 465 | def populate(self, lines): 466 | if not self.display.show_lnums: 467 | return 468 | self.clear() 469 | self.width = 1 470 | for i in range(0, len(lines)): 471 | self.add() 472 | 473 | # this method is called when a new number needs to be added. if the new number is 474 | # a digit longer than the last then the column width is increased! 475 | def add(self): 476 | self.numbers.append(urwid.Text(str(len(self.numbers)+1) + '| ', align='right')) 477 | if len(str(len(self.numbers)-1)) > self.width: 478 | self.width += 1 479 | new_col = urwid.Columns([(self.width+2, self.display.line_nums), self.display.listbox], focus_column=1) 480 | self.display.top.contents['body'] = (new_col, None) 481 | 482 | # this method simply deletes a line from the end of the line numbers 483 | # note: we have to check that we didnt delete the last line or we get an index out of bounds! 484 | def sub(self): 485 | if self.display.listbox.focus_position == len(self.numbers)-1: 486 | self.previous = len(self.numbers)-2 487 | self.numbers.pop(-1) 488 | 489 | class TextList(urwid.ListBox): 490 | def __init__(self, display): 491 | self.display = display 492 | self.lines = [] 493 | super().__init__(self.lines) 494 | self.fname = ' ' 495 | self.short_name = ' ' 496 | self.lexer = None 497 | self.config = self.display.config 498 | 499 | def populate(self, fname): 500 | # this function populates the TextList and creates a new tabs 501 | # The same Textlist is used for each tab but when tabs are switched the 502 | # contents of the tab are grabbed from the files TabInfo instance 503 | if fname not in self.display.file_names: 504 | # grab the lines from the file and strip the newline char 505 | # then iterate through and create a new TextLine object for each line 506 | try: 507 | with open(fname) as f: 508 | content = [x.strip('\n') for x in f.readlines()] 509 | except: 510 | self.redraw_tabs() 511 | return 512 | 513 | # the short name is the file name without a path 514 | self.short_name = strip_fname(fname) 515 | self.display.file_names.append(fname) 516 | new_lines = [] 517 | 518 | # grab the lines from the file and strip the newline char 519 | # Then iterate through and create a new TextLine object for each line 520 | for line in content: 521 | text = TextLine(line, self.display) 522 | new_lines.append(text) 523 | 524 | # if the file is empty then add one empty line so it can be displayed 525 | if len(new_lines) < 1: 526 | text = TextLine(' ', self.display) 527 | new_lines.append(text) 528 | 529 | # create a new tab (button widget) with the correct attributes 530 | self.display.cur_tab = self.display.tab_info[fname] = TabInfo(self.display) 531 | new_tab_info = self.display.tab_info[fname] 532 | new_tab_info.lines = new_lines 533 | new_tab_info.undo = UndoStack(self.display) 534 | new_tab_info.cursor = (0, 0) 535 | 536 | self.display.line_nums.populate(new_lines) 537 | button = urwid.Button(self.short_name) 538 | button._label.align = 'center' 539 | attrib = urwid.AttrMap(button, 'footer') 540 | self.display.tabs.append(attrib) 541 | # switch to the new tab 542 | self.switch_tabs(fname) 543 | else: 544 | self.display.line_nums.populate(self.lines) 545 | self.redraw_tabs() 546 | 547 | def redraw_tabs(self): 548 | # this is done to ensure that the bottom bar is re-drawn after opening files 549 | foot_col = urwid.Columns(self.display.tabs) 550 | foot = urwid.AttrMap(foot_col, 'footer') 551 | if self.display.layout: 552 | self.display.top.contents['header'] = (foot, None) 553 | else: 554 | self.display.top.contents['footer'] = (foot, None) 555 | 556 | def delete_tab(self, fname): 557 | files = self.display.file_names 558 | # make sure there is more than one file open 559 | if len(files) > 1: 560 | index = files.index(fname) 561 | if index < len(files)-1: # if not last in list 562 | new_name = files[index+1] 563 | else: # if last file in list 564 | new_name = files[index-1] 565 | # delete file and contents from master lists 566 | #del self.display.tab_info[fname] 567 | del files[index] 568 | del self.display.tabs[index] 569 | # reset the footer with new tab amount 570 | foot_col = urwid.Columns(self.display.tabs) 571 | foot = urwid.AttrMap(foot_col, 'footer') 572 | if self.display.layout: 573 | self.display.top.contents['header'] = (foot, None) 574 | else: 575 | self.display.top.contents['footer'] = (foot, None) 576 | 577 | self.switch_tabs(new_name) 578 | del self.display.tab_info[fname] 579 | 580 | def get_lexer(self): 581 | # this function gets the lexer depending on the files name 582 | try: 583 | lexer = get_lexer_for_filename(self.short_name) 584 | except pygments.util.ClassNotFound: 585 | lexer = TextLexer() 586 | 587 | lexer.add_filter('tokenmerge') 588 | 589 | return lexer 590 | 591 | def switch_tabs(self, fname): 592 | # this method switches to a tab according to the provided filename 593 | if self.fname != fname: # make sure we aren't already on this tab 594 | cur_tab_info = self.display.cur_tab 595 | if self.fname != ' ': 596 | cur_tab_info = self.display.tab_info[self.fname] 597 | cur_tab_info.lines[:] = self.lines 598 | try: 599 | line = self.focus_position 600 | col = self.focus.edit_pos 601 | cur_tab_info.cursor = (line, col) 602 | except: 603 | cur_tab_info.cursor = (0, 0) 604 | index = self.display.file_names.index(fname) 605 | tabs = self.display.tabs 606 | if self.fname != ' ': #hopefully ensures all lines are saved when switching tabs 607 | cur_tab_info.lines[:] = self.lines 608 | # change tab colors depending on current index 609 | for i in range(0, len(tabs)): 610 | if i != index: 611 | tabs[i].set_attr_map({None:'footer'}) 612 | else: 613 | tabs[i].set_attr_map({None:'selected'}) 614 | # re-assign the current path and filename 615 | self.fname = fname 616 | new_tab_info = self.display.tab_info[self.fname] 617 | self.short_name = strip_fname(fname) 618 | # repopulate the lines list from the line dict in the main class 619 | self.lines[:] = new_tab_info.lines 620 | self.display.line_nums.populate(self.lines[:]) 621 | self.display.top.set_focus('body') 622 | self.lexer = self.get_lexer() 623 | self.set_focus(new_tab_info.cursor[0]) 624 | self.focus.set_edit_pos(new_tab_info.cursor[1]) 625 | self.display.update_line_numbers() 626 | self.display.cur_tab = new_tab_info 627 | else: 628 | # not really needed since no mouse support :/ 629 | self.display.top.set_focus('body') 630 | return 631 | 632 | def do_enter(self): 633 | lead = self.get_leading() 634 | self.split_focus(self.focus_position) 635 | #text = self.focus.edit_text.strip() 636 | self.display.line_nums.add() 637 | self.display.loop.process_input(['down']) 638 | #if text != "": 639 | # self.focus.set_edit_pos(lead) 640 | # self.focus.set_edit_text(' ' * lead + self.focus.edit_text) 641 | 642 | def get_leading(self): 643 | #get the leading whitespace of a line! 644 | line = self.lines[self.focus_position] 645 | return len(self.focus.edit_text) - len(self.focus.edit_text.lstrip()) 646 | 647 | def get_tokens(self, text): 648 | # this function returns the tokens for the provided text 649 | return list(self.lexer.get_tokens(text)) 650 | 651 | def get_line(self, position): 652 | # gets the TextLine object at the given position 653 | # I don't think I use this anywhere 654 | if position < 0: 655 | return None 656 | 657 | elif len(self.lines) > position: 658 | return self.lines[position] 659 | 660 | def next(self, index): 661 | # get the line after the current position 662 | return self.get_line(index+1) 663 | 664 | def previous(self, index): 665 | # get the line after the current position 666 | return self.get_line(index-1) 667 | 668 | def combine_previous(self): 669 | # combine the line with the one before it (used with backspace) 670 | prev = self.previous(self.focus_position) 671 | if prev is None: 672 | return 673 | 674 | focus = self.focus 675 | f_pos = self.focus_position 676 | 677 | p_length = len(prev.edit_text) 678 | # don't mess with this, it is slightly magic, but it works! 679 | prev.set_edit_text(prev.edit_text + focus.edit_text) 680 | self.display.loop.process_input(['up']) 681 | self.focus.set_edit_pos(p_length) 682 | del self.lines[self.focus_position+1] 683 | 684 | def combine_next(self): 685 | # combine the line with the one after it (used with delete) 686 | below = self.next(self.focus_position) 687 | if below is None: 688 | return 689 | self.focus.set_edit_text(self.focus.text + below.text) 690 | del self.lines[self.focus_position+1] 691 | 692 | def split_focus(self, index): 693 | # split the current line at the cursor position (when enter is pressed) 694 | focus = self.lines[index] 695 | position = focus.edit_pos 696 | # make a new edit for split half of the line 697 | new_edit = TextLine(focus.text[position:], self.display) 698 | focus.set_edit_text(focus.text[:position]) 699 | self.focus.set_edit_pos(0) 700 | # insert the newline at the correct index 701 | self.lines.insert(index+1, new_edit) 702 | 703 | def del_line(self): 704 | pos = self.focus_position 705 | del self.lines[pos] 706 | self.focus.set_edit_pos(0) 707 | 708 | def save_file(self): 709 | # this function is used to save the current file. 710 | with open(self.fname, 'w') as f: 711 | for line in self.lines: 712 | f.write(line.edit_text.rstrip()+'\n') 713 | 714 | if self.short_name == 'config.txt': 715 | self.display.configure() 716 | self.display.register_palette() 717 | self.display.loop.screen.clear() 718 | self.display.update_top_bar() 719 | 720 | def keypress(self, size, key): 721 | # this function implements all the keypress behaviour of the text editor window 722 | # some of the keypress strings are grabbed from the config becuase they are customizable 723 | cur_tab = self.display.tab_info[self.fname] 724 | bkey, dkey = '', '' 725 | pos = 0 726 | if self.lines != []: 727 | if len(self.focus.edit_text) > 0: 728 | pos = self.focus.edit_pos 729 | bkey = self.focus.edit_text[pos-1] 730 | if pos < len(self.focus.edit_text): 731 | dkey = self.focus.edit_text[pos] 732 | 733 | ret = super().keypress(size, key) 734 | if self.display.finding: 735 | return 736 | 737 | if key in string.printable: 738 | pos = self.focus.edit_pos 739 | cur_tab.undo.log(pos, [self.focus_position, self.focus.edit_pos], 0) 740 | 741 | if key == 'down' or key == 'up' or key == 'page down' or key == 'page up': 742 | # we need to set the correct coming_from attribute or the line numbers won't 743 | # scroll correctly 744 | if key == 'down' or key == 'page down': 745 | cfrom = 'above' 746 | else: 747 | cfrom = 'below' 748 | self.display.update_line_numbers(cfrom=cfrom) 749 | 750 | elif key == 'backspace': 751 | cur_tab.undo.log(bkey, [self.focus_position, self.focus.edit_pos], 1) 752 | 753 | elif key == 'delete': 754 | cur_tab.undo.log(dkey, [self.focus_position, self.focus.edit_pos], 1) 755 | 756 | elif key == 'enter': 757 | cur_tab.undo.log('enter', [self.focus_position, self.focus.edit_pos], 0) 758 | self.do_enter() 759 | # the next two conditionals use regex to create the Ctrl+arrow behaviour 760 | elif key == "ctrl right" or key == "meta right": 761 | line = self.focus 762 | xpos = line.edit_pos 763 | if xpos == len(line.edit_text) and self.focus_position != len(self.lines)-1: 764 | self.set_focus(self.focus_position+1) 765 | line.set_edit_pos(0) 766 | self.display.update_line_numbers(cfrom='above') 767 | return 768 | 769 | re_word = RE_WORD 770 | m = re_word.search(line.edit_text or "", xpos) 771 | word_pos = len(line.edit_text) if m is None else m.end() 772 | line.set_edit_pos(word_pos) 773 | 774 | elif key == "ctrl left" or key == "meta left": 775 | line = self.focus 776 | xpos = line.edit_pos 777 | if xpos == 0 and self.focus_position != 0: 778 | self.set_focus(self.focus_position-1) 779 | self.focus.set_edit_pos(len(self.focus.edit_text)) 780 | self.display.update_line_numbers(cfrom='below') 781 | return 782 | 783 | re_word = RE_WORD 784 | starts = [m.start() for m in re_word.finditer(line.edit_text or "", 0, xpos)] 785 | word_pos = 0 if len(starts) == 0 else starts[-1] 786 | line.set_edit_pos(word_pos) 787 | 788 | elif key == "ctrl backspace": 789 | line = self.focus 790 | xpos = line.edit_pos 791 | 792 | starts = [m.start() for m in RE_WORD.finditer(line.edit_text or "", 0, xpos)] 793 | word_pos = 0 if len(starts) == 0 else starts[-1] 794 | line.set_edit_text(line.edit_text[word_pos:xpos]) 795 | 796 | # this elif moves to the next tab and if user is on the last tab goes to the frst 797 | elif key == self.config['nexttab']: 798 | index = self.display.file_names.index(self.fname) 799 | if index + 1 < len(self.display.file_names): 800 | next_index = index + 1 801 | else: 802 | next_index = 0 803 | 804 | self.switch_tabs(self.display.file_names[next_index]) 805 | self.display.cur_tab = self.display.tab_info[self.fname] 806 | 807 | elif key == self.config['prevtab']: 808 | index = self.display.file_names.index(self.fname) 809 | if index - 1 >= 0: 810 | next_index = index - 1 811 | else: 812 | next_index = -1 813 | 814 | self.switch_tabs(self.display.file_names[next_index]) 815 | self.display.cur_tab = self.display.tab_info[self.fname] 816 | 817 | # this elif closes the current tab 818 | elif key == self.config['closetab']: 819 | self.delete_tab(self.fname) 820 | self.display.save_tabs() 821 | # this elif saves the current tab 822 | elif key == self.config['save']: 823 | self.save_file() 824 | 825 | return ret 826 | 827 | class MainGUI(object): 828 | def __init__(self): 829 | # set up all the empty lists, dicts and strings needed 830 | # also create the widgets that will be used later 831 | self.cwd = os.getcwd() 832 | self.tab_info = {} 833 | self.file_names = [] 834 | self.palette = [] 835 | self.tabs = [] 836 | self.cur_tab = None 837 | 838 | self.finding = False 839 | self.show_term = False 840 | self.show_lnums = True 841 | 842 | self.state = '' 843 | self.rows = 0 844 | 845 | # this variable represents the UI layout. if this value is False 846 | # then the tabs are on bottom and status is on top. when this value 847 | # is True then the layout is switched! 848 | self.layout = False 849 | 850 | self.configure() 851 | 852 | self.stext = ('header', ['SCUM ', 853 | ('key', 'ESC'), ' Help ', 854 | ('key', 'ctrl s'), ' Save ', 855 | ('key', 'ctrl o'), ' Open ', 856 | ('key', 'ctrl x'), ' Exit' ]) 857 | 858 | self.openfile_stext = ('header', ['Open File: Arrows to navigate ', 859 | ('key', 'Space'), ' Select ', 860 | ('key', 'Enter'), ' Open ']) 861 | # editor state GUI 862 | self.bbar = urwid.Text('') 863 | 864 | self.tbar = urwid.Text(self.stext) 865 | self.tbar_text = self.tbar.text 866 | self.status = urwid.AttrMap(self.tbar, 'header') 867 | 868 | self.listbox = TextList(self) 869 | urwid.AttrMap(self.listbox, 'body') 870 | 871 | self.line_nums = LineNumbers(self) 872 | self.body_col = urwid.Columns([(3, self.line_nums), self.listbox], focus_column=1) 873 | 874 | self.foot_col = urwid.Columns(self.tabs) 875 | self.foot = urwid.AttrMap(self.foot_col, 'footer') 876 | 877 | self.finder = FindField(self) 878 | self.fedit = urwid.AttrMap(self.finder, 'footer') 879 | 880 | # openfile state GUI 881 | self.new_files = [] 882 | self.openfile_top = urwid.Text(self.openfile_stext) 883 | self.oftbar = urwid.AttrMap(self.openfile_top, 'header') 884 | 885 | self.browser = urwid.TreeListBox(urwid.TreeWalker(DirectoryNode(self.cwd, self))) 886 | self.browser.offset_rows = 1 887 | urwid.AttrWrap(self.browser, 'browse') 888 | 889 | self.openfile_bottom = urwid.Text(' ') 890 | self.ofbbar = urwid.AttrWrap(self.openfile_bottom, 'footer') 891 | 892 | self.top = urwid.Frame(self.body_col, header=self.status, footer=self.foot_col) 893 | self.update_top_bar() 894 | 895 | self.state = 'editor' 896 | 897 | self.term = ToggleTerm(self) 898 | self.termbox = urwid.LineBox(self.term) 899 | 900 | self.pile = urwid.Pile([self.top]) 901 | self.open_tabs() 902 | 903 | if len(self.tabs) == 0: 904 | self.listbox.populate(START_PATH) 905 | 906 | def display(self): 907 | # this method starts the main loop and such 908 | self.loop = urwid.MainLoop(self.pile, 909 | self.palette, 910 | handle_mouse = False, 911 | unhandled_input = self.keypress, 912 | pop_ups = True) 913 | self.loop.screen.set_terminal_properties(colors=256) 914 | self.register_palette() 915 | 916 | self.term.main_loop = self.loop 917 | try: 918 | self.loop.run() 919 | except: 920 | return 'failure' 921 | 922 | with open(TABS_PATH, 'a') as f: 923 | f.write(str(self.layout)) 924 | 925 | return 'exit' 926 | 927 | def configure(self): 928 | # this method is run to re-parse the config and set the palette 929 | self.config = read_config() 930 | 931 | self.style = get_style_by_name(self.config['style']) 932 | 933 | for item in palette_items: 934 | self.palette.append(tuple(self.config[item])) 935 | 936 | def update_top_bar(self): 937 | self.stext = ('header', ['SCUM ', 938 | ('key', 'ESC'), ' Help ', 939 | ('key', self.config['save']), ' Save ', 940 | ('key', self.config['open']), ' Open ', 941 | ('key', self.config['exit']), ' Exit' ]) 942 | 943 | self.tbar = urwid.Text(self.stext) 944 | self.status = urwid.AttrMap(self.tbar, 'header') 945 | 946 | self.top.contents['header'] = (self.status, None) 947 | 948 | def update_status(self): 949 | # this method is runs to update the top bar depending on the current state 950 | col, self.rows = self.loop.screen.get_cols_rows() 951 | 952 | selected = '' 953 | for f in self.new_files: 954 | selected += strip_fname(f) + ' | ' 955 | extra = col - len(selected) - 1 956 | self.ofbbar.set_text(selected) 957 | 958 | def update_line_numbers(self, cfrom=None): 959 | if self.show_lnums: 960 | focus_pos = self.listbox.focus_position 961 | lnums = self.line_nums 962 | lnums.set_focus(focus_pos, coming_from=cfrom) 963 | foc = lnums.numbers[focus_pos] 964 | 965 | prev = lnums.numbers[lnums.previous] 966 | lnums.numbers[lnums.previous] = urwid.Text(prev.text, align='right') 967 | 968 | lnums.numbers[focus_pos] = urwid.AttrWrap(foc, 'key') 969 | lnums.previous = focus_pos 970 | 971 | def switch_states(self, state): 972 | # this method is run to switch states, it reassigns what content is in the Frame 973 | if state == 'editor': 974 | self.top.contents['header'] = (self.status, None) 975 | self.top.contents['body'] = (self.body_col, None) 976 | self.top.contents['footer'] = (self.foot_col, None) 977 | if self.layout: 978 | self.top.contents['footer'] = (self.status, None) 979 | self.top.contents['header'] = (self.foot_col, None) 980 | 981 | elif state == 'openfile': 982 | path = strip_path(self.listbox.fname) 983 | self.browser = urwid.TreeListBox(urwid.TreeWalker(DirectoryNode(path, self))) 984 | self.top.contents['header'] = (self.oftbar, None) 985 | self.top.contents['body'] = (self.browser, None) 986 | self.top.contents['footer'] = (self.ofbbar, None) 987 | self.ofbbar.set_text('') 988 | 989 | self.state = state 990 | 991 | def toggle_line_numbers(self): 992 | self.show_lnums = not self.show_lnums 993 | if self.show_lnums: 994 | self.line_nums.populate(self.listbox.lines) 995 | self.body_col = urwid.Columns([(self.line_nums.width+2, self.line_nums), self.listbox], focus_column=1) 996 | else: 997 | self.body_col.contents.pop(0) 998 | 999 | self.top.contents['body'] = (self.body_col, None) 1000 | self.update_line_numbers() 1001 | 1002 | def toggle_term(self): 1003 | self.show_term = not self.show_term 1004 | if self.show_term: 1005 | cols, rows = self.loop.screen.get_cols_rows() 1006 | height = min(25, rows/2) 1007 | pile = self.pile 1008 | pile.contents.append((self.termbox, pile.options(height_type='given', height_amount=height))) 1009 | pile.focus_position = 1 1010 | self.term.main_loop = self.loop 1011 | else: 1012 | self.pile.contents.pop(-1) 1013 | 1014 | def toggle_layout(self): 1015 | self.layout = not self.layout 1016 | content = self.top.contents 1017 | content['header'], content['footer'] = content['footer'], content['header'] 1018 | 1019 | def open_tabs(self): 1020 | # this method reads the saved tabs from the data file and opens the files on start up 1021 | with open(TABS_PATH, 'r') as f: 1022 | lines = [line.strip('\n') for line in f.readlines()] 1023 | if len(lines) == 0: 1024 | with open(TABS_PATH, 'w') as f: 1025 | f.write(START_PATH + '\n') 1026 | f.write('False') 1027 | self.listbox.populate(START_PATH) 1028 | 1029 | for line in lines: 1030 | if line != lines[-1]: 1031 | self.listbox.populate(line) 1032 | else: 1033 | if line == 'True': 1034 | self.toggle_layout() 1035 | 1036 | self.save_tabs() 1037 | 1038 | def save_tabs(self): 1039 | # this method is run whenever a tab is opened or closed, it writes 1040 | # the current open file names to a data file to be read on start up 1041 | with open(TABS_PATH, 'w') as f: 1042 | for tab in self.file_names: 1043 | f.write(tab + '\n') 1044 | 1045 | def keypress(self, k): 1046 | # this method handles any keypresses that are unhandled by other widgets 1047 | foc = self.top.focus_position 1048 | # these conditionals will only be run if other widgets didnt handle them already, if right 1049 | # or left keypresses go unhandled we know we are at the beginning or end of a line 1050 | if k == 'left': 1051 | self.loop.process_input(['up']) 1052 | self.listbox.focus.set_edit_pos(len(self.listbox.focus.edit_text)) 1053 | 1054 | elif k == 'right' and self.state != 'openfile': 1055 | self.loop.process_input(['down']) 1056 | self.listbox.focus.set_edit_pos(0) 1057 | 1058 | elif k == 'backspace': 1059 | # Right here we run combine_previous() to combine the 1060 | # current text line with the one prior. This function 1061 | # returns the length of the previous line. We have to wait to set the edit_pos 1062 | # because self.loop.process_input changes the edit_pos 1063 | if self.listbox.focus_position != 0: 1064 | self.line_nums.sub() 1065 | self.listbox.combine_previous() 1066 | self.cur_tab.undo.log('backspace', [self.listbox.focus_position, self.listbox.focus.edit_pos], 1) 1067 | 1068 | elif k == 'delete': 1069 | self.listbox.combine_next() 1070 | self.cur_tab.undo.log('backspace', [self.listbox.focus_position, self.listbox.focus.edit_pos], 1) 1071 | 1072 | # this keypress opens up the configuration file so it can be edited 1073 | elif k == self.config['config']: 1074 | self.listbox.populate(CONFIG_PATH) 1075 | 1076 | # this keypress saves the changes of the config file and updates everything 1077 | 1078 | elif k == self.config['delline']: 1079 | if self.listbox.focus_position == 0 and len(self.listbox.lines) == 1: 1080 | self.listbox.focus.set_edit_text('') 1081 | return 1082 | 1083 | self.cur_tab.undo.log(self.listbox.focus.edit_text+'\n', [self.listbox.focus_position, 0], 1) 1084 | self.line_nums.sub() 1085 | self.listbox.del_line() 1086 | 1087 | elif k == self.config['layout']: 1088 | self.toggle_layout() 1089 | 1090 | elif k == self.config['linenum']: 1091 | self.toggle_line_numbers() 1092 | 1093 | elif k == self.config['terminal']: 1094 | self.toggle_term() 1095 | 1096 | elif k == self.config['open']: 1097 | self.new_files = [] 1098 | self.browser = urwid.TreeListBox(urwid.TreeWalker(DirectoryNode(self.cwd, self))) 1099 | self.browser.offset_rows = 1 1100 | self.switch_states('openfile') 1101 | 1102 | # this keypress only registers when enter is pressed in the open file state, otherwise the 1103 | # editor would have handled it. This means we need to open the selected files if there are any 1104 | # and set the state back to editor mode 1105 | 1106 | elif k == 'enter': 1107 | if self.state == 'openfile': 1108 | self.switch_states('editor') 1109 | if len(self.new_files) > 0: 1110 | for fname in self.new_files: 1111 | self.listbox.populate(fname) 1112 | else: 1113 | self.listbox.populate(self.file_names[0]) 1114 | 1115 | self.new_files = [] 1116 | self.save_tabs() 1117 | 1118 | elif k == self.config['find']: 1119 | self.finding = True 1120 | self.top.contents['footer'] = (self.fedit, None) 1121 | 1122 | elif k == self.config['undo']: 1123 | self.cur_tab.undo.undo() 1124 | self.update_line_numbers() 1125 | 1126 | elif k == 'ctrl x': 1127 | # get outta here! 1128 | raise urwid.ExitMainLoop() 1129 | 1130 | # user needs help... so give them this help file I guess. 1131 | elif k == 'esc': 1132 | self.listbox.populate(HELP_PATH) 1133 | 1134 | def register_palette(self): 1135 | """Converts pygmets style to urwid palatte""" 1136 | default = 'default' 1137 | palette = list(self.palette) 1138 | mapping = CONFIG['rgb_to_short'] 1139 | for tok in self.style.styles.keys(): 1140 | for t in tok.split()[::-1]: 1141 | st = self.style.styles[t] 1142 | if '#' in st: 1143 | break 1144 | if '#' not in st: 1145 | st = '' 1146 | st = st.split() 1147 | st.sort() # '#' comes before '[A-Za-z0-9]' 1148 | if len(st) == 0: 1149 | c = default 1150 | elif st[0].startswith('bg:'): 1151 | c = default 1152 | elif len(st[0]) == 7: 1153 | c = 'h' + rgb_to_short(st[0][1:], mapping)[0] 1154 | elif len(st[0]) == 4: 1155 | c = 'h' + rgb_to_short(st[0][1]*2 + st[0][2]*2 + st[0][3]*2, mapping)[0] 1156 | else: 1157 | c = default 1158 | a = urwid.AttrSpec(c, default, colors=256) 1159 | row = (tok, default, default, default, a.foreground, default) 1160 | palette.append(row) 1161 | self.loop.screen.register_palette(palette) 1162 | 1163 | --------------------------------------------------------------------------------