from __future__ import annotations from PySide6.QtCore import QSize from PySide6.QtWidgets import ( QApplication, QCheckBox, QComboBox, QFormLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QRadioButton, QSpinBox, QToolButton, QVBoxLayout, QWidget, ) from main_window.constants import SINGBOX_EDITOR_PROTOCOL_IDS, SINGBOX_EDITOR_PROTOCOL_OPTIONS class UITabsSingBoxEditorMixin: def _build_singbox_vless_editor(self, parent_layout: QVBoxLayout) -> None: grp = QGroupBox("Protocol editor (client)") self.grp_singbox_proto_editor = grp lay = QVBoxLayout(grp) self.lbl_singbox_proto_editor_info = QLabel( "Client-side fields only. Server billing/traffic/expiry fields are excluded." ) self.lbl_singbox_proto_editor_info.setStyleSheet("color: gray;") lay.addWidget(self.lbl_singbox_proto_editor_info) form = QFormLayout() self.frm_singbox_proto_form = form self.ent_singbox_proto_name = QLineEdit() self.ent_singbox_proto_name.setPlaceholderText("Profile name") form.addRow("Profile name:", self.ent_singbox_proto_name) self.chk_singbox_proto_enabled = QCheckBox("Enabled") self.chk_singbox_proto_enabled.setChecked(True) form.addRow("Enabled:", self.chk_singbox_proto_enabled) self.cmb_singbox_proto_protocol = QComboBox() for label, pid in SINGBOX_EDITOR_PROTOCOL_OPTIONS: self.cmb_singbox_proto_protocol.addItem(label, pid) self.cmb_singbox_proto_protocol.currentIndexChanged.connect( self.on_singbox_vless_editor_changed ) form.addRow("Protocol:", self.cmb_singbox_proto_protocol) self.ent_singbox_vless_server = QLineEdit() self.ent_singbox_vless_server.setPlaceholderText("example.com") form.addRow("Address:", self.ent_singbox_vless_server) self.spn_singbox_vless_port = QSpinBox() self.spn_singbox_vless_port.setRange(1, 65535) self.spn_singbox_vless_port.setValue(443) form.addRow("Port:", self.spn_singbox_vless_port) self.ent_singbox_vless_uuid = QLineEdit() self.ent_singbox_vless_uuid.setPlaceholderText("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") form.addRow("UUID:", self.ent_singbox_vless_uuid) self.ent_singbox_proto_password = QLineEdit() self.ent_singbox_proto_password.setPlaceholderText("password") form.addRow("Password:", self.ent_singbox_proto_password) self.cmb_singbox_vless_flow = QComboBox() self.cmb_singbox_vless_flow.addItem("None", "") # sing-box v1.12/v1.13 VLESS flow preset; field remains editable for custom/raw values. self.cmb_singbox_vless_flow.addItem("xtls-rprx-vision", "xtls-rprx-vision") self.cmb_singbox_vless_flow.setEditable(True) self.cmb_singbox_vless_flow.setInsertPolicy(QComboBox.NoInsert) form.addRow("Flow:", self.cmb_singbox_vless_flow) self.cmb_singbox_vless_packet_encoding = QComboBox() self.cmb_singbox_vless_packet_encoding.addItem("auto", "") self.cmb_singbox_vless_packet_encoding.addItem("xudp", "xudp") form.addRow("Packet encoding:", self.cmb_singbox_vless_packet_encoding) self.cmb_singbox_ss_method = QComboBox() self.cmb_singbox_ss_method.setEditable(True) self.cmb_singbox_ss_method.setInsertPolicy(QComboBox.NoInsert) for method in ( "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "none", ): self.cmb_singbox_ss_method.addItem(method, method) form.addRow("SS method:", self.cmb_singbox_ss_method) self.ent_singbox_ss_plugin = QLineEdit() self.ent_singbox_ss_plugin.setPlaceholderText("obfs-local;obfs=http;obfs-host=example.com") form.addRow("SS plugin:", self.ent_singbox_ss_plugin) self.spn_singbox_hy2_up_mbps = QSpinBox() self.spn_singbox_hy2_up_mbps.setRange(0, 100000) form.addRow("HY2 up mbps:", self.spn_singbox_hy2_up_mbps) self.spn_singbox_hy2_down_mbps = QSpinBox() self.spn_singbox_hy2_down_mbps.setRange(0, 100000) form.addRow("HY2 down mbps:", self.spn_singbox_hy2_down_mbps) self.ent_singbox_hy2_obfs = QLineEdit() self.ent_singbox_hy2_obfs.setPlaceholderText("salamander") form.addRow("HY2 obfs type:", self.ent_singbox_hy2_obfs) self.ent_singbox_hy2_obfs_password = QLineEdit() self.ent_singbox_hy2_obfs_password.setPlaceholderText("obfs password") form.addRow("HY2 obfs password:", self.ent_singbox_hy2_obfs_password) self.cmb_singbox_tuic_congestion = QComboBox() self.cmb_singbox_tuic_congestion.setEditable(True) self.cmb_singbox_tuic_congestion.setInsertPolicy(QComboBox.NoInsert) self.cmb_singbox_tuic_congestion.addItem("Default", "") self.cmb_singbox_tuic_congestion.addItem("bbr", "bbr") self.cmb_singbox_tuic_congestion.addItem("cubic", "cubic") self.cmb_singbox_tuic_congestion.addItem("new_reno", "new_reno") form.addRow("TUIC congestion:", self.cmb_singbox_tuic_congestion) self.cmb_singbox_tuic_udp_mode = QComboBox() self.cmb_singbox_tuic_udp_mode.addItem("Default", "") self.cmb_singbox_tuic_udp_mode.addItem("native", "native") self.cmb_singbox_tuic_udp_mode.addItem("quic", "quic") form.addRow("TUIC UDP relay:", self.cmb_singbox_tuic_udp_mode) self.chk_singbox_tuic_zero_rtt = QCheckBox("Enable zero RTT handshake") form.addRow("TUIC zero RTT:", self.chk_singbox_tuic_zero_rtt) self.ent_singbox_wg_private_key = QLineEdit() self.ent_singbox_wg_private_key.setPlaceholderText("wireguard private key") self.ent_singbox_wg_private_key.setEchoMode(QLineEdit.PasswordEchoOnEdit) form.addRow("WG private key:", self.ent_singbox_wg_private_key) self.ent_singbox_wg_peer_public_key = QLineEdit() self.ent_singbox_wg_peer_public_key.setPlaceholderText("peer public key") self.ent_singbox_wg_peer_public_key.setEchoMode(QLineEdit.PasswordEchoOnEdit) form.addRow("WG peer public key:", self.ent_singbox_wg_peer_public_key) self.ent_singbox_wg_psk = QLineEdit() self.ent_singbox_wg_psk.setPlaceholderText("pre-shared key (optional)") self.ent_singbox_wg_psk.setEchoMode(QLineEdit.PasswordEchoOnEdit) form.addRow("WG pre-shared key:", self.ent_singbox_wg_psk) self.ent_singbox_wg_local_address = QLineEdit() self.ent_singbox_wg_local_address.setPlaceholderText("10.0.0.2/32,fd00::2/128") form.addRow("WG local address:", self.ent_singbox_wg_local_address) self.ent_singbox_wg_reserved = QLineEdit() self.ent_singbox_wg_reserved.setPlaceholderText("0,0,0 (optional)") form.addRow("WG reserved:", self.ent_singbox_wg_reserved) self.spn_singbox_wg_mtu = QSpinBox() self.spn_singbox_wg_mtu.setRange(0, 9200) form.addRow("WG MTU:", self.spn_singbox_wg_mtu) self.cmb_singbox_vless_transport = QComboBox() self.cmb_singbox_vless_transport.addItem("TCP (RAW)", "tcp") self.cmb_singbox_vless_transport.addItem("WebSocket", "ws") self.cmb_singbox_vless_transport.addItem("gRPC", "grpc") self.cmb_singbox_vless_transport.addItem("HTTP", "http") self.cmb_singbox_vless_transport.addItem("HTTP Upgrade", "httpupgrade") self.cmb_singbox_vless_transport.addItem("QUIC", "quic") self.cmb_singbox_vless_transport.currentIndexChanged.connect( self.on_singbox_vless_editor_changed ) form.addRow("Transport:", self.cmb_singbox_vless_transport) self.ent_singbox_vless_path = QLineEdit() self.ent_singbox_vless_path.setPlaceholderText("/") form.addRow("Transport path:", self.ent_singbox_vless_path) self.ent_singbox_vless_grpc_service = QLineEdit() self.ent_singbox_vless_grpc_service.setPlaceholderText("service-name") form.addRow("gRPC service:", self.ent_singbox_vless_grpc_service) self.cmb_singbox_vless_security = QComboBox() self.cmb_singbox_vless_security.addItem("None", "none") self.cmb_singbox_vless_security.addItem("TLS", "tls") self.cmb_singbox_vless_security.addItem("Reality", "reality") self.cmb_singbox_vless_security.currentIndexChanged.connect( self.on_singbox_vless_editor_changed ) form.addRow("Security:", self.cmb_singbox_vless_security) self.ent_singbox_vless_sni = QLineEdit() self.ent_singbox_vless_sni.setPlaceholderText("www.example.com") form.addRow("SNI:", self.ent_singbox_vless_sni) self.ent_singbox_tls_alpn = QLineEdit() self.ent_singbox_tls_alpn.setPlaceholderText("h2,http/1.1") form.addRow("TLS ALPN:", self.ent_singbox_tls_alpn) self.cmb_singbox_vless_utls_fp = QComboBox() self.cmb_singbox_vless_utls_fp.addItem("Default", "") self.cmb_singbox_vless_utls_fp.addItem("chrome", "chrome") self.cmb_singbox_vless_utls_fp.addItem("firefox", "firefox") self.cmb_singbox_vless_utls_fp.addItem("safari", "safari") self.cmb_singbox_vless_utls_fp.addItem("edge", "edge") form.addRow("uTLS fingerprint:", self.cmb_singbox_vless_utls_fp) self.ent_singbox_vless_reality_pk = QLineEdit() self.ent_singbox_vless_reality_pk.setPlaceholderText("Reality public key") form.addRow("Reality public key:", self.ent_singbox_vless_reality_pk) self.ent_singbox_vless_reality_sid = QLineEdit() self.ent_singbox_vless_reality_sid.setPlaceholderText("short_id") form.addRow("Reality short id:", self.ent_singbox_vless_reality_sid) self.chk_singbox_vless_insecure = QCheckBox("Allow insecure TLS") form.addRow("TLS insecure:", self.chk_singbox_vless_insecure) self.chk_singbox_vless_sniff = QCheckBox("Enable sniffing for local inbound") self.chk_singbox_vless_sniff.setChecked(True) form.addRow("Sniffing:", self.chk_singbox_vless_sniff) lay.addLayout(form) wg_helpers = QHBoxLayout() self.btn_singbox_wg_paste_private = QToolButton() self.btn_singbox_wg_paste_private.setText("Paste private") self.btn_singbox_wg_paste_private.clicked.connect( lambda: self._paste_line_edit_from_clipboard(self.ent_singbox_wg_private_key) ) wg_helpers.addWidget(self.btn_singbox_wg_paste_private) self.btn_singbox_wg_copy_private = QToolButton() self.btn_singbox_wg_copy_private.setText("Copy private") self.btn_singbox_wg_copy_private.clicked.connect( lambda: self._copy_line_edit_to_clipboard(self.ent_singbox_wg_private_key) ) wg_helpers.addWidget(self.btn_singbox_wg_copy_private) self.btn_singbox_wg_paste_peer = QToolButton() self.btn_singbox_wg_paste_peer.setText("Paste peer") self.btn_singbox_wg_paste_peer.clicked.connect( lambda: self._paste_line_edit_from_clipboard(self.ent_singbox_wg_peer_public_key) ) wg_helpers.addWidget(self.btn_singbox_wg_paste_peer) self.btn_singbox_wg_copy_peer = QToolButton() self.btn_singbox_wg_copy_peer.setText("Copy peer") self.btn_singbox_wg_copy_peer.clicked.connect( lambda: self._copy_line_edit_to_clipboard(self.ent_singbox_wg_peer_public_key) ) wg_helpers.addWidget(self.btn_singbox_wg_copy_peer) self.btn_singbox_wg_paste_psk = QToolButton() self.btn_singbox_wg_paste_psk.setText("Paste PSK") self.btn_singbox_wg_paste_psk.clicked.connect( lambda: self._paste_line_edit_from_clipboard(self.ent_singbox_wg_psk) ) wg_helpers.addWidget(self.btn_singbox_wg_paste_psk) self.btn_singbox_wg_copy_psk = QToolButton() self.btn_singbox_wg_copy_psk.setText("Copy PSK") self.btn_singbox_wg_copy_psk.clicked.connect( lambda: self._copy_line_edit_to_clipboard(self.ent_singbox_wg_psk) ) wg_helpers.addWidget(self.btn_singbox_wg_copy_psk) wg_helpers.addStretch(1) self.wdg_singbox_wg_key_helpers = QWidget() self.wdg_singbox_wg_key_helpers.setLayout(wg_helpers) lay.addWidget(self.wdg_singbox_wg_key_helpers) self.lbl_singbox_proto_guardrails = QLabel("Guardrails: address/port/uuid required") self.lbl_singbox_proto_guardrails.setStyleSheet("color: gray;") lay.addWidget(self.lbl_singbox_proto_guardrails) parent_layout.addWidget(grp) self.on_singbox_vless_editor_changed() def _set_proto_form_row_visible(self, field: QWidget, visible: bool) -> None: field.setVisible(visible) label = None form = getattr(self, "frm_singbox_proto_form", None) if form is not None: try: label = form.labelForField(field) except Exception: label = None if label is not None: label.setVisible(visible) def _copy_line_edit_to_clipboard(self, field: QLineEdit) -> None: txt = str(field.text() or "").strip() if txt: QApplication.clipboard().setText(txt) def _paste_line_edit_from_clipboard(self, field: QLineEdit) -> None: txt = str(QApplication.clipboard().text() or "").strip() field.setText(txt) def _current_editor_protocol(self) -> str: return str(self.cmb_singbox_proto_protocol.currentData() or "vless").strip().lower() or "vless" def _is_supported_editor_protocol(self, protocol: str) -> bool: return str(protocol or "").strip().lower() in SINGBOX_EDITOR_PROTOCOL_IDS def on_singbox_vless_editor_changed(self, _index: int = 0) -> None: protocol = self._current_editor_protocol() self._singbox_editor_protocol = protocol transport = str(self.cmb_singbox_vless_transport.currentData() or "tcp").strip().lower() security = str(self.cmb_singbox_vless_security.currentData() or "none").strip().lower() if protocol == "vless": self.cmb_singbox_vless_security.setEnabled(True) elif protocol == "trojan": if security == "reality": idx = self.cmb_singbox_vless_security.findData("tls") self.cmb_singbox_vless_security.setCurrentIndex(idx if idx >= 0 else 1) security = "tls" self.cmb_singbox_vless_security.setEnabled(True) elif protocol in ("hysteria2", "tuic"): idx = self.cmb_singbox_vless_security.findData("tls") self.cmb_singbox_vless_security.setCurrentIndex(idx if idx >= 0 else 1) security = "tls" self.cmb_singbox_vless_security.setEnabled(False) elif protocol == "wireguard": idx = self.cmb_singbox_vless_security.findData("none") self.cmb_singbox_vless_security.setCurrentIndex(idx if idx >= 0 else 0) security = "none" self.cmb_singbox_vless_security.setEnabled(False) else: idx = self.cmb_singbox_vless_security.findData("none") self.cmb_singbox_vless_security.setCurrentIndex(idx if idx >= 0 else 0) security = "none" self.cmb_singbox_vless_security.setEnabled(False) path_needed = transport in ("ws", "http", "httpupgrade") grpc_needed = transport == "grpc" transport_supported = protocol in ("vless", "trojan") self.cmb_singbox_vless_transport.setEnabled(transport_supported) self.ent_singbox_vless_path.setEnabled(transport_supported and path_needed) self.ent_singbox_vless_grpc_service.setEnabled(transport_supported and grpc_needed) tls_like = security in ("tls", "reality") reality = security == "reality" self.ent_singbox_vless_sni.setEnabled(tls_like) self.ent_singbox_tls_alpn.setEnabled(tls_like) self.cmb_singbox_vless_utls_fp.setEnabled(tls_like) self.chk_singbox_vless_insecure.setEnabled(tls_like) self.ent_singbox_vless_reality_pk.setEnabled(reality) self.ent_singbox_vless_reality_sid.setEnabled(reality) show_vless_auth = protocol == "vless" show_password = protocol in ("trojan", "shadowsocks", "hysteria2", "tuic") show_ss = protocol == "shadowsocks" show_hy2 = protocol == "hysteria2" show_tuic = protocol == "tuic" show_wg = protocol == "wireguard" self._set_proto_form_row_visible(self.ent_singbox_vless_uuid, show_vless_auth or show_tuic) self._set_proto_form_row_visible(self.ent_singbox_proto_password, show_password) self._set_proto_form_row_visible(self.cmb_singbox_vless_flow, show_vless_auth) self._set_proto_form_row_visible(self.cmb_singbox_vless_packet_encoding, show_vless_auth) self._set_proto_form_row_visible(self.cmb_singbox_ss_method, show_ss) self._set_proto_form_row_visible(self.ent_singbox_ss_plugin, show_ss) self._set_proto_form_row_visible(self.spn_singbox_hy2_up_mbps, show_hy2) self._set_proto_form_row_visible(self.spn_singbox_hy2_down_mbps, show_hy2) self._set_proto_form_row_visible(self.ent_singbox_hy2_obfs, show_hy2) self._set_proto_form_row_visible(self.ent_singbox_hy2_obfs_password, show_hy2) self._set_proto_form_row_visible(self.cmb_singbox_tuic_congestion, show_tuic) self._set_proto_form_row_visible(self.cmb_singbox_tuic_udp_mode, show_tuic) self._set_proto_form_row_visible(self.chk_singbox_tuic_zero_rtt, show_tuic) self._set_proto_form_row_visible(self.ent_singbox_wg_private_key, show_wg) self._set_proto_form_row_visible(self.ent_singbox_wg_peer_public_key, show_wg) self._set_proto_form_row_visible(self.ent_singbox_wg_psk, show_wg) self._set_proto_form_row_visible(self.ent_singbox_wg_local_address, show_wg) self._set_proto_form_row_visible(self.ent_singbox_wg_reserved, show_wg) self._set_proto_form_row_visible(self.spn_singbox_wg_mtu, show_wg) self.wdg_singbox_wg_key_helpers.setVisible(show_wg) self._set_proto_form_row_visible(self.cmb_singbox_vless_transport, transport_supported) self._set_proto_form_row_visible(self.ent_singbox_vless_path, transport_supported) self._set_proto_form_row_visible(self.ent_singbox_vless_grpc_service, transport_supported) self._set_proto_form_row_visible(self.cmb_singbox_vless_security, protocol not in ("shadowsocks", "wireguard")) self._set_proto_form_row_visible(self.ent_singbox_vless_sni, tls_like) self._set_proto_form_row_visible(self.ent_singbox_tls_alpn, tls_like) self._set_proto_form_row_visible(self.cmb_singbox_vless_utls_fp, tls_like) self._set_proto_form_row_visible(self.chk_singbox_vless_insecure, tls_like) self._set_proto_form_row_visible(self.ent_singbox_vless_reality_pk, reality) self._set_proto_form_row_visible(self.ent_singbox_vless_reality_sid, reality) tips = ["Guardrails:"] if protocol == "vless": tips.append("address/port/uuid required") elif protocol == "trojan": tips.append("address/port/password required") elif protocol == "shadowsocks": tips.append("address/port/SS method/password required") elif protocol == "hysteria2": tips.append("address/port/password required") elif protocol == "tuic": tips.append("address/port/uuid/password required") elif protocol == "wireguard": tips.append("address/port/private_key/peer_public_key/local_address required") if reality: tips.append("reality.public_key is required") if transport_supported and grpc_needed: tips.append("gRPC service is required") if transport_supported and path_needed: tips.append("transport path is required") self.lbl_singbox_proto_guardrails.setText(" | ".join(tips))