├── temp.jpg ├── templates └── index.html ├── example.py ├── .gitignore ├── README.md └── websktop.py /temp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juancarlospaco/websktop/HEAD/temp.jpg -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 |

Hello World !. This is a Demo App of ImportD. 2 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from importd import d 2 | import websktop 3 | 4 | @d("/") 5 | def idx(request): 6 | return "index.html" 7 | 8 | if __name__ in "__main__": 9 | websktop.main(d) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | 39 | # Custom 40 | .directory 41 | secret.txt 42 | db.sqlite 43 | db.sqlite3 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | websktop 2 | ======== 3 | 4 | *"Kill the line betweeen Desktop, Mobile and Web"* 5 | 6 | 7 | [![GPL License](http://img.shields.io/badge/license-GPL-blue.svg?style=plastic)](http://opensource.org/licenses/GPL-3.0) [![LGPL License](http://img.shields.io/badge/license-LGPL-blue.svg?style=plastic)](http://opensource.org/licenses/LGPL-3.0) [![Python Version](https://img.shields.io/badge/Python-3-brightgreen.svg?style=plastic)](http://python.org) [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif "Donate with or without Credit Card")](http://goo.gl/cB7PR) 8 | 9 | 10 | ![screenshot](https://raw.githubusercontent.com/juancarlospaco/websktop/master/temp.jpg "Websktop App") 11 | 12 | 13 | **MetaFramework for Desktop Apps.** 14 | 15 | - Using [Python3](https://www.python.org) + [Qt5](http://qt.io) + [Django](https://www.djangoproject.com/). 16 | - [Material Design Lite](http://www.getmdl.io) compatible. 17 | - [Twitter Bootstrap 3+](http://getbootstrap.com) compatible. 18 | - [Brython 3](brython.info) compatible. 19 | - Single file standalone executable output is possible via [`*.PYZ` for Python3](https://www.python.org/dev/peps/pep-0441/). 20 | - Just like [Node-webkit](https://github.com/rogerwang/node-webkit "Node-webkit") but using Python instead of JavaScript. 21 | - Inspired by [WebUI](https://github.com/Widdershin/WebUI/ "WebUI"). 22 | - It will also run on any platforms where Qt and ImportD can run. 23 | - Set its own Process name and show up on Process lists, GUI Customization via CSS. 24 | - Sets Smooth CPU usage, Single Instance via Sockets. 25 | - Custom Built Window with Semi-Transparent with particles and Rounded Corners. 26 | 27 | 28 | # Install permanently on the system: 29 | 30 | **PIP:** *(Recommended!)* 31 | ``` 32 | sudo pip3 install importd 33 | 34 | git clone https://github.com/juancarlospaco/websktop.git 35 | cd websktop 36 | 37 | python3 example.py 38 | ``` 39 | - TODO: More info!!!. 40 | 41 | 42 | # Why?: 43 | 44 | - I wanted a quick and simple way to create cute Material Design Lite Apps for Desktop with the Power of Django. 45 | - Like a Node-Webkit or Atom-Shell but using Python 3 instead of JavaScript. 46 | 47 | 48 | # Requisites: 49 | 50 | - **Linux / Os X** *(No MS Window YET)* 51 | - [Python 3.x](https://www.python.org "Python Homepage") *(No Python2)* 52 | - [PyQt 5.x](http://www.riverbankcomputing.co.uk/software/pyqt/download5 "PyQt5 Homepage") *(No Qt4)* 53 | - [ImportD](https://github.com/amitu/importd "ImportD") 54 | 55 | **Optionals:** 56 | - [QDarkStyleSheet *(CSS base for Qt5)*](https://github.com/ColinDuquesnoy/QDarkStyleSheet#qdarkstylesheet) `sudo pip3 install qdarkstyle` 57 | 58 | 59 | # Files Description 60 | - `example.py` Hello World Example App. 61 | - `/templates/` HTML Templates for Hello World App. 62 | - `websktop.py` The only file actually needed, where the magic happens. 63 | 64 | 65 | # References 66 | - http://reinout.vanrees.org/weblog/2015/06/02/09-django-desktop.html *(Too Complex, Not Code, just a comment)* 67 | - https://github.com/tominsam/djangokit *(Mac-Only, too Complex)* 68 | - https://github.com/scaphilo/dbuilder#dbuilder *(Windows-Only, too Complex, Dead)* 69 | 70 | 71 | # Coding Style Guide: 72 | 73 | - Lint, [PEP-8](https://www.python.org/dev/peps/pep-0008), [PEP-257](https://www.python.org/dev/peps/pep-0257), [PyLama](https://github.com/klen/pylama#-pylama), [iSort](https://github.com/timothycrosley/isort) must Pass Ok. `pip install pep8 pep257 pylama isort` 74 | - If theres any kind of Tests, they must Pass Ok, if theres no Tests, its ok, if Tests provided, is even better. 75 | 76 | 77 | # Contributors: 78 | 79 | - **Please Star this Repo on Github !**, it helps to show up faster on searchs. 80 | - **Ad-Hocracy Meritocracy**: 3 Pull Requests Merged on Master you become Repo Admin. *Join us!* 81 | - [Help](https://help.github.com/articles/using-pull-requests) and more [Help](https://help.github.com/articles/fork-a-repo) and Interactive Quick [Git Tutorial](https://try.github.io). 82 | 83 | 84 | # Licence: 85 | 86 | - GNU GPL Latest Version *AND* GNU LGPL Latest Version *AND* any Licence [YOU Request via Bug Report](https://github.com/juancarlospaco/unicodemoticon/issues/new). 87 | 88 | 89 | # Ethics and Humanism Policy: 90 | - May this FLOSS be always Pristine and Clean, No AdWare, No Spamm, No BundleWare, No Infomercial, No MalWare. 91 | - This project is [LGBTQQIAAP friendly](http://www.urbandictionary.com/define.php?term=LGBTQQIAAP "Whats LGBTQQIAAP"). 92 | 93 | 94 | Donate, Charityware : 95 | --------------------- 96 | 97 | - [Charityware](https://en.wikipedia.org/wiki/Donationware) is a licensing model that supplies fully operational unrestricted software to the user and requests an optional donation be paid to a third-party beneficiary non-profit. The amount of donation may be left to the discretion of the user. Its GPL-compatible and Enterprise ready. 98 | - If you want to Donate please [click here](http://www.icrc.org/eng/donations/index.jsp) or [click here](http://www.atheistalliance.org/support-aai/donate) or [click here](http://www.msf.org/donate) or [click here](http://richarddawkins.net/) or [click here](http://www.supportunicef.org/) or [click here](http://www.amnesty.org/en/donate) 99 | -------------------------------------------------------------------------------- /websktop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | """Websktop.""" 6 | 7 | 8 | __version__ = '1.0.0' 9 | __license__ = ' GPLv3+ LGPLv3+ ' 10 | __author__ = ' Juan Carlos ' 11 | __email__ = ' juancarlospaco@gmail.com ' 12 | __url__ = 'https://github.com/juancarlospaco/websktop' 13 | __source__ = ('https://raw.githubusercontent.com/websktop/' 14 | 'websktop/master/websktop.py') 15 | 16 | 17 | import os 18 | import signal 19 | import socket 20 | import sys 21 | from ctypes import byref, cdll, create_string_buffer 22 | from getpass import getuser 23 | from platform import platform, python_version 24 | from random import randint 25 | from tempfile import mkdtemp 26 | from webbrowser import open_new_tab 27 | 28 | from PyQt5.QtCore import Qt, QThread, QTimer, QUrl 29 | from PyQt5.QtGui import QColor, QCursor, QIcon, QPainter, QPalette, QPen 30 | from PyQt5.QtNetwork import QNetworkProxyFactory 31 | from PyQt5.QtWebKit import QWebSettings 32 | from PyQt5.QtWebKitWidgets import QWebView 33 | from PyQt5.QtWidgets import (QApplication, QDesktopWidget, QFileDialog, 34 | QFontDialog, QLabel, QMainWindow, QMenu, 35 | QMessageBox, QShortcut, QStyle, QToolBar, QWidget) 36 | 37 | try: 38 | import resource 39 | except ImportError: 40 | resource = None 41 | try: 42 | import qdarkstyle # https://github.com/ColinDuquesnoy/QDarkStyleSheet 43 | except ImportError: # sudo pip3 install qdarkstyle 44 | qdarkstyle = None # 100% optional, but cute. 45 | 46 | 47 | HTML_PLACEHOLDER = """ 48 |

Powered by ImportD, Django, Qt5, Python3.""" 49 | 50 | 51 | ############################################################################## 52 | # GUI 53 | 54 | 55 | class WebView(QWebView): 56 | 57 | """Main QWebView.""" 58 | 59 | def __init__(self, parent=None): 60 | """Initialize QWebView.""" 61 | super(WebView, self).__init__(parent) 62 | self.setStyleSheet("background-color: transparent") 63 | QNetworkProxyFactory.setUseSystemConfiguration(True) 64 | settings, temporary_directory = self.settings(), mkdtemp() 65 | settings.setDefaultTextEncoding("utf-8") 66 | settings.setIconDatabasePath(temporary_directory) 67 | settings.setLocalStoragePath(temporary_directory) 68 | settings.setOfflineStoragePath(temporary_directory) 69 | settings.setMaximumPagesInCache(settings.maximumPagesInCache() * 2) 70 | settings.setOfflineWebApplicationCachePath(temporary_directory) 71 | settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True) 72 | settings.setAttribute(QWebSettings.LocalStorageEnabled, True) 73 | settings.setAttribute(QWebSettings.OfflineStorageDatabaseEnabled, True) 74 | settings.setAttribute(QWebSettings.PluginsEnabled, True) 75 | settings.setAttribute(QWebSettings.DnsPrefetchEnabled, True) 76 | settings.setAttribute(QWebSettings.JavascriptCanOpenWindows, True) 77 | settings.setAttribute(QWebSettings.JavascriptCanCloseWindows, True) 78 | settings.setAttribute(QWebSettings.JavascriptCanAccessClipboard, True) 79 | settings.setAttribute(QWebSettings.SpatialNavigationEnabled, True) 80 | settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) 81 | settings.setAttribute(QWebSettings.LocalContentCanAccessFileUrls, True) 82 | settings.setAttribute(QWebSettings.CSSGridLayoutEnabled, True) 83 | settings.setAttribute(QWebSettings.ScrollAnimatorEnabled, True) 84 | settings.setAttribute( 85 | QWebSettings.LocalContentCanAccessRemoteUrls, True) 86 | settings.setAttribute( 87 | QWebSettings.OfflineWebApplicationCacheEnabled, True) 88 | self.setHtml(HTML_PLACEHOLDER.strip(), QUrl("http://127.0.0.1:8000/")) 89 | settings.setUserStyleSheetUrl(QUrl( # HTML Transparent Background. 90 | "data:text/css;charset=utf-8;base64," 91 | "Ym9keXtiYWNrZ3JvdW5kOnRyYW5zcGFyZW50fQ==")) 92 | self.page().setForwardUnsupportedContent(True) 93 | 94 | 95 | class MainWindow(QMainWindow): 96 | 97 | """Main Window.""" 98 | 99 | def __init__(self, parent=None): 100 | """Initialize MainWindow.""" 101 | super(MainWindow, self).__init__(parent) 102 | self.ram_info, self.ram_timer = QLabel(self), QTimer(self) 103 | self.menubar, self.view = QMenu(self), WebView(self) 104 | self.ram_timer.timeout.connect(self.update_statusbar) 105 | self.ram_timer.start(60000) # Every 60 seconds 106 | self.statusBar().insertPermanentWidget(0, self.ram_info) 107 | self.setMinimumSize(640, 480) 108 | self.setMaximumSize(QDesktopWidget().screenGeometry().width() * 2, 109 | QDesktopWidget().screenGeometry().height() * 2) 110 | self.palette().setBrush(QPalette.Base, Qt.transparent) 111 | self.setPalette(self.palette()) # Transparent palette 112 | self.setAttribute(Qt.WA_OpaquePaintEvent, False) # no opaque paint 113 | self.setAttribute(Qt.WA_TranslucentBackground, True) # translucent 114 | QShortcut("Ctrl+q", self, activated=self.close) 115 | self.make_toolbar() 116 | self.make_menubar() 117 | self.update_statusbar() 118 | self.setCentralWidget(self.view) 119 | if qdarkstyle: 120 | self.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) 121 | 122 | def paintEvent(self, event): 123 | """Paint transparent background,animated pattern,background text.""" 124 | painter, font = QPainter(self), self.font() 125 | painter.fillRect(event.rect(), Qt.transparent) # fill transparent rect 126 | painter.setPen(QPen(QColor(randint(9, 255), randint(9, 255), 255))) 127 | painter.rotate(30) # Rotate painter ~30 Degree 128 | font.setBold(True) # Set painter Font for text 129 | font.setPixelSize(100) 130 | painter.setFont(font) 131 | painter.drawText(99, 99, "Python Qt") # draw the background text 132 | painter.rotate(-30) # Rotate -30 the QPen back 133 | painter.setPen(Qt.NoPen) # set the pen to no pen 134 | painter.setBrush(QColor("black")) # Background Color 135 | painter.setOpacity(0.9) # Background Opacity 136 | painter.drawRoundedRect(self.rect(), 25, 25) # Back Rounded Borders 137 | for i in range(2048): # animated random dots background pattern 138 | x = randint(10, self.size().width() - 10) 139 | y = randint(10, self.size().height() - 10) 140 | painter.setPen(QPen(QColor(randint(9, 255), randint(9, 255), 255))) 141 | painter.drawPoint(x, y) 142 | QMainWindow.paintEvent(self, event) 143 | 144 | def make_toolbar(self, list_of_actions=None): 145 | """Make or Update the main Tool Bar.""" 146 | self.toolbar = QToolBar(self) 147 | self.left_spacer, self.right_spacer = QWidget(self), QWidget(self) 148 | self.left_spacer.setSizePolicy(1 | 2, 1 | 2) # Expanding, Expanding 149 | self.right_spacer.setSizePolicy(1 | 2, 1 | 2) # Expanding, Expanding 150 | self.toolbar.addAction("Menu", 151 | lambda: self.menubar.exec_(QCursor.pos())) 152 | self.toolbar.addWidget(self.left_spacer) 153 | self.toolbar.addSeparator() 154 | self.toolbar.addAction(QIcon.fromTheme("help-contents"), 155 | "Help and Docs", lambda: open_new_tab(__url__)) 156 | self.toolbar.addAction(QIcon.fromTheme("help-about"), "About Qt 5", 157 | lambda: QMessageBox.aboutQt(self)) 158 | self.toolbar.addAction(QIcon.fromTheme("help-about"), "About Python 3", 159 | lambda: open_new_tab('http://python.org/about')) 160 | self.toolbar.addAction(QIcon.fromTheme("application-exit"), 161 | "Quit", self.close) 162 | self.toolbar.addSeparator() 163 | if list_of_actions and len(list_of_actions): 164 | for action in list_of_actions: # if list_of_actions, add actions. 165 | self.toolbar.addAction(action) 166 | self.toolbar.addSeparator() 167 | self.toolbar.addWidget(self.right_spacer) 168 | self.addToolBar(self.toolbar) 169 | if sys.platform.startswith("win"): 170 | self.toolbar.hide() # windows dont have QIcon.fromTheme,so hide. 171 | return self.toolbar 172 | 173 | def make_menubar(self, list_of_actions=None): 174 | """Make or Update the main Tool Bar.""" 175 | self.menuBar().addMenu("&File").addAction("Exit", self.close) 176 | self.menubar.addMenu("&File").addAction("Exit", self.close) 177 | viewMenu = self.menuBar().addMenu("&View") 178 | viewMenu.addAction( 179 | "Toggle ToolBar", lambda: 180 | self.toolbar.setVisible(not self.toolbar.isVisible())) 181 | viewMenu.addAction( 182 | "Toggle StatusBar", lambda: 183 | self.statusBar().setVisible(not self.statusBar().isVisible())) 184 | viewMenu.addAction( 185 | "Toggle MenuBar", lambda: 186 | self.menuBar().setVisible(not self.menuBar().isVisible())) 187 | viewMenu2 = self.menubar.addMenu("&View") 188 | for action in viewMenu.actions(): 189 | viewMenu2.addAction(action) 190 | windowMenu = self.menuBar().addMenu("&Window") 191 | windowMenu.addAction("Minimize", lambda: self.showMinimized()) 192 | windowMenu.addAction("Maximize", lambda: self.showMaximized()) 193 | windowMenu.addAction("Restore", lambda: self.showNormal()) 194 | windowMenu.addAction("Full-Screen", lambda: self.showFullScreen()) 195 | windowMenu.addAction("Center", lambda: self.center()) 196 | windowMenu.addAction("Top-Left", lambda: self.move(0, 0)) 197 | windowMenu.addAction("To Mouse", lambda: self.move(QCursor.pos())) 198 | windowMenu.addSeparator() 199 | windowMenu.addAction("Increase size", lambda: self.resize( 200 | self.size().width() * 1.5, self.size().height() * 1.5)) 201 | windowMenu.addAction("Decrease size", lambda: self.resize( 202 | self.size().width() // 1.5, self.size().height() // 1.5)) 203 | windowMenu.addAction("Minimum size", lambda: 204 | self.resize(self.minimumSize())) 205 | windowMenu.addAction("Maximum size", lambda: 206 | self.resize(self.maximumSize())) 207 | windowMenu.addAction("Horizontal Wide", lambda: self.resize( 208 | self.maximumSize().width(), self.minimumSize().height())) 209 | windowMenu.addAction("Vertical Tall", lambda: self.resize( 210 | self.minimumSize().width(), self.maximumSize().height())) 211 | windowMenu.addSeparator() 212 | windowMenu.addAction("Disable Resize", lambda: 213 | self.setFixedSize(self.size())) 214 | windowMenu2 = self.menubar.addMenu("&Window") 215 | for action in windowMenu.actions(): 216 | windowMenu2.addAction(action) 217 | optionMenu = self.menuBar().addMenu("&Options") 218 | optionMenu.addAction("Set Interface Font...", lambda: 219 | self.setFont(QFontDialog.getFont(self)[0])) 220 | optionMenu.addAction("Load CSS Skin...", lambda: 221 | self.setStyleSheet(self.skin())) 222 | optionMenu.addAction("Take ScreenShoot...", lambda: self.grab().save( 223 | QFileDialog.getSaveFileName(self, "Save", os.path.expanduser("~"), 224 | "(*.png) PNG image file", "png")[0])) 225 | optionMenu2 = self.menubar.addMenu("&Options") 226 | for action in optionMenu.actions(): 227 | optionMenu2.addAction(action) 228 | helpMenu = self.menuBar().addMenu("&Help") 229 | helpMenu.addAction("About Qt 5", lambda: QMessageBox.aboutQt(self)) 230 | helpMenu.addAction("About Python 3", lambda: 231 | open_new_tab('https://www.python.org/about')) 232 | helpMenu.addSeparator() 233 | if sys.platform.startswith('linux'): 234 | helpMenu.addAction("View Source Code", 235 | lambda: open_new_tab(__file__)) 236 | helpMenu.addAction("View GitHub Repo", lambda: open_new_tab(__url__)) 237 | helpMenu.addAction("Report Bugs", lambda: 238 | open_new_tab(__url__ + '/issues?state=open')) 239 | helpMenu2 = self.menubar.addMenu("&Help") 240 | for action in helpMenu.actions(): 241 | helpMenu2.addAction(action) 242 | return self.menuBar() 243 | 244 | def update_statusbar(self, custom_message=None): 245 | """Make or Update the Status Bar.""" 246 | statusbar = self.statusBar() 247 | if resource: 248 | ram_use = int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * 249 | resource.getpagesize() / 1024 / 1024) 250 | ram_byt = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') 251 | ram_all = int(ram_byt / 1024 / 1024) 252 | self.ram_info.setText("{0} / {1} Mb".format(ram_use, ram_all)) 253 | self.ram_info.setToolTip( 254 | "{0} of {1} MegaBytes of RAM Memory.".format(ram_use, ram_all)) 255 | if custom_message and len(custom_message): 256 | return statusbar.showMessage(custom_message) 257 | return statusbar.showMessage(__doc__) 258 | 259 | def skin(self, filename=None): 260 | """Open QSS from filename,if no QSS return None,if no filename ask.""" 261 | if not filename: 262 | filename = str(QFileDialog.getOpenFileName( 263 | self, __doc__ + " - Open QSS Skin", os.path.expanduser("~"), 264 | "CSS Cascading Style Sheet for Qt 5 (*.qss);;All (*.*)")[0]) 265 | if filename and os.path.isfile(filename): 266 | with open(filename, 'r', encoding="utf-8-sig") as file_to_read: 267 | text = file_to_read.read().strip() 268 | if text: 269 | return text 270 | 271 | def center(self): 272 | """Center and resize the window.""" 273 | self.showNormal() 274 | self.resize(QDesktopWidget().screenGeometry().width() // 1.25, 275 | QDesktopWidget().screenGeometry().height() // 1.25) 276 | qr = self.frameGeometry() 277 | qr.moveCenter(QDesktopWidget().availableGeometry().center()) 278 | return self.move(qr.topLeft()) 279 | 280 | def closeEvent(self, event): 281 | """Ask to Quit.""" 282 | return event.accept() if QMessageBox.question( 283 | self, "Close", "

Quit ?.", QMessageBox.Yes | QMessageBox.No, 284 | QMessageBox.No) == QMessageBox.Yes else event.ignore() 285 | 286 | 287 | ############################################################################## 288 | # Helpers 289 | 290 | 291 | def make_post_execution_message(app: str=__doc__.splitlines()[0].strip()): 292 | """Simple Post-Execution Message with information about RAM and Time. 293 | 294 | >>> make_post_execution_message() >= 0 295 | True 296 | """ 297 | ram_use = int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * 298 | resource.getpagesize() / 1024 / 1024 if resource else 0) 299 | ram_all = int(os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') 300 | / 1024 / 1024) 301 | print("Total Maximum RAM Memory used: ~{0} of {1} MegaBytes.".format( 302 | ram_use, ram_all)) 303 | print("Thanks for using this App,share your experience!{0}".format(""" 304 | Twitter: https://twitter.com/home?status=I%20Like%20{n}!:%20{u} 305 | Facebook: https://www.facebook.com/share.php?u={u}&t=I%20Like%20{n} 306 | G+: https://plus.google.com/share?url={u}""".format(u=__url__, n=app))) 307 | 308 | 309 | def make_root_check_and_encoding_debug() -> bool: 310 | """Debug and Log Encodings and Check for root/administrator,return Boolean. 311 | 312 | >>> make_root_check_and_encoding_debug() 313 | True 314 | """ 315 | print(__doc__ + __version__) 316 | print("Python {0} on {1}.".format(python_version(), platform())) 317 | print("STDIN Encoding: {0}.".format(sys.stdin.encoding)) 318 | print("STDERR Encoding: {0}.".format(sys.stderr.encoding)) 319 | print("STDOUT Encoding:{}".format(getattr(sys.stdout, "encoding", ""))) 320 | print("Default Encoding: {0}.".format(sys.getdefaultencoding())) 321 | print("FileSystem Encoding: {0}.".format(sys.getfilesystemencoding())) 322 | print("PYTHONIOENCODING Encoding: {0}.".format( 323 | os.environ.get("PYTHONIOENCODING", None))) 324 | os.environ["PYTHONIOENCODING"] = "utf-8" 325 | sys.dont_write_bytecode = True 326 | if not sys.platform.startswith("win"): # root check 327 | if not os.geteuid(): 328 | print("Runing as root is not Recommended,NOT Run as root!.") 329 | return False 330 | elif sys.platform.startswith("win"): # administrator check 331 | if getuser().lower().startswith("admin"): 332 | print("Runing as Administrator is not Recommended!.") 333 | return False 334 | return True 335 | 336 | 337 | def set_process_name_and_cpu_priority(name: str) -> bool: 338 | """Set process name and cpu priority. 339 | 340 | >>> set_process_name_and_cpu_priority("test_test") 341 | True 342 | """ 343 | try: 344 | os.nice(19) # smooth cpu priority 345 | libc = cdll.LoadLibrary("libc.so.6") # set process name 346 | buff = create_string_buffer(len(name.lower().strip()) + 1) 347 | buff.value = bytes(name.lower().strip().encode("utf-8")) 348 | libc.prctl(15, byref(buff), 0, 0, 0) 349 | except Exception: 350 | return False # this may fail on windows and its normal, so be silent. 351 | else: 352 | print("Process Name set to: {0}.".format(name)) 353 | return True 354 | 355 | 356 | def set_single_instance(name: str, single_instance: bool=True, port: int=8888): 357 | """Set process name and cpu priority, return socket.socket object or None. 358 | 359 | >>> isinstance(set_single_instance("test"), socket.socket) 360 | True 361 | """ 362 | __lock = None 363 | if single_instance: 364 | try: # Single instance app ~crossplatform, uses udp socket. 365 | print("Creating Abstract UDP Socket Lock for Single Instance.") 366 | __lock = socket.socket( 367 | socket.AF_UNIX if sys.platform.startswith("linux") 368 | else socket.AF_INET, socket.SOCK_STREAM) 369 | __lock.bind( 370 | "\0_{name}__lock".format(name=str(name).lower().strip()) 371 | if sys.platform.startswith("linux") else ("127.0.0.1", port)) 372 | except socket.error as e: 373 | print(e) 374 | else: 375 | print("Socket Lock for Single Instance: {}.".format(__lock)) 376 | else: # if multiple instance want to touch same file bad things can happen 377 | print("Multiple instance on same file can cause Race Condition.") 378 | return __lock 379 | 380 | 381 | ############################################################################## 382 | # Call to work 383 | 384 | 385 | class DThread(QThread): 386 | 387 | """QThread to run ImportD.""" 388 | 389 | def __init__(self, parent=None, importd=None): 390 | """Initialize DThread.""" 391 | super(DThread, self).__init__(parent) 392 | self.importd, self.parent = importd, parent 393 | 394 | def run(self): 395 | """Take D instance and run it on a separate Thread.""" 396 | self.importd.main() 397 | 398 | 399 | def main(d): 400 | """Main Loop.""" 401 | make_root_check_and_encoding_debug() 402 | set_process_name_and_cpu_priority("websktop") 403 | set_single_instance("websktop") 404 | signal.signal(signal.SIGINT, signal.SIG_DFL) # CTRL+C work to quit app 405 | app = QApplication(sys.argv) 406 | app.setApplicationName("websktop") 407 | app.setOrganizationName("websktop") 408 | app.setOrganizationDomain("websktop") 409 | # app.instance().setQuitOnLastWindowClosed(False) # no quit on dialog quit 410 | icon = QIcon(app.style().standardPixmap(QStyle.SP_FileIcon)) 411 | app.setWindowIcon(icon) 412 | window = MainWindow() 413 | window.show() 414 | importd_thread = DThread(app, d) 415 | importd_thread.finished.connect(app.exit) # if ImportD Quits then Quit GUI 416 | app.aboutToQuit.connect(importd_thread.exit) # UI Quits then Quit ImportD 417 | importd_thread.start() 418 | make_post_execution_message() 419 | sys.exit(app.exec()) 420 | 421 | 422 | if __name__ in '__main__': 423 | main() 424 | --------------------------------------------------------------------------------