class Window(QMainWindow): def __init__(self): super(Window, self).__init__() # Set size and centre window self.setGeometry(50, 50, 500, 400) qtRectangle = self.frameGeometry() centerPoint = QDesktopWidget().availableGeometry().center() qtRectangle.moveCenter(centerPoint) self.move(qtRectangle.topLeft()) self.setWindowTitle("Rthenticator") self.setWindowIcon(QIcon('icon.ico')) self.setStyleSheet("background-color: #2F3031") self.home() def home(self): # Init QSystemTrayIcon self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setIcon(QIcon('icon.ico')) show_action = QAction("Show", self) quit_action = QAction("Exit", self) hide_action = QAction("Hide", self) show_action.triggered.connect(self.show) hide_action.triggered.connect(self.hide) quit_action.triggered.connect(qApp.quit) tray_menu = QMenu() tray_menu.addAction(show_action) tray_menu.addAction(hide_action) tray_menu.addAction(quit_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.activated.connect(self.systemIcon) self.tray_icon.show() # Button Setup self.btnImport = QPushButton("Import", self) self.btnImport.move(50, 320) self.btnImport.setStyleSheet( "background-color: #737C7D; color: #E9E6E4") self.btnImport.clicked.connect(self.btnImportClicked) # Listbox Setup self.Listbox = QListWidget(self) self.Listbox.setAlternatingRowColors(True) self.Listbox.setFixedSize(220, 300) self.Listbox.move(10, 10) self.Listbox.setStyleSheet( "alternate-background-color: #3F4041; color: #E9E6E4;") self.Listbox.itemClicked.connect(self.listboxClicked) for key in sorted(secrets): self.Listbox.addItem(key) self.Listbox.setCurrentRow(0) self.Listbox.currentItem().setSelected(True) # Listview context menu self.Listbox.setContextMenuPolicy(Qt.CustomContextMenu) self.Listbox.customContextMenuRequested.connect(self.showMenu) self.Listbox.itemChanged.connect(self.listboxChanged) self.old_name = "" # Frame Setup self.Frame = QFrame(self) self.Frame.setFixedSize(220, 300) self.Frame.move(266, 10) self.Frame.setFrameShape(QFrame.Shape.Panel) self.Frame.setFrameShadow(QFrame.Shadow.Plain) self.Frame.setStyleSheet("color: #828790") # Progress Bar Setup self.progress = QProgressBar(self) self.progress.setGeometry(266, 325, 200, 20) self.progress.setTextVisible(False) self.progress.setStyleSheet( "QProgressBar::chunk { background: #6187CB; }") self.progress.setRange(1, 29) # Progress Bar Timer Setup self.timer = QTimer() self.timer.timeout.connect(self.progressTimer) self.timer.start(1000) # Label Setup self.label = QLabel(self) self.label.setGeometry(310, 220, 150, 40) self.label.setText("") self.label.setFont(QFont("Arial", 30, QFont.Bold)) self.label.setStyleSheet("color: #E9E6E4") self.label.setTextInteractionFlags(Qt.TextSelectableByMouse) self.image = QLabel(self) self.image.setGeometry(300, 40, 150, 150) self.Listbox.setFocus(True) self.listboxClicked() self.show() # Restore view when tray icon doubleclicked def systemIcon(self, reason): if reason == QSystemTrayIcon.DoubleClick: self.show() self.copy_auth_code() # Override closeEvent, to intercept the window closing event def closeEvent(self, event): event.ignore() self.hide() self.tray_icon.showMessage("Tray Program", "Application was minimized to Tray", QSystemTrayIcon.Information, 2000) def copy_auth_code(self): """ Copies Authentication code to the clipboard """ answer = self.Listbox.currentItem().text() totp = pyotp.TOTP(secrets[answer][0]) self.label.setText(str(totp.now())) pyperclip.copy(totp.now()) def progressTimer(self): """ Updates progress timer Copies authentication code to clipboard once timer has reached 0 and main window is not in system tray """ current_time = int(time.time() % 30) self.progress.setValue(current_time) if current_time == 0: if self.isVisible(): self.copy_auth_code() def setImage(self): """ Reads from the images directory to see if there is a matching logo and must be the same name Splits the text on a `:` png files only """ item = self.Listbox.currentItem().text().split(":")[0] fname = f"images/{item}.png" if os.path.isfile(fname): pixmap = QPixmap(fname).scaled(150, 150) self.image.setPixmap(pixmap) else: self.image.setPixmap(QPixmap()) def listboxClicked(self): """ Listbox has been clicked """ self.setImage() self.copy_auth_code() def btnImportClicked(self): """ Imports a QR-code png file and add its to secrets.json """ fileName, _ = QFileDialog.getOpenFileName( self, "QFileDialog.getOpenFileName()", "", "All Files (*)") if fileName: test = unquote( decode(Image.open(fileName))[0].data.decode("utf-8")) query = urlsplit(test).query params = parse_qs(query) start = "/totp/" end = "\\?" test = re.search(f'{start}(.*){end}', test).group(1) secrets[test] = [params['secret'][0]] self.Listbox.addItem(test) with open('secrets.json', 'w') as fh: json.dump(secrets, fh, sort_keys=True, indent=4) def showMenu(self, pos): """ Displays right click context menu, with 2 options - Rename - Allows us to rename an entry - Delete - Aloows us to remove and entry """ menu = QMenu() renameAction = menu.addAction("Rename") deleteAction = menu.addAction("Delete") action = menu.exec_(self.Listbox.viewport().mapToGlobal(pos)) if action == renameAction: this_item = self.Listbox.currentItem() self.Listbox.blockSignals( True ) # Block signals so we dont trigger the listboxChanged function this_item.setFlags( this_item.flags() | Qt.ItemIsEditable) # Allows us to edit the item self.Listbox.blockSignals(False) # Re-enables signals self.old_name = this_item.text() self.Listbox.edit(self.Listbox.currentIndex()) if action == deleteAction: self.showMessageBox() def showMessageBox(self): """ Creates and displays a message box for delete confirmation """ box = QMessageBox() box.setIcon(QMessageBox.Question) box.setWindowTitle('Warning!') box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) box.setStyleSheet("background-color: #2F3031;") box.setText( "<FONT COLOR='#E9E6E4'>Do you really wish to delete this?</FONT>") btnYes = box.button(QMessageBox.Yes) btnYes.setStyleSheet("background-color: #737C7D; color: #E9E6E4") btnYes.setText('Yes') btnNo = box.button(QMessageBox.No) btnNo.setStyleSheet("background-color: #737C7D; color: #E9E6E4") btnNo.setText('No') box.exec_() if box.clickedButton() == btnYes: items = self.Listbox.selectedItems() for item in items: new_name = item.text() self.Listbox.takeItem(self.Listbox.row(item)) secrets.pop(new_name) with open('secrets.json', 'w') as fh: json.dump(secrets, fh, sort_keys=True, indent=4) def listboxChanged(self): """ Called when we have changed text of an item in the listbox """ new_name = self.Listbox.currentItem().text() self.Listbox.blockSignals( True) # Block signals so we dont trigger ourselves this_item = self.Listbox.currentItem() this_item.setFlags(this_item.flags() & ~Qt.ItemIsEditable) # Turn off the Editable flag self.Listbox.blockSignals(False) # Re-enables signals to be processed secrets[new_name] = secrets.pop(self.old_name) with open('secrets.json', 'w') as fh: json.dump(secrets, fh, sort_keys=True, indent=4)