|
| 1 | +import tkinter as tk |
| 2 | +from tkinter import ttk, filedialog, messagebox |
| 3 | + |
| 4 | +# Modifier masks |
| 5 | +MOD_NONE = 0x00 |
| 6 | +MOD_CTRL = 0x01 |
| 7 | +MOD_SHIFT = 0x02 |
| 8 | +MOD_ALT = 0x04 |
| 9 | +MOD_GUI = 0x08 # Windows (GUI) key |
| 10 | + |
| 11 | +# Keycodes set (partial, extend as needed) |
| 12 | +KEY_CODES = { |
| 13 | + 'a': 0x04, 'b': 0x05, 'c': 0x06, 'd': 0x07, 'e': 0x08, 'f': 0x09, 'g': 0x0A, |
| 14 | + 'h': 0x0B, 'i': 0x0C, 'j': 0x0D, 'k': 0x0E, 'l': 0x0F, 'm': 0x10, 'n': 0x11, |
| 15 | + 'o': 0x12, 'p': 0x13, 'q': 0x14, 'r': 0x15, 's': 0x16, 't': 0x17, 'u': 0x18, |
| 16 | + 'v': 0x19, 'w': 0x1A, 'x': 0x1B, 'y': 0x1C, 'z': 0x1D, |
| 17 | + |
| 18 | + '1': 0x1E, '2': 0x1F, '3': 0x20, '4': 0x21, '5': 0x22, |
| 19 | + '6': 0x23, '7': 0x24, '8': 0x25, '9': 0x26, '0': 0x27, |
| 20 | + |
| 21 | + 'ENTER': 0x28, 'ESC': 0x29, 'BACKSPACE': 0x2A, 'TAB': 0x2B, 'SPACE': 0x2C, |
| 22 | + 'MINUS': 0x2D, 'EQUAL': 0x2E, 'LEFTBRACE': 0x2F, 'RIGHTBRACE': 0x30, 'BACKSLASH': 0x31, |
| 23 | + 'SEMICOLON': 0x33, 'APOSTROPHE': 0x34, 'GRAVE': 0x35, 'COMMA': 0x36, 'DOT': 0x37, |
| 24 | + 'SLASH': 0x38, |
| 25 | +} |
| 26 | + |
| 27 | +SHIFT_CHARACTERS = { |
| 28 | + '!': '1', '@': '2', '#': '3', '$': '4', '%': '5', |
| 29 | + '^': '6', '&': '7', '*': '8', '(': '9', ')': '0', |
| 30 | + '_': 'MINUS', '+': 'EQUAL', '{': 'LEFTBRACE', '}': 'RIGHTBRACE', |
| 31 | + '|': 'BACKSLASH', ':': 'SEMICOLON', '"': 'APOSTROPHE', |
| 32 | + '~': 'GRAVE', '<': 'COMMA', '>': 'DOT', '?': 'SLASH' |
| 33 | +} |
| 34 | + |
| 35 | +def char_to_keycode(c): |
| 36 | + if c.isalpha(): |
| 37 | + mod = MOD_SHIFT if c.isupper() else MOD_NONE |
| 38 | + key = KEY_CODES[c.lower()] |
| 39 | + return (mod, key) |
| 40 | + elif c.isdigit(): |
| 41 | + return (MOD_NONE, KEY_CODES[c]) |
| 42 | + elif c == ' ': |
| 43 | + return (MOD_NONE, KEY_CODES['SPACE']) |
| 44 | + elif c in SHIFT_CHARACTERS: |
| 45 | + mod = MOD_SHIFT |
| 46 | + base_char = SHIFT_CHARACTERS[c] |
| 47 | + key = KEY_CODES[base_char] |
| 48 | + return (mod, key) |
| 49 | + else: |
| 50 | + symbol_map = { |
| 51 | + '-': 'MINUS', '=': 'EQUAL', '[': 'LEFTBRACE', ']': 'RIGHTBRACE', |
| 52 | + '\\': 'BACKSLASH', ';': 'SEMICOLON', '\'': 'APOSTROPHE', '`': 'GRAVE', |
| 53 | + ',': 'COMMA', '.': 'DOT', '/': 'SLASH' |
| 54 | + } |
| 55 | + if c in symbol_map: |
| 56 | + return (MOD_NONE, KEY_CODES[symbol_map[c]]) |
| 57 | + return (MOD_NONE, KEY_CODES['SPACE']) # fallback |
| 58 | + |
| 59 | +def convert_duckyscript_to_arduino(code: str, layout='US', sketch_name='duckify_sketch'): |
| 60 | + lines = code.strip().splitlines() |
| 61 | + output = [] |
| 62 | + output_message = (""" |
| 63 | +//+-----------------------------------+ |
| 64 | +//|This sketch is converted by Sparky.| |
| 65 | +//| .-------. | |
| 66 | +//| By | PXZYA | | |
| 67 | +//| '-------' | |
| 68 | +//+-----------------------------------+ |
| 69 | +""") |
| 70 | + output.append(output_message) |
| 71 | + output.append(f'//-> Platform: Digispark') |
| 72 | + output.append(f'//-> Keyboard Layout: {layout}\n') |
| 73 | + output.append('#include "DigiKeyboard.h"\n') |
| 74 | + |
| 75 | + string_arrays = [] |
| 76 | + string_index = 0 |
| 77 | + setup_lines = [] |
| 78 | + |
| 79 | + default_delay = 0 |
| 80 | + has_default_delay = False |
| 81 | + |
| 82 | + def progmem_array(string): |
| 83 | + arr = [] |
| 84 | + for c in string: |
| 85 | + mod, key = char_to_keycode(c) |
| 86 | + arr.append(mod) |
| 87 | + arr.append(key) |
| 88 | + return arr |
| 89 | + |
| 90 | + for line in lines: |
| 91 | + stripped = line.strip() |
| 92 | + if not stripped or stripped.startswith('//'): |
| 93 | + # Preserve comments |
| 94 | + if stripped.startswith('//'): |
| 95 | + output.append(stripped) |
| 96 | + continue |
| 97 | + |
| 98 | + parts = stripped.split() |
| 99 | + cmd = parts[0].upper() |
| 100 | + |
| 101 | + if cmd == 'DELAY': |
| 102 | + ms = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 0 |
| 103 | + setup_lines.append(f' DigiKeyboard.delay({ms}); // DELAY {ms}') |
| 104 | + continue |
| 105 | + |
| 106 | + if cmd == 'DEFAULTDELAY' or cmd == 'DEFAULT_DELAY': |
| 107 | + ms = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 0 |
| 108 | + default_delay = ms |
| 109 | + has_default_delay = True |
| 110 | + setup_lines.append(f' DigiKeyboard.delay({ms}); // DEFAULTDELAY {ms}') |
| 111 | + continue |
| 112 | + |
| 113 | + if cmd == 'STRING': |
| 114 | + string_content = stripped[7:] |
| 115 | + array_name = f'key_arr_{string_index}' |
| 116 | + string_index += 1 |
| 117 | + bytes_arr = progmem_array(string_content) |
| 118 | + bytes_str = ', '.join(str(b) for b in bytes_arr) |
| 119 | + output.append(f'// {string_content}') |
| 120 | + output.append(f'const uint8_t {array_name}[] PROGMEM = {{{bytes_str}}};') |
| 121 | + setup_lines.append(f' duckyString({array_name}, sizeof({array_name})); // STRING {string_content}') |
| 122 | + if has_default_delay: |
| 123 | + setup_lines.append(f' DigiKeyboard.delay({default_delay});') |
| 124 | + continue |
| 125 | + |
| 126 | + if cmd == 'ENTER': |
| 127 | + setup_lines.append(f' DigiKeyboard.sendKeyStroke({KEY_CODES["ENTER"]}, 0); // ENTER') |
| 128 | + if has_default_delay: |
| 129 | + setup_lines.append(f' DigiKeyboard.delay({default_delay});') |
| 130 | + continue |
| 131 | + |
| 132 | + # Handle modifiers + key combos like GUI r |
| 133 | + modifiers = 0 |
| 134 | + keycode = None |
| 135 | + mod_map = {'CTRL': MOD_CTRL, 'CONTROL': MOD_CTRL, 'ALT': MOD_ALT, 'SHIFT': MOD_SHIFT, 'GUI': MOD_GUI, 'WINDOWS': MOD_GUI} |
| 136 | + |
| 137 | + # Count modifiers in all but last token, last token is key |
| 138 | + for p in parts[:-1]: |
| 139 | + if p.upper() in mod_map: |
| 140 | + modifiers |= mod_map[p.upper()] |
| 141 | + last_token = parts[-1].lower() |
| 142 | + |
| 143 | + if last_token in KEY_CODES: |
| 144 | + keycode = KEY_CODES[last_token] |
| 145 | + elif len(last_token) == 1: |
| 146 | + # single char |
| 147 | + mod, kc = char_to_keycode(last_token) |
| 148 | + if mod != MOD_NONE: |
| 149 | + modifiers |= mod |
| 150 | + keycode = kc |
| 151 | + else: |
| 152 | + # unknown fallback to 'r' |
| 153 | + keycode = KEY_CODES['r'] |
| 154 | + |
| 155 | + if cmd.upper() in mod_map and len(parts) == 1: |
| 156 | + # Single modifier press? Ignore for now or could handle if needed |
| 157 | + continue |
| 158 | + |
| 159 | + # Compose sendKeyStroke for modifier+key |
| 160 | + setup_lines.append(f' DigiKeyboard.sendKeyStroke({keycode}, {modifiers}); // {" ".join(parts)}') |
| 161 | + if has_default_delay: |
| 162 | + setup_lines.append(f' DigiKeyboard.delay({default_delay});') |
| 163 | + |
| 164 | + output.append('') |
| 165 | + output.append('void duckyString(const uint8_t* keys, size_t len) { ') |
| 166 | + output.append(' for(size_t i=0; i<len; i+=2) {') |
| 167 | + output.append(' DigiKeyboard.sendKeyStroke(pgm_read_byte_near(keys + i+1), pgm_read_byte_near(keys + i));') |
| 168 | + output.append(' }') |
| 169 | + output.append('}\n') |
| 170 | + |
| 171 | + output.append('void setup() {') |
| 172 | + output.append(' pinMode(1, OUTPUT); // Enable LED') |
| 173 | + output.append(' digitalWrite(1, LOW); // Turn LED off') |
| 174 | + output.append(' DigiKeyboard.sendKeyStroke(0); // Tell computer no key is pressed\n') |
| 175 | + |
| 176 | + output.extend(setup_lines) |
| 177 | + |
| 178 | + output.append('}\n') |
| 179 | + output.append('void loop() {}') |
| 180 | + output.append('') |
| 181 | + output.append('// Created by Sparky') |
| 182 | + |
| 183 | + return '\n'.join(output) |
| 184 | + |
| 185 | + |
| 186 | +class sparklingGUI: |
| 187 | + def __init__(self, root): |
| 188 | + self.root = root |
| 189 | + self.root.title("Sparky Script Converter") |
| 190 | + self.root.geometry("470x700") |
| 191 | + self.root.configure(bg="#f4f6f8") |
| 192 | + self.root.iconbitmap("src/icon.ico") |
| 193 | + |
| 194 | + |
| 195 | + # Title |
| 196 | + tk.Label(root, text="Sparky", font=("Segoe UI", 18, "bold"), bg="#f4f6f8", fg="#000000").pack(pady=10) |
| 197 | + tk.Label(root, text="Convert Ducky Script → Arduino (.ino) for Digispark", bg="#f4f6f8").pack(pady=5) |
| 198 | + |
| 199 | + # Layout / name inputs |
| 200 | + frame_top = tk.Frame(root, bg="#f4f6f8") |
| 201 | + frame_top.pack(fill=tk.X, padx=20, pady=10) |
| 202 | + |
| 203 | + tk.Label(frame_top, text="Keyboard Layout:", bg="#f4f6f8").grid(row=0, column=0, sticky="w") |
| 204 | + self.layout_var = tk.StringVar(value="US") |
| 205 | + ttk.Combobox(frame_top, textvariable=self.layout_var, values=["US", "DE", "FR", "GB"], width=10, state="readonly").grid(row=0, column=1, padx=5) |
| 206 | + |
| 207 | + tk.Label(frame_top, text="Sketch Name:", bg="#f4f6f8").grid(row=0, column=2, sticky="w", padx=(20, 0)) |
| 208 | + self.name_var = tk.StringVar(value="sparky_sketch") |
| 209 | + tk.Entry(frame_top, textvariable=self.name_var, width=20).grid(row=0, column=3, padx=5) |
| 210 | + |
| 211 | + # Script input |
| 212 | + tk.Label(root, text="Ducky Script Input:", bg="#f4f6f8").pack(anchor="w", padx=20) |
| 213 | + self.script_text = tk.Text(root, height=12, font=("Consolas", 11)) |
| 214 | + self.script_text.pack(fill=tk.BOTH, expand=False, padx=20, pady=(0, 10)) |
| 215 | + |
| 216 | + # Buttons |
| 217 | + frame_buttons = tk.Frame(root, bg="#f4f6f8") |
| 218 | + frame_buttons.pack(pady=5) |
| 219 | + ttk.Button(frame_buttons, text="Convert", command=self.convert_script).grid(row=0, column=0, padx=10) |
| 220 | + ttk.Button(frame_buttons, text="Save as .ino", command=self.save_file).grid(row=0, column=1, padx=10) |
| 221 | + ttk.Button(frame_buttons, text="Clear", command=self.clear_text).grid(row=0, column=2, padx=10) |
| 222 | + ttk.Button(frame_buttons, text="Exit", command=root.quit).grid(row=0, column=3, padx=10) |
| 223 | + |
| 224 | + # Output |
| 225 | + tk.Label(root, text="Arduino IDE Sketch Output (.ino):", bg="#f4f6f8").pack(anchor="w", padx=20) |
| 226 | + self.output_text = tk.Text(root, height=18, font=("Consolas", 11), bg="#f0f0f0") |
| 227 | + self.output_text.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 20)) |
| 228 | + self.output_text.configure(state="disabled") |
| 229 | + |
| 230 | + def convert_script(self): |
| 231 | + script = self.script_text.get("1.0", tk.END) |
| 232 | + layout = self.layout_var.get() |
| 233 | + name = self.name_var.get().strip() or "sparkling_sketch" |
| 234 | + try: |
| 235 | + result = convert_duckyscript_to_arduino(script, layout, name) |
| 236 | + self.output_text.configure(state="normal") |
| 237 | + self.output_text.delete("1.0", tk.END) |
| 238 | + self.output_text.insert(tk.END, result) |
| 239 | + self.output_text.configure(state="disabled") |
| 240 | +# messagebox.showinfo("Success", "Conversion complete!") |
| 241 | + except Exception as e: |
| 242 | + messagebox.showerror("Error", str(e)) |
| 243 | + |
| 244 | + def save_file(self): |
| 245 | + output = self.output_text.get("1.0", tk.END).strip() |
| 246 | + if not output: |
| 247 | + messagebox.showwarning("No Output", "Please convert a script first.") |
| 248 | + return |
| 249 | + file = filedialog.asksaveasfilename( |
| 250 | + defaultextension=".ino", |
| 251 | + filetypes=[("Arduino Sketch", "*.ino"), ("Text files", "*.txt")], |
| 252 | + title="Save Arduino Sketch" |
| 253 | + ) |
| 254 | + if file: |
| 255 | + with open(file, "w", encoding="utf-8") as f: |
| 256 | + f.write(output) |
| 257 | + messagebox.showinfo("Saved", f"File saved as:\n{file}") |
| 258 | + |
| 259 | + def clear_text(self): |
| 260 | + self.script_text.delete("1.0", tk.END) |
| 261 | + self.output_text.configure(state="normal") |
| 262 | + self.output_text.delete("1.0", tk.END) |
| 263 | + self.output_text.configure(state="disabled") |
| 264 | + |
| 265 | + |
| 266 | +if __name__ == "__main__": |
| 267 | + root = tk.Tk() |
| 268 | + app = sparklingGUI(root) |
| 269 | + root.mainloop() |
0 commit comments