Compare commits

..

No commits in common. "2f4c3103b8588f756e4b08cc837e0d645177ab3a" and "f5f132914b72f2a4252ee5f06d4327c50c0fa332" have entirely different histories.

5 changed files with 150 additions and 389 deletions

2
.gitignore vendored
View File

@ -1,2 +0,0 @@
config.ini
__pycache__/

View File

@ -1,2 +0,0 @@
192.168.30.71
192.168.30.69

View File

@ -1,113 +1,40 @@
#!/usr/bin/python3 #!/usr/bin/python3
import sys import sys
import tasmotonov import subprocess
import configparser
from PySide6.QtUiTools import QUiLoader from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication,QFileDialog from PySide6.QtWidgets import QApplication,QFileDialog
from PySide6.QtCore import QFile, QUrl, QIODevice from PySide6.QtCore import QFile, QUrl, QIODevice
# set some standard values
filename = "" filename = ""
action = "toggle" action = "toggle"
def load_config(file="config.ini"): # load config from configparser.ConfigParser object def tab1_load_file():
to_load = configparser.ConfigParser()
to_load.read(file)
global filename, action
if "DEFAULT" in to_load:
if "file" in to_load['DEFAULT']:
filename = to_load['DEFAULT']['file']
if filename != "":
tab1_load_file()
if "inline" in to_load['DEFAULT']:
window.tab2_plainTextEdit.setPlainText(to_load['DEFAULT']['inline'])
if "action" in to_load['DEFAULT']:
a_new = to_load['DEFAULT']['action']
if a_new != "":
action = a_new
# else: no config there yet
def save_config(file="config.ini"):
window.tab3_textBrowser.append("Saving configuration!")
global filename, action
to_save = configparser.ConfigParser()
to_save['DEFAULT'] = {}
to_save['DEFAULT']['file'] = str(filename)
to_save['DEFAULT']['inline'] = str(window.tab2_plainTextEdit.toPlainText())
to_save['DEFAULT']['action'] = str(action)
with open(file, 'w') as configfile:
to_save.write(configfile)
# tab1 slots
def tab1_load_file_pressed():
global filename global filename
dialog = QFileDialog() dialog = QFileDialog()
dialog.setFileMode(QFileDialog.AnyFile) dialog.setFileMode(QFileDialog.AnyFile)
#fileNames = QStringList()
if dialog.exec(): if dialog.exec():
filename = dialog.selectedFiles()[0] filename = dialog.selectedFiles()[0]
tab1_load_file() window.tab1_label.setText(f"File loaded: {filename}")
def tab1_load_file(): window.tab1_label.show()
global filename window.tab1_filecontent_textBrowser.setSource(QUrl(filename))
window.tab1_label.setText(f"File loaded: {filename}")
window.tab3_textBrowser.append(f"File loaded: {filename}")
window.tab1_label.show()
window.tab1_filecontent_textBrowser.setSource(QUrl(filename))
window.tab1_label_help.show()
# clear listWidget and add the items
window.tab1_listWidget.clear()
window.tab1_listWidget.addItems(tasmotonov.TasmotonovRunner("file", filename, action, False, False).get_addresses())
def tab1_clear():
global filename
filename = ""
window.tab1_label.setText(f"File loaded: ")
window.tab3_textBrowser.append(f"File unloaded")
window.tab1_label.hide()
window.tab1_label_help.hide()
window.tab1_filecontent_textBrowser.clear()
window.tab1_listWidget.clear()
def tab1_action(): def tab1_action():
if filename != "": if filename != "":
tasmotonov_runner = tasmotonov.TasmotonovRunner("file", filename, action, False, False) p = subprocess.Popen(["python3", "tasmotonov.py", "file", filename, action], stdout=subprocess.PIPE)
tasmotonov_runner.run() out, err = p.communicate()
window.tab3_textBrowser.append("\n==== RUNNING ====\n\n" + str(tasmotonov_runner.logger.log_string) + "\n=================\n") window.tab3_textBrowser.append("==== RUNNING ====\n" + str(out) + "\n=================\n")
tasmotonov_runner.logger.log_string = ""
else: else:
window.tab3_textBrowser.append("Will not run, no file selected!") window.tab3_textBrowser.append("Will not run, no file selected!")
def tab1_single_action():
if filename != "":
tasmotonov_runner = tasmotonov.TasmotonovRunner("file", filename, action, False, False)
tasmotonov_runner.run_single(window.tab1_listWidget.currentRow())
window.tab3_textBrowser.append(str(tasmotonov_runner.logger.log_string))
# tab2 slots
def tab2_plainTextEdit_change():
# clear listWidget
window.tab2_listWidget.clear()
content = window.tab2_plainTextEdit.toPlainText()
if content != "":
tasmotonov_runner = tasmotonov.TasmotonovRunner("inline", content, action, False, False)
window.tab2_listWidget.addItems(tasmotonov_runner.get_addresses())
window.tab3_textBrowser.append(str(tasmotonov_runner.logger.log_string))
def tab2_action(): def tab2_action():
content = window.tab2_plainTextEdit.toPlainText() content = window.tab2_plainTextEdit.toPlainText()
if content != "": if content != "":
tasmotonov_runner = tasmotonov.TasmotonovRunner("inline", content, action, False, False) p = subprocess.Popen(["python3", "tasmotonov.py", "inline", content, action], stdout=subprocess.PIPE)
tasmotonov_runner.run() out, err = p.communicate()
window.tab3_textBrowser.append("\n==== RUNNING ====\n\n" + str(tasmotonov_runner.logger.log_string) + "\n=================\n") window.tab3_textBrowser.append("==== RUNNING ====\n" + str(out) + "\n=================\n")
print(out)
else: else:
window.tab3_textBrowser.append("Will not run, no input given!") window.tab3_textBrowser.append("Will not run, no input given!")
def tab2_single_action():
content = window.tab2_plainTextEdit.toPlainText()
tasmotonov_runner = tasmotonov.TasmotonovRunner("inline", content, action, False, False)
if len(tasmotonov_runner.get_addresses()) != 0:
tasmotonov_runner.run_single(window.tab2_listWidget.currentRow())
window.tab3_textBrowser.append(str(tasmotonov_runner.logger.log_string))
# tab3 slots
def tab3_clear():
window.tab3_textBrowser.clear()
# other slots
def select_on(): def select_on():
global action global action
action = "on" action = "on"
@ -137,29 +64,14 @@ if __name__ == "__main__":
sys.exit(-1) sys.exit(-1)
window.tab1_label.hide() window.tab1_label.hide()
window.tab1_label_help.hide() window.tab1_load_file_pushButton.clicked.connect(tab1_load_file)
window.tab1_load_file_pushButton.clicked.connect(tab1_load_file_pressed)
window.tab1_clear_pushButton.clicked.connect(tab1_clear)
window.tab1_action_pushButton.clicked.connect(tab1_action) window.tab1_action_pushButton.clicked.connect(tab1_action)
window.tab1_single_action_pushButton.clicked.connect(tab1_single_action)
window.tab2_plainTextEdit.textChanged.connect(tab2_plainTextEdit_change)
window.tab2_action_pushButton.clicked.connect(tab2_action) window.tab2_action_pushButton.clicked.connect(tab2_action)
window.tab2_single_action_pushButton.clicked.connect(tab2_single_action)
window.radioButton_on.clicked.connect(select_on) window.radioButton_on.clicked.connect(select_on)
window.radioButton_off.clicked.connect(select_off) window.radioButton_off.clicked.connect(select_off)
window.radioButton_toggle.clicked.connect(select_toggle) window.radioButton_toggle.clicked.connect(select_toggle)
window.tab3_clear_button.clicked.connect(tab3_clear)
load_config()
window.show() window.show()
# execute app loop and define exit strategy sys.exit(app.exec())
code = app.exec()
save_config()
sys.exit(code)

View File

@ -18,15 +18,29 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import sys # for exiting with the correct exit code
import argparse # to parse the cli args! import argparse # to parse the cli args!
import requests # needed to access the http endpoints import requests # needed to access the http endpoints
import ipaddress # to validate IP addresses import ipaddress # to validate IP addresses
from fqdn import FQDN # validate FQDNs from fqdn import FQDN # validate FQDNs
version = "v1.1.0" version = "v1.0.0"
parser = argparse.ArgumentParser(
prog='Tasmotonov - simply toggle multiple tasmota lights',
formatter_class=argparse.RawDescriptionHelpFormatter,
description='A very simple script which allows you to turn on/off multiple tasmota devices specified.',
epilog='Info: if you choose a file as source, this files needs to contain the addresses of the tasmota devices either comma-separated, semicolon-separated, or newline-separated!\n\n© Benjamin Burkhardt, 2025')
parser.add_argument('source', help='Select either to read the adresses (of the devices) from a "file" or from "inline"', choices=['file', 'inline'])
parser.add_argument('data', help='Either the path to the file, or a comma- or semicolon-separated list of tasmota adresses.')
parser.add_argument('action', help='Select to turn all tasmota devices "on" or "off" or "toggle" (case insensitive)', choices=['on', 'off', 'toggle'])
parser.add_argument('-v', '--verbose', help='Turn on verbose file output', action='store_true')
parser.add_argument('--version', action='version', version=f'Tasmotonov.py {version}')
# some helpers / utils # some helpers / utils
def is_ip(string): def is_ip(string):
try: try:
i = ipaddress.ip_address(string) i = ipaddress.ip_address(string)
@ -40,192 +54,104 @@ def is_fqdn(string):
# logging # logging
class Logger():
def __init__(self, verbose=True, enable_stdout=True): class COLORS:
self.verbose = verbose HEADER = '\033[95m'
self.enable_stdout = enable_stdout OKBLUE = '\033[94m'
self.log_string = "" # string variable where the logged lines are stored OKCYAN = '\033[96m'
self.COLORS = { OKGREEN = '\033[92m'
'HEADER':'\033[95m', WARNING = '\033[93m'
'OKBLUE':'\033[94m', FAIL = '\033[91m'
'OKCYAN':'\033[96m', ENDC = '\033[0m'
'OKGREEN':'\033[92m', BOLD = '\033[1m'
'WARNING':'\033[93m', UNDERLINE = '\033[4m'
'FAIL':'\033[91m',
'ENDC':'\033[0m', def log(to_log, verbose = True, end = '\n'):
'BOLD': '\033[1m', if (verbose and args.verbose) or not verbose:
'UNDERLINE': '\033[4m' print(to_log, end=end)
} def log_error(to_log, end='\n'):
log(COLORS.FAIL + '[ERROR] ' + COLORS.ENDC + to_log, verbose=False, end=end)
def log(self, to_log, verbose = True, end = '\n'): def log_warning(to_log, end='\n'):
if self.enable_stdout and ((verbose and self.verbose) or not verbose): log(COLORS.WARNING + '[WARNING] ' + COLORS.ENDC + to_log, verbose=False, end=end)
print(to_log, end=end) def log_success(to_log, end='\n'):
if ((verbose and self.verbose) or not verbose): log(COLORS.OKGREEN + '[SUCCESS] ' + COLORS.ENDC + to_log, verbose=False, end=end)
# remove the shell colors
to_log_color_cleaned = to_log
for color in list(self.COLORS.values()):
to_log_color_cleaned = to_log_color_cleaned.replace(color, '')
self.log_string += to_log_color_cleaned + end
def log_error(self, to_log, end='\n'):
self.log(self.COLORS['FAIL'] + '[ERROR] ' + self.COLORS['ENDC'] + to_log, verbose=False, end=end)
def log_warning(self, to_log, end='\n'):
self.log(self.COLORS['WARNING'] + '[WARNING] ' + self.COLORS['ENDC'] + to_log, verbose=False, end=end)
def log_success(self, to_log, end='\n'):
self.log(self.COLORS['OKGREEN'] + '[SUCCESS] ' + self.COLORS['ENDC'] + to_log, verbose=False, end=end)
# the tasmotonov runner class # the real program
class TasmotonovRunner:
def __init__(self, source, data, action, verbose=True, enable_stdout=False):
if str(source).lower() in ['file', 'inline']:
self.source = source
else:
error_string = 'The source param must be either "file" or "inline" (case-insensitive)'
logger.log_error(error_string)
raise ValueError(error_string)
self.data = data
if str(action).upper() in ['ON', 'OFF', 'TOGGLE']:
self.action = action.upper()
else:
error_string = 'The action param must be either "on", "off" or "toggle" (case-insensitive)'
logger.log_error(error_string)
raise ValueError(error_string)
if type(verbose) == bool and type(enable_stdout) == bool:
self.logger = Logger(verbose, enable_stdout)
else:
error_string = 'Both verbose and enable_stdout params must be boolean'
logger.log_error(error_string)
raise ValueError(error_string)
self.logger.log(f'Initialized runner: source={source}, data={data}, action={action}, verbose={verbose}')
self.tasmota_addresses = self.remove_invalid_addresses(self.clean_addresses(self.parse_addresses())) # returns a list
def parse_addresses(self):
# convert the given adresses to a list
tasmota_addresses = []
if self.source == 'file':
try:
self.logger.log('Now trying to open the given file...', end='')
with open(self.data, 'r') as file:
contents = file.read()
if ';' in contents:
tasmota_addresses = contents.split(';')
elif ',' in contents:
tasmota_addresses = contents.split(',')
elif '\n' in contents:
tasmota_addresses = contents.split('\n')
elif ' ' in contents:
tasmota_addresses = contents.split(' ')
else:
tasmota_addresses = [contents]
self.logger.log('Done.')
self.logger.log(f'Collected addresses: {tasmota_addresses}')
except FileNotFoundError as e:
self.logger.log_error(f'Failed reading addresses. File with the path "{self.data} can\'t be opened because it doesn\'t exist. Exiting.')
raise e
elif self.source == 'inline': if __name__ == '__main__':
self.logger.log('Now reading from inline... ', end='') args = parser.parse_args() # parse all given arguments
if ';' in self.data: log(f'Parsed args: {args}')
tasmota_addresses = self.data.split(';')
elif ',' in self.data:
tasmota_addresses = self.data.split(',')
elif '\n' in self.data:
tasmota_addresses = self.data.split('\n')
elif ' ' in self.data:
tasmota_addresses = self.data.split(' ')
else:
tasmota_addresses = [self.data]
self.logger.log('Done.')
self.logger.log(f'Collected addresses: {tasmota_addresses}')
return tasmota_addresses # convert the given adresses to a list
tasmota_addresses = []
def clean_addresses(self, addresses_raw): if args.source == 'file':
# clean them up (e.g. remove newlines if the file has a random newline somewhere
tasmota_addresses_cleaned = []
for address in addresses_raw:
to_add = address.replace('\n', '').replace(';', '').replace(',', '').replace(' ', '')
if to_add != '':
tasmota_addresses_cleaned.append(to_add)
return tasmota_addresses_cleaned
def remove_invalid_addresses(self, addressse_raw):
# now check data for consistency and integrity (is it really an ip address or a domain name?)
# remove the ones that are not valid and log a warning
tasmota_addresses_validated = []
self.logger.log('Now validating given addresses...')
for address in addressse_raw:
if is_ip(address) or is_fqdn(address):
tasmota_addresses_validated.append(address)
else:
self.logger.log_warning(f'The address "{address}" is neither a valid IP address nor a FQDN / domain name; SKIPPING THIS ONE.')
self.logger.log('Validated given adddresses.')
return tasmota_addresses_validated
# some getters
def get_addresses(self):
return self.tasmota_addresses
def get_address(self, index):
return self.tasmota_addresses[index]
def run_custom_action(self, action):
# and finally turn on or off or toggle the lights!
for address in self.tasmota_addresses:
self.logger.log('Running the action "{action}" on the following device: {address}')
try:
answer = requests.get(f'http://{address}/cm?cmnd=Power%20{str(action).lower()}')
success = True
except requests.exceptions.ConnectionError:
self.logger.log_warning(f'Could not connect to "{address}". Skipping...')
success = False
if answer.status_code != 200:
self.logger.log_warning(f'{address} returned the following non-200 status code: {answer.status_code}. Skipping...')
success = False
if success:
self.logger.log_success(f"Ran action {str(action).upper()} on {self.logger.COLORS['OKCYAN']}{address}{self.logger.COLORS['ENDC']} successfully.")
def run(self):
self.run_custom_action(self.action)
def run_single_custom_action(self, index, action):
address = self.tasmota_addresses[index]
self.logger.log('Running the action "{action}" on the following device: {address}')
try: try:
answer = None log('Now trying to open the given file...', end='')
answer = requests.get(f'http://{address}/cm?cmnd=Power%20{str(action).lower()}') with open(args.data, 'r') as file:
contents = file.read()
if ';' in contents:
tasmota_addresses = contents.split(';')
elif ',' in contents:
tasmota_addresses = contents.split(',')
elif '\n' in contents:
tasmota_addresses = contents.split('\n')
else:
tasmota_addresses = [contents]
log('Done.')
log(f'Collected addresses: {tasmota_addresses}')
except FileNotFoundError:
log_error(f'Failed reading addresses. File with the path "{args.data} can\'t be opened because it doesn\'t exist. Exiting.')
sys.exit(1)
elif args.source == 'inline':
log('Now reading from inline... ', end='')
if ';' in args.data:
tasmota_addresses = args.data.split(';')
elif ',' in args.data:
tasmota_addresses = args.data.split(',')
elif '\n' in args.data:
tasmota_addresses = args.data.split('\n')
else:
tasmota_addresses = [args.data]
log('Done.')
log(f'Collected addresses: {tasmota_addresses}')
else:
log_error(f'You need to specify either "file" or "inline" as the data source for addresses!')
sys.exit(1)
# clean them up (e.g. remove newlines if the file has a random newline somewhere
tasmota_addresses_cleaned = []
for address in tasmota_addresses:
if address != '':
tasmota_addresses_cleaned.append(address.replace('\n', '').replace(';', '').replace(',', '').replace(' ', ''))
# now check data for consistency and integrity (is it really an ip address or a domain name?)
tasmota_addresses_validated = []
log('Now validating given addresses...')
for address in tasmota_addresses_cleaned:
if is_ip(address) or is_fqdn(address):
tasmota_addresses_validated.append(address)
else:
log_warning(f'The address "{address}" is neither a valid IP address nor a FQDN / domain name; SKIPPING THIS ONE.')
log('Validated given adddresses.')
# and finally turn on or off or toggle the lights!
for address in tasmota_addresses_validated:
log('Running the action "{args.action}" on the following device: {address}')
try:
answer = requests.get(f'http://{address}/cm?cmnd=Power%20{str(args.action).lower()}')
success = True success = True
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
self.logger.log_warning(f'Could not connect to "{address}". Skipping...') log_warning(f'Could not connect to "{address}". Skipping...')
success = False success = False
if answer: if answer.status_code != 200:
if answer.status_code != 200: log_warning(f'{address} returned the following non-200 status code: {answer.status_code}. Skipping...')
self.logger.log_warning(f'{address} returned the following non-200 status code: {answer.status_code}. Skipping...')
success = False
else:
self.logger.log_warning(f'{address} does not work. Skipping...')
success = False success = False
if success: if success:
self.logger.log_success(f"Ran action {str(action).upper()} on {self.logger.COLORS['OKCYAN']}{address}{self.logger.COLORS['ENDC']} successfully. ") log_success(f"Ran action {str(args.action).upper()} on {COLORS.OKCYAN}{address}{COLORS.ENDC} successfully.")
def run_single(self, index):
self.run_single_custom_action(index, self.action)
# when run as a script in CLI
if __name__ == '__main__':
parser = argparse.ArgumentParser(
prog='Tasmotonov - simply toggle multiple tasmota lights',
formatter_class=argparse.RawDescriptionHelpFormatter,
description='A very simple script which allows you to turn on/off multiple tasmota devices specified.',
epilog='Info: if you choose a file as source, this files needs to contain the addresses of the tasmota devices either comma-separated, semicolon-separated, space-separated, or newline-separated!\n\n© Benjamin Burkhardt, 2025')
parser.add_argument('source', help='Select either to read the adresses (of the devices) from a "file" or from "inline"', choices=['file', 'inline'])
parser.add_argument('data', help='Either the path to the file, or a comma-, space- or semicolon-separated list of tasmota adresses.')
parser.add_argument('action', help='Select to turn all tasmota devices "on" or "off" or "toggle" (case insensitive)', choices=['on', 'off', 'toggle'])
parser.add_argument('-v', '--verbose', help='Turn on verbose file output', action='store_true')
parser.add_argument('--version', action='version', version=f'Tasmotonov.py {version}')
args = parser.parse_args() # parse all given arguments sys.exit(0)
runner = TasmotonovRunner(source=args.source, data=args.data, action=args.action, verbose=args.verbose, enable_stdout=True)
runner.run()

View File

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1166</width> <width>630</width>
<height>546</height> <height>404</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -20,8 +20,8 @@
<enum>QTabWidget::TabShape::Rounded</enum> <enum>QTabWidget::TabShape::Rounded</enum>
</property> </property>
<widget class="QWidget" name="centralwidget"> <widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QGridLayout" name="gridLayout">
<item> <item row="0" column="1">
<widget class="QTabWidget" name="tabWidget"> <widget class="QTabWidget" name="tabWidget">
<property name="tabPosition"> <property name="tabPosition">
<enum>QTabWidget::TabPosition::East</enum> <enum>QTabWidget::TabPosition::East</enum>
@ -30,7 +30,7 @@
<enum>QTabWidget::TabShape::Rounded</enum> <enum>QTabWidget::TabShape::Rounded</enum>
</property> </property>
<property name="currentIndex"> <property name="currentIndex">
<number>0</number> <number>1</number>
</property> </property>
<property name="usesScrollButtons"> <property name="usesScrollButtons">
<bool>true</bool> <bool>true</bool>
@ -40,37 +40,20 @@
</property> </property>
<widget class="QWidget" name="tab1_from_file"> <widget class="QWidget" name="tab1_from_file">
<attribute name="title"> <attribute name="title">
<string>Bulk From File</string> <string>From File</string>
</attribute> </attribute>
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QGridLayout" name="gridLayout_2">
<item row="8" column="0"> <item row="3" column="0">
<widget class="QPushButton" name="tab1_action_pushButton"> <widget class="QTextBrowser" name="tab1_filecontent_textBrowser">
<property name="minimumSize"> <property name="frameShadow">
<size> <enum>QFrame::Shadow::Sunken</enum>
<width>414</width>
<height>0</height>
</size>
</property> </property>
<property name="text"> <property name="openExternalLinks">
<string>Run bulk action (specified below)</string> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="1"> <item row="2" column="0">
<widget class="QPushButton" name="tab1_single_action_pushButton">
<property name="text">
<string>Run single action (specified below)</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="tab1_label_help">
<property name="text">
<string>Select an item and run an action:</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="tab1_label"> <widget class="QLabel" name="tab1_label">
<property name="enabled"> <property name="enabled">
<bool>true</bool> <bool>true</bool>
@ -86,77 +69,28 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0"> <item row="8" column="0">
<widget class="QTextBrowser" name="tab1_filecontent_textBrowser"> <widget class="QPushButton" name="tab1_action_pushButton">
<property name="sizePolicy"> <property name="text">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> <string>Run!</string>
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>414</width>
<height>0</height>
</size>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Sunken</enum>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="1" column="0">
<widget class="QListWidget" name="tab1_listWidget"/>
</item>
<item row="1" column="0" colspan="2">
<widget class="QPushButton" name="tab1_load_file_pushButton"> <widget class="QPushButton" name="tab1_load_file_pushButton">
<property name="text"> <property name="text">
<string>Load file</string> <string>Load file</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0" colspan="2">
<widget class="QPushButton" name="tab1_clear_pushButton">
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab2_from_inline"> <widget class="QWidget" name="tab2_from_inline">
<attribute name="title"> <attribute name="title">
<string>Bulk From Inline</string> <string>From Inline</string>
</attribute> </attribute>
<layout class="QGridLayout" name="gridLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item row="1" column="1"> <item>
<widget class="QListWidget" name="tab2_listWidget"/>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="tab2_single_action_pushButton">
<property name="text">
<string>Run single action (specified below)</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="tab2_action_pushButton">
<property name="text">
<string>Run bulk action (specified below)!</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="tab2_label_help_single">
<property name="text">
<string>Single actions: select the address and click the button below</string>
</property>
</widget>
</item>
<item row="0" column="0" rowspan="2">
<widget class="QPlainTextEdit" name="tab2_plainTextEdit"> <widget class="QPlainTextEdit" name="tab2_plainTextEdit">
<property name="lineWrapMode"> <property name="lineWrapMode">
<enum>QPlainTextEdit::LineWrapMode::WidgetWidth</enum> <enum>QPlainTextEdit::LineWrapMode::WidgetWidth</enum>
@ -172,6 +106,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="tab2_action_pushButton">
<property name="text">
<string>Run!</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab3_output"> <widget class="QWidget" name="tab3_output">
@ -187,27 +128,13 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QTextBrowser" name="tab3_textBrowser"> <widget class="QTextBrowser" name="tab3_textBrowser"/>
<property name="acceptDrops">
<bool>false</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="tab3_clear_button">
<property name="text">
<string>Clear</string>
</property>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>
</widget> </widget>
</item> </item>
<item> <item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QRadioButton" name="radioButton_on"> <widget class="QRadioButton" name="radioButton_on">