settings.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. ########################################################################
  2. # Searx-Qt - Lightweight desktop application for Searx.
  3. # Copyright (C) 2020-2022 CYBERDEViL
  4. #
  5. # This file is part of Searx-Qt.
  6. #
  7. # Searx-Qt is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Searx-Qt is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. #
  20. ########################################################################
  21. from PyQt5.QtWidgets import (
  22. QWidget,
  23. QVBoxLayout,
  24. QFormLayout,
  25. QCheckBox,
  26. QLabel,
  27. QDoubleSpinBox,
  28. QLineEdit,
  29. QComboBox,
  30. QHBoxLayout,
  31. QSizePolicy,
  32. QTabWidget,
  33. QPlainTextEdit,
  34. QSpacerItem
  35. )
  36. from PyQt5.QtCore import Qt, pyqtSignal, QVariant
  37. from searxqt.views.guard import GuardSettings
  38. from searxqt.widgets.buttons import Button
  39. from searxqt.widgets.dialogs import UrlDialog
  40. from searxqt.translations import _
  41. from searxqt.themes import Themes
  42. from searxqt.core import log
  43. HAVE_SOCKS = False
  44. try:
  45. import socks
  46. HAVE_SOCKS = True
  47. del socks
  48. except ImportError:
  49. log.debug("pysocks not installed! No socks proxy support.")
  50. class ProxyWidget(QWidget):
  51. changed = pyqtSignal(str) # self.str()
  52. def __init__(self, parent=None):
  53. QWidget.__init__(self, parent=parent)
  54. layout = QVBoxLayout(self)
  55. hLayout = QHBoxLayout()
  56. self._proxyType = QComboBox(self)
  57. self._proxyType.setSizePolicy(QSizePolicy(
  58. QSizePolicy.Maximum,
  59. QSizePolicy.Fixed))
  60. typeList = ['http']
  61. if HAVE_SOCKS:
  62. typeList += ['socks4', 'socks5']
  63. for item in typeList:
  64. self._proxyType.addItem(item)
  65. self._proxyStr = QLineEdit(self)
  66. self._proxyStr.setPlaceholderText("user:pass@host:port")
  67. hLayout.addWidget(self._proxyType)
  68. hLayout.addWidget(self._proxyStr)
  69. layout.addLayout(hLayout)
  70. self._proxyDns = QCheckBox(_("Proxy DNS"), self)
  71. layout.addWidget(self._proxyDns)
  72. if not HAVE_SOCKS:
  73. self._proxyDns.setToolTip(_("Install pysocks for socks support."))
  74. self._proxyDns.setEnabled(False)
  75. self._proxyStr.textChanged.connect(self.__changed)
  76. self._proxyType.currentIndexChanged.connect(self.__typeChanged)
  77. self._proxyDns.toggled.connect(self.__dnsChanged)
  78. def __changed(self):
  79. self.changed.emit(self.str())
  80. def __typeChanged(self, index):
  81. """ From proxy type combobox
  82. """
  83. if index == 0:
  84. self._proxyDns.setEnabled(False)
  85. self._proxyDns.setToolTip(_("Not available for http proxy."))
  86. else:
  87. self._proxyDns.setEnabled(True)
  88. if self.str():
  89. self.__changed()
  90. def __dnsChanged(self, state):
  91. """ From proxy dns checkbox
  92. """
  93. if self.str():
  94. self.__changed()
  95. def reset(self):
  96. self._proxyDns.setChecked(True)
  97. self._proxyStr.setText("")
  98. self._proxyType.setCurrentIndex(0)
  99. self.__typeChanged(0)
  100. def setStr(self, _str):
  101. self.reset()
  102. seq = _str.split(':')
  103. if len(seq) > 1:
  104. index = self._proxyType.findText(seq[0].rstrip('h'))
  105. if index != -1:
  106. self._proxyType.setCurrentIndex(index)
  107. self._proxyStr.setText(_str[len(seq[0]) + 3:])
  108. if seq[0] not in ['socks5h', 'socks4h']:
  109. self._proxyDns.setChecked(False)
  110. def str(self):
  111. if self._proxyStr.text():
  112. return "{0}://{1}".format(
  113. self.protocolStr(),
  114. self._proxyStr.text())
  115. return ""
  116. def protocol(self): return self._proxyType.currentText()
  117. def protocolStr(self):
  118. if (self.protocol() in ['socks4', 'socks5']
  119. and self._proxyDns.isChecked()):
  120. return "{0}h".format(self.protocol())
  121. return self.protocol()
  122. class RequestsSettings(QWidget):
  123. def __init__(self, model, parent=None):
  124. """
  125. @param model:
  126. @type model: searxqt.models.RequestSettingsModel
  127. """
  128. QWidget.__init__(self, parent=parent)
  129. self._model = model
  130. layout = QFormLayout(self)
  131. # Verify checkbox
  132. self._verifyCheck = QCheckBox(self)
  133. layout.addRow(QLabel(_("Verify") + " (SSL):"), self._verifyCheck)
  134. # Timeout double spinbox
  135. self._timeoutSpin = QDoubleSpinBox(self)
  136. self._timeoutSpin.setSuffix(" sec")
  137. self._timeoutSpin.setMinimum(3)
  138. self._timeoutSpin.setMaximum(300)
  139. layout.addRow(QLabel(_("Timeout") + ":"), self._timeoutSpin)
  140. # Proxy
  141. proxyLayout = QFormLayout()
  142. layout.addRow(QLabel(_("Proxy") + ":"), proxyLayout)
  143. self._httpProxy = ProxyWidget(self)
  144. self._httpsProxy = ProxyWidget(self)
  145. proxyLayout.addRow(QLabel("Http:"), self._httpProxy)
  146. proxyLayout.addRow(QLabel("Https:"), self._httpsProxy)
  147. # Headers
  148. # User-agent
  149. userAgentLayout = QFormLayout()
  150. layout.addRow(QLabel(_("User-agents") + ":"), userAgentLayout)
  151. self._userAgentStringsEdit = QPlainTextEdit(self)
  152. self._userAgentStringsEdit.setToolTip(
  153. """- One user-agent string per line.
  154. - Default user-agent string is the first (top) line.
  155. - Empty lines will be removed.
  156. - Leave empty to not send any user-agent string."""
  157. )
  158. userAgentLayout.addWidget(self._userAgentStringsEdit)
  159. self._userAgentEditButton = Button("", self)
  160. self._userAgentEditButton.setCheckable(True)
  161. userAgentLayout.addWidget(self._userAgentEditButton)
  162. self._randomUserAgent = QCheckBox(_("Random"), self)
  163. self._randomUserAgent.setToolTip(
  164. """When checked it will pick a random
  165. user-agent from the list for each request."""
  166. )
  167. userAgentLayout.addWidget(self._randomUserAgent)
  168. # Init values for view
  169. self._changed()
  170. # Connections
  171. self._timeoutSpin.valueChanged.connect(self.__timeoutEdited)
  172. self._verifyCheck.stateChanged.connect(self.__verifyEdited)
  173. self._httpProxy.changed.connect(self.__proxyEdited)
  174. self._httpsProxy.changed.connect(self.__proxyEdited)
  175. self._userAgentEditButton.toggled.connect(self._toggleUserAgentEdit)
  176. self._randomUserAgent.stateChanged.connect(self._randomUserAgentEdited)
  177. self.__unsetWidgetsEditMode()
  178. def _randomUserAgentEdited(self, state):
  179. self._model.randomUserAgent = bool(state)
  180. def __setWidgetsEditMode(self):
  181. self._userAgentEditButton.setText(_("Save"))
  182. self._userAgentStringsEdit.setReadOnly(False)
  183. self._userAgentStringsEdit.setFocus()
  184. def __unsetWidgetsEditMode(self):
  185. self._userAgentEditButton.setText(_("Edit"))
  186. self._userAgentStringsEdit.setReadOnly(True)
  187. def _toggleUserAgentEdit(self, state):
  188. if state:
  189. self.__setWidgetsEditMode()
  190. else:
  191. self.__unsetWidgetsEditMode()
  192. self.__UserAgentStringsEdited(
  193. self._userAgentStringsEdit.toPlainText()
  194. )
  195. def __UserAgentStringsEdited(self, value):
  196. """
  197. @param value: String with the ?user-agent(s)
  198. @type value: str
  199. """
  200. self._model.useragents = [s for s in value.split('\n') if s]
  201. self._userAgentListChanged()
  202. def __timeoutEdited(self, value):
  203. self._model.timeout = value
  204. def __verifyEdited(self, state):
  205. self._model.verify = bool(state)
  206. def __proxyEdited(self, text):
  207. self._model.proxies = {
  208. 'http': self._httpProxy.str(), 'https': self._httpsProxy.str()}
  209. def _userAgentListChanged(self):
  210. txt = ""
  211. for userAgentStr in self._model.useragents:
  212. if not txt:
  213. txt = userAgentStr
  214. else:
  215. txt += "\n{}".format(userAgentStr)
  216. self._userAgentStringsEdit.setPlainText(txt)
  217. def _changed(self):
  218. self._verifyCheck.setChecked(self._model.verify)
  219. self._timeoutSpin.setValue(self._model.timeout)
  220. self._httpProxy.setStr(self._model.proxies.get('http', 'socks5h://'))
  221. self._httpsProxy.setStr(self._model.proxies.get('https', 'socks5h://'))
  222. self._userAgentListChanged()
  223. self._randomUserAgent.setChecked(self._model.randomUserAgent)
  224. class Stats2Settings(QWidget):
  225. def __init__(self, model, parent=None):
  226. """
  227. @type model: SearxStats2Model
  228. """
  229. QWidget.__init__(self, parent=parent)
  230. self._model = model
  231. layout = QVBoxLayout(self)
  232. infoLabel = QLabel(_(
  233. "The Searx-Stats2 project lists public Searx instances with"
  234. " statistics. The original instance is running at"
  235. " https://searx.space/. This is where Searx-Qt will request"
  236. " a list with instances when the update button is pressed."),
  237. self
  238. )
  239. infoLabel.setWordWrap(True)
  240. layout.addWidget(infoLabel, 0, Qt.AlignTop)
  241. hLayout = QHBoxLayout()
  242. label = QLabel("URL:", self)
  243. label.setSizePolicy(
  244. QSizePolicy(
  245. QSizePolicy.Maximum, QSizePolicy.Maximum
  246. )
  247. )
  248. self._urlLabel = QLabel(model.url, self)
  249. self._urlEditButton = Button(_("Edit"), self)
  250. self._urlResetButton = Button(_("Reset"), self)
  251. hLayout.addWidget(label, 0, Qt.AlignTop)
  252. hLayout.addWidget(self._urlLabel, 0, Qt.AlignTop)
  253. hLayout.addWidget(self._urlEditButton, 0, Qt.AlignTop)
  254. hLayout.addWidget(self._urlResetButton, 0, Qt.AlignTop)
  255. spacer = QSpacerItem(
  256. 20, 40, QSizePolicy.Minimum, QSizePolicy.MinimumExpanding
  257. )
  258. layout.addLayout(hLayout)
  259. layout.addItem(spacer)
  260. self._urlEditButton.clicked.connect(self.__urlEditClicked)
  261. self._urlResetButton.clicked.connect(self.__urlResetClicked)
  262. model.changed.connect(self.__modelChanged)
  263. def __modelChanged(self):
  264. self._urlLabel.setText(self._model.url)
  265. def __urlEditClicked(self):
  266. dialog = UrlDialog(self._model.url)
  267. if dialog.exec():
  268. self._model.url = dialog.url
  269. def __urlResetClicked(self):
  270. self._model.reset()
  271. class LogLevelSettings(QWidget):
  272. def __init__(self, parent=None):
  273. QWidget.__init__(self, parent=parent)
  274. layout = QVBoxLayout(self)
  275. label = QLabel(_("<h2>CLI output level</h2>"), self)
  276. self.__cbInfo = QCheckBox(_("Info"), self)
  277. self.__cbWarning = QCheckBox(_("Warning"), self)
  278. self.__cbDebug = QCheckBox(_("Debug"), self)
  279. self.__cbError = QCheckBox(_("Error"), self)
  280. if log.LogLevel & log.LogLevels.INFO:
  281. self.__cbInfo.setChecked(True)
  282. if log.LogLevel & log.LogLevels.WARNING:
  283. self.__cbWarning.setChecked(True)
  284. if log.LogLevel & log.LogLevels.DEBUG:
  285. self.__cbDebug.setChecked(True)
  286. if log.LogLevel & log.LogLevels.ERROR:
  287. self.__cbError.setChecked(True)
  288. layout.addWidget(label)
  289. if log.DebugMode == True:
  290. label = QLabel(
  291. _("Debug mode enabled via environment variable"
  292. " 'SEARXQT_DEBUG'. The settings below are ignored,"
  293. " unset 'SEARXQT_DEBUG' and restart Searx-Qt to disable"
  294. " debug mode."),
  295. parent=self
  296. )
  297. label.setWordWrap(True)
  298. layout.addWidget(label)
  299. layout.addWidget(self.__cbInfo)
  300. layout.addWidget(self.__cbWarning)
  301. layout.addWidget(self.__cbDebug)
  302. layout.addWidget(self.__cbError)
  303. self.__cbInfo.stateChanged.connect(self.__stateChangedInfo)
  304. self.__cbWarning.stateChanged.connect(self.__stateChangedWarning)
  305. self.__cbDebug.stateChanged.connect(self.__stateChangedDebug)
  306. self.__cbError.stateChanged.connect(self.__stateChangedError)
  307. def __stateChanged(self, logLevel, state):
  308. if state:
  309. log.LogLevel |= logLevel
  310. else:
  311. log.LogLevel &= ~logLevel
  312. def __stateChangedInfo(self, state):
  313. self.__stateChanged(log.LogLevels.INFO, state)
  314. def __stateChangedWarning(self, state):
  315. self.__stateChanged(log.LogLevels.WARNING, state)
  316. def __stateChangedDebug(self, state):
  317. self.__stateChanged(log.LogLevels.DEBUG, state)
  318. def __stateChangedError(self, state):
  319. self.__stateChanged(log.LogLevels.ERROR, state)
  320. class GeneralSettings(QWidget):
  321. def __init__(self, parent=None):
  322. QWidget.__init__(self, parent=parent)
  323. layout = QVBoxLayout(self)
  324. # Theme
  325. label = QLabel("<h2>{0}</h2>".format(_("Theme")), self)
  326. layout.addWidget(label, 0, Qt.AlignTop)
  327. formLayout = QFormLayout()
  328. layout.addLayout(formLayout)
  329. self.__themesCombo = QComboBox(self)
  330. currentTheme = Themes.currentTheme
  331. indexOfCurrentTheme = 0
  332. index = 1
  333. self.__themesCombo.addItem("None", QVariant(None))
  334. for theme in Themes.themes:
  335. data = QVariant(theme)
  336. self.__themesCombo.addItem(theme.name, data)
  337. if theme.key == currentTheme:
  338. indexOfCurrentTheme = index
  339. index += 1
  340. self.__themesCombo.setCurrentIndex(indexOfCurrentTheme)
  341. formLayout.addRow(
  342. QLabel(_("Theme:"), self),
  343. self.__themesCombo
  344. )
  345. self.__stylesCombo = QComboBox(self)
  346. currentStyle = Themes.currentStyle
  347. indexOfCurrentStyle = 0
  348. index = 0
  349. for style in Themes.styles:
  350. self.__stylesCombo.addItem(style, QVariant(style))
  351. if style == currentStyle:
  352. indexOfCurrentStyle = index
  353. index += 1
  354. self.__stylesCombo.setCurrentIndex(indexOfCurrentStyle)
  355. formLayout.addRow(
  356. QLabel(_("Base style:"), self),
  357. self.__stylesCombo
  358. )
  359. applyButton = Button("Apply", self)
  360. applyButton.clicked.connect(self.__applyTheme)
  361. layout.addWidget(applyButton, 0, Qt.AlignTop)
  362. # Log level
  363. logLevelSettings = LogLevelSettings(self)
  364. layout.addWidget(logLevelSettings, 1, Qt.AlignTop)
  365. def __applyTheme(self):
  366. index = self.__stylesCombo.currentIndex()
  367. style = self.__stylesCombo.itemData(index, Qt.UserRole)
  368. Themes.setStyle(style)
  369. index = self.__themesCombo.currentIndex()
  370. theme = self.__themesCombo.itemData(index, Qt.UserRole)
  371. Themes.setTheme(theme.key if theme is not None else "")
  372. Themes.repolishAllWidgets()
  373. class SettingsWindow(QTabWidget):
  374. closed = pyqtSignal()
  375. def __init__(self, model, guard, parent=None):
  376. """
  377. @type model: SettingsModel
  378. """
  379. QTabWidget.__init__(self, parent=parent)
  380. self.setWindowTitle(_("Settings"))
  381. # General settings
  382. self._generalView = GeneralSettings(self)
  383. self.addTab(self._generalView, _("General"))
  384. # Requests settings
  385. self._requestsView = RequestsSettings(model.requests, self)
  386. self.addTab(self._requestsView, _("Connection"))
  387. # Stats2 settings
  388. if model.stats2:
  389. self._stats2View = Stats2Settings(model.stats2, self)
  390. self.addTab(self._stats2View, "Searx-Stats2")
  391. # Guard settings
  392. self._guardView = GuardSettings(guard, self)
  393. self.addTab(self._guardView, _("Guard"))
  394. def closeEvent(self, event):
  395. QTabWidget.closeEvent(self, event)
  396. self.closed.emit()