settings.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  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. QFrame,
  24. QVBoxLayout,
  25. QFormLayout,
  26. QGridLayout,
  27. QCheckBox,
  28. QLabel,
  29. QDoubleSpinBox,
  30. QLineEdit,
  31. QComboBox,
  32. QHBoxLayout,
  33. QSizePolicy,
  34. QTabWidget,
  35. QPlainTextEdit,
  36. QSpacerItem
  37. )
  38. from PyQt5.QtCore import Qt, pyqtSignal, QVariant
  39. from searxqt.views.guard import GuardSettings
  40. from searxqt.widgets.buttons import Button
  41. from searxqt.widgets.dialogs import UrlDialog
  42. from searxqt.translations import _
  43. from searxqt.themes import Themes
  44. from searxqt.core import log
  45. from searxqt.core.requests import HAVE_SOCKS, ProxyProtocol
  46. class ProxyWidget(QWidget):
  47. """
  48. @param model:
  49. @type model: searxqt.models.RequestSettingsModel
  50. """
  51. def __init__(self, model, parent=None):
  52. QWidget.__init__(self, parent=parent)
  53. self.__model = model
  54. layout = QFormLayout(self)
  55. self._proxyEnabledCheck = QCheckBox(self)
  56. self._proxyDNSCheck = QCheckBox(self)
  57. self._proxyType = QComboBox(self)
  58. self._proxyType.addItem("http")
  59. if HAVE_SOCKS:
  60. self._proxyType.addItem("socks4")
  61. self._proxyType.addItem("socks5")
  62. self._proxyStr = QLineEdit(self)
  63. self._proxyStr.setPlaceholderText(_("user:pass@host:port"))
  64. layout.addRow(QLabel(_("Enabled")), self._proxyEnabledCheck)
  65. layout.addRow(QLabel(_("Proxy DNS")), self._proxyDNSCheck)
  66. layout.addRow(QLabel(_("Protocol")), self._proxyType)
  67. layout.addRow(QLabel(_("Address")), self._proxyStr)
  68. # Set values
  69. self._setValuesFromModel(model)
  70. # Connections
  71. self._proxyEnabledCheck.toggled.connect(self.__enabledToggled)
  72. self._proxyDNSCheck.toggled.connect(self.__dnsToggled)
  73. self._proxyType.currentIndexChanged.connect(self.__protocolChanged)
  74. self._proxyStr.textEdited.connect(self.__hostTextEdited)
  75. def _setValuesFromModel(self, model):
  76. self._proxyEnabledCheck.setChecked(model.proxyEnabled)
  77. self.enableWidgets(model.proxyEnabled) # enable/disable widgets
  78. self._proxyDNSCheck.setChecked(model.proxyDNS)
  79. if model.proxyProtocol == ProxyProtocol.HTTP:
  80. self._proxyType.setCurrentIndex(0)
  81. elif HAVE_SOCKS:
  82. if model.proxyProtocol == ProxyProtocol.SOCKS4:
  83. self._proxyType.setCurrentIndex(1)
  84. elif model.proxyProtocol == ProxyProtocol.SOCKS5:
  85. self._proxyType.setCurrentIndex(2)
  86. self._proxyStr.setText(model.proxyHost)
  87. def __enabledToggled(self, state):
  88. self.__model.proxyEnabled = bool(state)
  89. self.enableWidgets(state)
  90. def enableWidgets(self, state):
  91. self._proxyDNSCheck.setEnabled(state)
  92. self._proxyType.setEnabled(state)
  93. self._proxyStr.setEnabled(state)
  94. def __dnsToggled(self, state):
  95. self.__model.proxyDNS = bool(state)
  96. def __protocolChanged(self, index):
  97. if index == 0:
  98. self.__model.proxyProtocol = ProxyProtocol.HTTP
  99. self._proxyDNSCheck.setEnabled(False)
  100. self._proxyDNSCheck.setToolTip(_("Not available for http proxy."))
  101. return
  102. elif index == 1:
  103. self.__model.proxyProtocol = ProxyProtocol.SOCKS4
  104. else: # index == 2
  105. self.__model.proxyProtocol = ProxyProtocol.SOCKS5
  106. self._proxyDNSCheck.setEnabled(True)
  107. def __hostTextEdited(self, newText):
  108. # TODO verify format
  109. self.__model.proxyHost = newText
  110. class TextEdit(QFrame):
  111. def __init__(self, model, parent=None):
  112. QFrame.__init__(self, parent)
  113. self._model = model
  114. layout = QVBoxLayout(self)
  115. buttonLayout = QHBoxLayout()
  116. self._textEdit = QPlainTextEdit(self)
  117. self._textEdit.setReadOnly(True)
  118. self._textEdit.setLineWrapMode(QPlainTextEdit.NoWrap)
  119. self._editButton = Button(_("Edit"), self)
  120. self._editButton.setCheckable(True)
  121. self._editButton.setChecked(False)
  122. self._cancelButton = Button(_("Cancel"), self)
  123. self._cancelButton.setCheckable(False)
  124. self._cancelButton.hide()
  125. buttonLayout.addWidget(self._cancelButton, 0, Qt.AlignLeft)
  126. buttonLayout.addWidget(self._editButton, 0, Qt.AlignLeft)
  127. buttonLayout.addStretch(1)
  128. layout.addWidget(self._textEdit)
  129. layout.addLayout(buttonLayout)
  130. self._editButton.toggled.connect(self.__toggleEdit)
  131. self._cancelButton.clicked.connect(self.__cancelEdit)
  132. def __toggleEdit(self, state):
  133. if state:
  134. self._editButton.setText(_("Save"))
  135. self._textEdit.setReadOnly(False)
  136. self._cancelButton.show()
  137. else:
  138. self._editButton.setText(_("Edit"))
  139. self._textEdit.setReadOnly(True)
  140. self._cancelButton.hide()
  141. self.updateToModel()
  142. def __cancelEdit(self, state):
  143. self._editButton.setText(_("Edit"))
  144. self._cancelButton.hide()
  145. self._textEdit.setReadOnly(True)
  146. self._editButton.setChecked(False)
  147. self.updateFromModel()
  148. def updateFromModel(self):
  149. pass # reimplement this
  150. def updateToModel(self):
  151. pass # reimplement this
  152. class RequestsHeaderEdit(TextEdit):
  153. def __init__(self, model, parent=None):
  154. TextEdit.__init__(self, model, parent)
  155. warningLabel = QLabel(_("WARNING! Edit below only when you know what "
  156. "you are doing!"), self)
  157. warningLabel.setWordWrap(True)
  158. self.layout().insertWidget(0, warningLabel)
  159. self.updateFromModel()
  160. def updateFromModel(self):
  161. text = ""
  162. for key, value in self._model.extraHeaders.items():
  163. text += f"\"{key}\" \"{value}\"\n"
  164. self._textEdit.setPlainText(text)
  165. def updateToModel(self):
  166. self._model.extraHeaders.clear()
  167. for line in self._textEdit.toPlainText().split("\n"):
  168. if " " not in line:
  169. continue
  170. key, value = line.split(" ", 1)
  171. value = value.lstrip().rstrip()
  172. if not key.startswith("\"") or not key.endswith("\""):
  173. # TODO malformed, inform the user
  174. continue
  175. if not value.startswith("\"") or not value.endswith("\""):
  176. # TODO malformed, inform the user
  177. continue
  178. key = key[1:len(key)-1]
  179. value = value[1:len(value)-1]
  180. if key.lower() == "user-agent":
  181. # TODO inform the user
  182. continue
  183. if "\"" in key:
  184. # TODO malformed, inform the user
  185. continue
  186. if "\"" in value:
  187. # TODO malformed, inform the user
  188. continue
  189. self._model.extraHeaders.update({key: value})
  190. self.updateFromModel()
  191. class UserAgentsEdit(TextEdit):
  192. def __init__(self, model, parent=None):
  193. TextEdit.__init__(self, model, parent)
  194. self._textEdit.setToolTip(
  195. """- One user-agent string per line.
  196. - Default user-agent string is the first (top) line.
  197. - Empty lines will be removed.
  198. - Leave empty to not send any user-agent string."""
  199. )
  200. self._randomUserAgent = QCheckBox(_("Random"), self)
  201. self._randomUserAgent.setToolTip(
  202. """When checked it will pick a random
  203. user-agent from the list for each request."""
  204. )
  205. self.layout().addWidget(self._randomUserAgent)
  206. self.updateFromModel()
  207. self._randomUserAgent.stateChanged.connect(self._randomUserAgentEdited)
  208. def _randomUserAgentEdited(self, state):
  209. self._model.randomUserAgent = bool(state)
  210. def updateToModel(self):
  211. text = self._textEdit.toPlainText()
  212. self._model.useragents = [s for s in text.split('\n') if s]
  213. self.updateFromModel()
  214. def updateFromModel(self):
  215. text = ""
  216. for userAgentStr in self._model.useragents:
  217. if not text:
  218. text = userAgentStr
  219. else:
  220. text += f"\n{userAgentStr}"
  221. self._textEdit.setPlainText(text)
  222. class RequestsSettings(QWidget):
  223. def __init__(self, model, parent=None):
  224. """
  225. @param model:
  226. @type model: searxqt.models.RequestSettingsModel
  227. """
  228. QWidget.__init__(self, parent=parent)
  229. self._model = model
  230. layout = QFormLayout(self)
  231. # Verify checkbox
  232. self._verifyCheck = QCheckBox(self)
  233. layout.addRow(QLabel(_("Verify") + " (SSL):"), self._verifyCheck)
  234. # Timeout double spinbox
  235. self._timeoutSpin = QDoubleSpinBox(self)
  236. self._timeoutSpin.setSuffix(" sec")
  237. self._timeoutSpin.setMinimum(3)
  238. self._timeoutSpin.setMaximum(300)
  239. layout.addRow(QLabel(_("Timeout") + ":"), self._timeoutSpin)
  240. # Proxy
  241. self._proxyWidget = ProxyWidget(self._model, self)
  242. layout.addRow(QLabel(_("Proxy") + ":"), self._proxyWidget)
  243. # Headers
  244. # User-agent
  245. self._useragents = UserAgentsEdit(model, self)
  246. layout.addRow(QLabel(_("User-Agents") + ":"), self._useragents)
  247. # Additional headers
  248. self._extraHeaders = RequestsHeaderEdit(model, self)
  249. layout.addRow(QLabel(_("Extra headers") + ":"), self._extraHeaders)
  250. # Init values for view
  251. self._changed()
  252. # Connections
  253. self._verifyCheck.stateChanged.connect(self.__verifyEdited)
  254. self._timeoutSpin.valueChanged.connect(self.__timeoutEdited)
  255. def __timeoutEdited(self, value):
  256. self._model.timeout = value
  257. def __verifyEdited(self, state):
  258. self._model.verifySSL = bool(state)
  259. def _changed(self):
  260. self._verifyCheck.setChecked(self._model.verifySSL)
  261. self._timeoutSpin.setValue(self._model.timeout)
  262. class SearxngSettings(QWidget):
  263. def __init__(self, model, parent=None):
  264. """
  265. model: SearchModel
  266. """
  267. QWidget.__init__(self, parent=parent)
  268. layout = QGridLayout(self)
  269. self.__searchModel = model
  270. self._useHtmlParser = QCheckBox(self)
  271. self._useHtmlParser.setChecked(model.parseHtml)
  272. self._safeSearch = QCheckBox(self)
  273. self._safeSearch.setChecked(model.safeSearch)
  274. infoLabel = QLabel(
  275. _("Since many SearXNG instances block API requests "
  276. "we now have a option to not use it and parse HTML "
  277. "response instead. Check \"Parse HTML\" below for "
  278. "that, when left unchecked it will use the API "
  279. "instead."), self)
  280. infoLabel.setWordWrap(True)
  281. layout.addWidget(infoLabel, 0, 0, 1, 2, Qt.AlignTop)
  282. layout.addWidget(QLabel(_("Parse HTML"), self), 1, 0,
  283. Qt.AlignLeft|Qt.AlignTop)
  284. layout.addWidget(self._useHtmlParser, 1, 1,
  285. Qt.AlignLeft|Qt.AlignTop)
  286. infoLabel = QLabel(_("Enable \"Safe search\" for search engines that "
  287. "support it."), self)
  288. infoLabel.setWordWrap(True)
  289. layout.addWidget(infoLabel, 2, 0, 1, 2, Qt.AlignTop)
  290. layout.addWidget(QLabel(_("Safe Search"), self), 3, 0,
  291. Qt.AlignLeft|Qt.AlignTop)
  292. layout.addWidget(self._safeSearch, 3, 1, Qt.AlignLeft|Qt.AlignTop)
  293. layout.setRowStretch(3, 1)
  294. layout.setColumnStretch(1, 1)
  295. self._useHtmlParser.stateChanged.connect(self.__useHtmlParserChanged)
  296. self._safeSearch.stateChanged.connect(self.__safeSearchChanged)
  297. def __useHtmlParserChanged(self, state):
  298. self.__searchModel.parseHtml = bool(state)
  299. def __safeSearchChanged(self, state):
  300. self.__searchModel.safeSearch = bool(state)
  301. class Stats2Settings(QWidget): # TODO rename to SearxSpaceSettings and rename stats2 to searxspace or something..
  302. def __init__(self, model, parent=None):
  303. """
  304. @type model: SearxStats2Model
  305. """
  306. QWidget.__init__(self, parent=parent)
  307. self._model = model
  308. layout = QVBoxLayout(self)
  309. infoLabel = QLabel(_(
  310. "The Searx-Stats2 project lists public Searx instances with"
  311. " statistics. The original instance is running at"
  312. " https://searx.space/. This is where Searx-Qt will request"
  313. " a list with instances when the update button is pressed."),
  314. self
  315. )
  316. infoLabel.setWordWrap(True)
  317. layout.addWidget(infoLabel, 0, Qt.AlignTop)
  318. hLayout = QHBoxLayout()
  319. label = QLabel("URL:", self)
  320. label.setSizePolicy(
  321. QSizePolicy(
  322. QSizePolicy.Maximum, QSizePolicy.Maximum
  323. )
  324. )
  325. self._urlLabel = QLabel(model.url, self)
  326. self._urlEditButton = Button(_("Edit"), self)
  327. self._urlResetButton = Button(_("Reset"), self)
  328. hLayout.addWidget(label, 0, Qt.AlignTop)
  329. hLayout.addWidget(self._urlLabel, 0, Qt.AlignTop)
  330. hLayout.addWidget(self._urlEditButton, 0, Qt.AlignTop)
  331. hLayout.addWidget(self._urlResetButton, 0, Qt.AlignTop)
  332. spacer = QSpacerItem(
  333. 20, 40, QSizePolicy.Minimum, QSizePolicy.MinimumExpanding
  334. )
  335. layout.addLayout(hLayout)
  336. layout.addItem(spacer)
  337. self._urlEditButton.clicked.connect(self.__urlEditClicked)
  338. self._urlResetButton.clicked.connect(self.__urlResetClicked)
  339. model.changed.connect(self.__modelChanged)
  340. def __modelChanged(self):
  341. self._urlLabel.setText(self._model.url)
  342. def __urlEditClicked(self):
  343. dialog = UrlDialog(self._model.url)
  344. if dialog.exec():
  345. self._model.url = dialog.url
  346. def __urlResetClicked(self):
  347. self._model.reset()
  348. class LogLevelSettings(QWidget):
  349. def __init__(self, parent=None):
  350. QWidget.__init__(self, parent=parent)
  351. layout = QVBoxLayout(self)
  352. label = QLabel(_("<h2>CLI output level</h2>"), self)
  353. self.__cbInfo = QCheckBox(_("Info"), self)
  354. self.__cbWarning = QCheckBox(_("Warning"), self)
  355. self.__cbDebug = QCheckBox(_("Debug"), self)
  356. self.__cbError = QCheckBox(_("Error"), self)
  357. if log.LogLevel & log.LogLevels.INFO:
  358. self.__cbInfo.setChecked(True)
  359. if log.LogLevel & log.LogLevels.WARNING:
  360. self.__cbWarning.setChecked(True)
  361. if log.LogLevel & log.LogLevels.DEBUG:
  362. self.__cbDebug.setChecked(True)
  363. if log.LogLevel & log.LogLevels.ERROR:
  364. self.__cbError.setChecked(True)
  365. layout.addWidget(label)
  366. if log.DebugMode == True:
  367. label = QLabel(
  368. _("Debug mode enabled via environment variable"
  369. " 'SEARXQT_DEBUG'. The settings below are ignored,"
  370. " unset 'SEARXQT_DEBUG' and restart Searx-Qt to disable"
  371. " debug mode."),
  372. parent=self
  373. )
  374. label.setWordWrap(True)
  375. layout.addWidget(label)
  376. layout.addWidget(self.__cbInfo)
  377. layout.addWidget(self.__cbWarning)
  378. layout.addWidget(self.__cbDebug)
  379. layout.addWidget(self.__cbError)
  380. self.__cbInfo.stateChanged.connect(self.__stateChangedInfo)
  381. self.__cbWarning.stateChanged.connect(self.__stateChangedWarning)
  382. self.__cbDebug.stateChanged.connect(self.__stateChangedDebug)
  383. self.__cbError.stateChanged.connect(self.__stateChangedError)
  384. def __stateChanged(self, logLevel, state):
  385. if state:
  386. log.LogLevel |= logLevel
  387. else:
  388. log.LogLevel &= ~logLevel
  389. def __stateChangedInfo(self, state):
  390. self.__stateChanged(log.LogLevels.INFO, state)
  391. def __stateChangedWarning(self, state):
  392. self.__stateChanged(log.LogLevels.WARNING, state)
  393. def __stateChangedDebug(self, state):
  394. self.__stateChanged(log.LogLevels.DEBUG, state)
  395. def __stateChangedError(self, state):
  396. self.__stateChanged(log.LogLevels.ERROR, state)
  397. class GeneralSettings(QWidget):
  398. def __init__(self, parent=None):
  399. QWidget.__init__(self, parent=parent)
  400. layout = QVBoxLayout(self)
  401. # Theme
  402. label = QLabel(f"<h2>{_('Theme')}</h2>", self)
  403. layout.addWidget(label, 0, Qt.AlignTop)
  404. formLayout = QFormLayout()
  405. layout.addLayout(formLayout)
  406. self.__themesCombo = QComboBox(self)
  407. currentTheme = Themes.currentTheme
  408. indexOfCurrentTheme = 0
  409. index = 1
  410. self.__themesCombo.addItem("None", QVariant(None))
  411. for theme in Themes.themes:
  412. data = QVariant(theme)
  413. self.__themesCombo.addItem(theme.name, data)
  414. if theme.key == currentTheme:
  415. indexOfCurrentTheme = index
  416. index += 1
  417. self.__themesCombo.setCurrentIndex(indexOfCurrentTheme)
  418. formLayout.addRow(
  419. QLabel(_("Theme:"), self),
  420. self.__themesCombo
  421. )
  422. self.__stylesCombo = QComboBox(self)
  423. currentStyle = Themes.currentStyle
  424. indexOfCurrentStyle = 0
  425. index = 0
  426. for style in Themes.styles:
  427. self.__stylesCombo.addItem(style, QVariant(style))
  428. if style == currentStyle:
  429. indexOfCurrentStyle = index
  430. index += 1
  431. self.__stylesCombo.setCurrentIndex(indexOfCurrentStyle)
  432. formLayout.addRow(
  433. QLabel(_("Base style:"), self),
  434. self.__stylesCombo
  435. )
  436. applyButton = Button("Apply", self)
  437. applyButton.clicked.connect(self.__applyTheme)
  438. layout.addWidget(applyButton, 0, Qt.AlignTop)
  439. # Log level
  440. logLevelSettings = LogLevelSettings(self)
  441. layout.addWidget(logLevelSettings, 1, Qt.AlignTop)
  442. def __applyTheme(self):
  443. index = self.__stylesCombo.currentIndex()
  444. style = self.__stylesCombo.itemData(index, Qt.UserRole)
  445. Themes.setStyle(style)
  446. index = self.__themesCombo.currentIndex()
  447. theme = self.__themesCombo.itemData(index, Qt.UserRole)
  448. Themes.setTheme(theme.key if theme is not None else "")
  449. Themes.repolishAllWidgets()
  450. class SettingsWindow(QTabWidget):
  451. closed = pyqtSignal()
  452. def __init__(self, model, searchModel, guard, parent=None):
  453. """
  454. @type model: SettingsModel
  455. """
  456. QTabWidget.__init__(self, parent=parent)
  457. self.setWindowTitle(_("Settings"))
  458. # General settings
  459. self._generalView = GeneralSettings(self)
  460. self.addTab(self._generalView, _("General"))
  461. # Requests settings
  462. self._requestsView = RequestsSettings(model.requests, self)
  463. self.addTab(self._requestsView, _("Connection"))
  464. # SearXNG settings
  465. self._searxView = SearxngSettings(searchModel, self)
  466. self.addTab(self._searxView, "SearXNG")
  467. # Stats2 settings
  468. if model.stats2:
  469. self._stats2View = Stats2Settings(model.stats2, self)
  470. self.addTab(self._stats2View, "Searx-Stats2")
  471. # Guard settings
  472. self._guardView = GuardSettings(guard, self)
  473. self.addTab(self._guardView, _("Guard"))
  474. def closeEvent(self, event):
  475. QTabWidget.closeEvent(self, event)
  476. self.closed.emit()