├── 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 | [](http://opensource.org/licenses/GPL-3.0) [](http://opensource.org/licenses/LGPL-3.0) [](http://python.org) [](http://goo.gl/cB7PR)
8 |
9 |
10 | 
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 |
--------------------------------------------------------------------------------