KeyboardPaster 0.1.24__tar.gz → 0.1.26__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/KeyboardPaster.egg-info/PKG-INFO +25 -3
  2. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/KeyboardPaster.egg-info/SOURCES.txt +6 -1
  3. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/KeyboardPaster.egg-info/requires.txt +2 -2
  4. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/PKG-INFO +25 -3
  5. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/README.md +22 -0
  6. keyboardpaster-0.1.26/keyboardpaster/app.py +503 -0
  7. keyboardpaster-0.1.26/keyboardpaster/bootstrap.py +20 -0
  8. keyboardpaster-0.1.26/keyboardpaster/desktop.py +96 -0
  9. keyboardpaster-0.1.26/keyboardpaster/keyboard_input.py +705 -0
  10. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/keyboardpaster/keyboard_layout_detector.py +22 -0
  11. keyboardpaster-0.1.26/keyboardpaster/keyboardpaster_app.kv +324 -0
  12. keyboardpaster-0.1.26/keyboardpaster/res/images/keyboardpaster.png +0 -0
  13. keyboardpaster-0.1.26/keyboardpaster/shared_resources.py +15 -0
  14. keyboardpaster-0.1.26/keyboardpaster.desktop +10 -0
  15. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/setup.py +13 -4
  16. keyboardpaster-0.1.24/keyboardpaster/app.py +0 -407
  17. keyboardpaster-0.1.24/keyboardpaster/keyboardpaster_app.kv +0 -173
  18. keyboardpaster-0.1.24/keyboardpaster/shared_resources.py +0 -1
  19. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/KeyboardPaster.egg-info/dependency_links.txt +0 -0
  20. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/KeyboardPaster.egg-info/entry_points.txt +0 -0
  21. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/KeyboardPaster.egg-info/top_level.txt +0 -0
  22. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/LICENSE +0 -0
  23. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/keyboardpaster/__init__.py +0 -0
  24. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/keyboardpaster/modules/__init__.py +0 -0
  25. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/keyboardpaster/modules/autoupdate.py +0 -0
  26. {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: KeyboardPaster
3
- Version: 0.1.24
3
+ Version: 0.1.26
4
4
  Summary: A Python package to type text using the keyboard module
5
5
  Home-page: https://github.com/KnutssonDevelopment/KeyboardPaster
6
6
  Author: Brian Knutsson
@@ -19,8 +19,8 @@ Requires-Python: >=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: pynput
22
- Requires-Dist: kivy
23
- Requires-Dist: kivymd
22
+ Requires-Dist: kivy<2.4,>=2.2
23
+ Requires-Dist: kivymd<2,>=1.2
24
24
  Requires-Dist: requests
25
25
  Provides-Extra: windows
26
26
  Requires-Dist: pywin32; extra == "windows"
@@ -48,10 +48,32 @@ A Python package to type text using the keyboard module. This is useful in situa
48
48
  - Paste 10 different strings using the keyboard
49
49
  - Save string for future usage
50
50
 
51
+ ## Install (Linux / pipx)
52
+
53
+ ```bash
54
+ pipx install keyboardpaster
55
+ ```
56
+
57
+ KivyMD is pinned to 1.x so pipx does not need to compile `pycairo`.
58
+
59
+ If you install KivyMD 2.x yourself, or a dependency pulls in `pycairo`, install Cairo headers first:
60
+
61
+ ```bash
62
+ sudo apt install libcairo2-dev pkg-config
63
+ ```
64
+
65
+ On GNOME Wayland you also need `ydotool` / `ydotoold` for typing into other apps.
66
+
51
67
  ## Future features
52
68
  - Paste images as base64 code
53
69
 
54
70
  ## Changes
71
+ ### v0.1.26
72
+ - BUGFIX: Pin KivyMD to 1.x so pipx install does not require pycairo/Cairo headers
73
+
74
+ ### v0.1.25
75
+ - BUGFIX: Dropbox menu resulted in crash when multiple profiles were available
76
+
55
77
  ### v0.1.24
56
78
  - BUGFIX: Corrected Python requirement
57
79
 
@@ -1,5 +1,6 @@
1
1
  LICENSE
2
2
  README.md
3
+ keyboardpaster.desktop
3
4
  setup.py
4
5
  KeyboardPaster.egg-info/PKG-INFO
5
6
  KeyboardPaster.egg-info/SOURCES.txt
@@ -9,8 +10,12 @@ KeyboardPaster.egg-info/requires.txt
9
10
  KeyboardPaster.egg-info/top_level.txt
10
11
  keyboardpaster/__init__.py
11
12
  keyboardpaster/app.py
13
+ keyboardpaster/bootstrap.py
14
+ keyboardpaster/desktop.py
15
+ keyboardpaster/keyboard_input.py
12
16
  keyboardpaster/keyboard_layout_detector.py
13
17
  keyboardpaster/keyboardpaster_app.kv
14
18
  keyboardpaster/shared_resources.py
15
19
  keyboardpaster/modules/__init__.py
16
- keyboardpaster/modules/autoupdate.py
20
+ keyboardpaster/modules/autoupdate.py
21
+ keyboardpaster/res/images/keyboardpaster.png
@@ -1,6 +1,6 @@
1
1
  pynput
2
- kivy
3
- kivymd
2
+ kivy<2.4,>=2.2
3
+ kivymd<2,>=1.2
4
4
  requests
5
5
 
6
6
  [linux]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: KeyboardPaster
3
- Version: 0.1.24
3
+ Version: 0.1.26
4
4
  Summary: A Python package to type text using the keyboard module
5
5
  Home-page: https://github.com/KnutssonDevelopment/KeyboardPaster
6
6
  Author: Brian Knutsson
@@ -19,8 +19,8 @@ Requires-Python: >=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: pynput
22
- Requires-Dist: kivy
23
- Requires-Dist: kivymd
22
+ Requires-Dist: kivy<2.4,>=2.2
23
+ Requires-Dist: kivymd<2,>=1.2
24
24
  Requires-Dist: requests
25
25
  Provides-Extra: windows
26
26
  Requires-Dist: pywin32; extra == "windows"
@@ -48,10 +48,32 @@ A Python package to type text using the keyboard module. This is useful in situa
48
48
  - Paste 10 different strings using the keyboard
49
49
  - Save string for future usage
50
50
 
51
+ ## Install (Linux / pipx)
52
+
53
+ ```bash
54
+ pipx install keyboardpaster
55
+ ```
56
+
57
+ KivyMD is pinned to 1.x so pipx does not need to compile `pycairo`.
58
+
59
+ If you install KivyMD 2.x yourself, or a dependency pulls in `pycairo`, install Cairo headers first:
60
+
61
+ ```bash
62
+ sudo apt install libcairo2-dev pkg-config
63
+ ```
64
+
65
+ On GNOME Wayland you also need `ydotool` / `ydotoold` for typing into other apps.
66
+
51
67
  ## Future features
52
68
  - Paste images as base64 code
53
69
 
54
70
  ## Changes
71
+ ### v0.1.26
72
+ - BUGFIX: Pin KivyMD to 1.x so pipx install does not require pycairo/Cairo headers
73
+
74
+ ### v0.1.25
75
+ - BUGFIX: Dropbox menu resulted in crash when multiple profiles were available
76
+
55
77
  ### v0.1.24
56
78
  - BUGFIX: Corrected Python requirement
57
79
 
@@ -6,10 +6,32 @@ A Python package to type text using the keyboard module. This is useful in situa
6
6
  - Paste 10 different strings using the keyboard
7
7
  - Save string for future usage
8
8
 
9
+ ## Install (Linux / pipx)
10
+
11
+ ```bash
12
+ pipx install keyboardpaster
13
+ ```
14
+
15
+ KivyMD is pinned to 1.x so pipx does not need to compile `pycairo`.
16
+
17
+ If you install KivyMD 2.x yourself, or a dependency pulls in `pycairo`, install Cairo headers first:
18
+
19
+ ```bash
20
+ sudo apt install libcairo2-dev pkg-config
21
+ ```
22
+
23
+ On GNOME Wayland you also need `ydotool` / `ydotoold` for typing into other apps.
24
+
9
25
  ## Future features
10
26
  - Paste images as base64 code
11
27
 
12
28
  ## Changes
29
+ ### v0.1.26
30
+ - BUGFIX: Pin KivyMD to 1.x so pipx install does not require pycairo/Cairo headers
31
+
32
+ ### v0.1.25
33
+ - BUGFIX: Dropbox menu resulted in crash when multiple profiles were available
34
+
13
35
  ### v0.1.24
14
36
  - BUGFIX: Corrected Python requirement
15
37
 
@@ -0,0 +1,503 @@
1
+ import re
2
+ import json
3
+ import subprocess
4
+ import importlib.resources
5
+ import keyboardpaster
6
+
7
+ from keyboardpaster.bootstrap import configure_kivy_environment
8
+
9
+ configure_kivy_environment()
10
+
11
+ from kivymd.app import MDApp
12
+ from kivy.properties import ObjectProperty, StringProperty, BooleanProperty
13
+ from kivy.uix.textinput import TextInput
14
+ from kivy.lang import Builder
15
+ from kivy.clock import Clock
16
+ from kivy.core.window import Window
17
+ from kivy.factory import Factory
18
+ from kivy.metrics import dp
19
+ from kivy.uix.dropdown import DropDown
20
+ from kivymd.uix.selectioncontrol.selectioncontrol import MDCheckbox
21
+ from kivymd.uix.button.button import MDRaisedButton
22
+ from kivymd.uix.dialog import MDDialog
23
+ from kivymd.uix.textfield import MDTextField
24
+
25
+ from keyboardpaster.keyboard_input import type_string
26
+ from keyboardpaster.keyboard_layout_detector import get_keyboard_layout
27
+ from keyboardpaster.modules.autoupdate import autoupdate
28
+ from keyboardpaster.shared_resources import app_version, app_icon_path
29
+ from keyboardpaster.desktop import ensure_user_desktop_entry
30
+
31
+ SPECIAL_CHARS_SHIFT = {
32
+ 'EN_US': {
33
+ '~': '`', '!': '1', '@': '2', '#': '3', '$': '4',
34
+ '%': '5', '^': '6', '&': '7', '*': '8', '(': '9',
35
+ ')': '0', '_': '-', '+': '=', '{': '[', '}': ']',
36
+ '|': '\\', ':': ';', '"': "'", '<': ',', '>': '.',
37
+ '?': '/'
38
+ },
39
+ 'DA_DK': {
40
+ '½': '§', '!': '1', '"': '2', '#': '3', '¤': '4', '%': '5',
41
+ '&': '6', '/': '7', '(': '8', ')': '9', '=': '0',
42
+ '?': '+', '`': '§', ':': ';', ';': ',', '>': '<', '_': '/',
43
+ '*': '\\'
44
+ }
45
+ }
46
+
47
+ SPECIAL_CHARS_ALT_GR = {
48
+ 'EN_US': {},
49
+ 'DA_DK': {
50
+ '$': '4', '£': '3', '@': '2', '{': '7', '}': '0', '[': '8',
51
+ ']': '9', '|': '´', '€': 'E',
52
+ # ¨ key (physical ]); used by pynput path. ydotool uses DA_DK_YDOTOOL_COMBO.
53
+ '~': ']',
54
+ }
55
+ }
56
+
57
+ # Dynamic window sizing around slots (matches keyboardpaster_app.kv dp values)
58
+ WINDOW_WIDTH = int(dp(960))
59
+ HEADER_HEIGHT = int(dp(68))
60
+ FOOTER_HEIGHT = int(dp(72))
61
+ BODY_TOP_PADDING = int(dp(12))
62
+ SLOT_HEIGHT = int(dp(56))
63
+ SLOT_SPACING = int(dp(10))
64
+ SLOT_AREA_PADDING_V = int(dp(16)) # container padding top+bottom
65
+ MAX_WINDOW_HEIGHT = int(dp(720))
66
+
67
+ """
68
+ SPECIAL_CHARS_SPACE = {
69
+ 'EN_US': {},
70
+ 'DA_DK': {
71
+ '^': '¨'
72
+ }
73
+ }
74
+ """
75
+
76
+ def type_string_with_delay(
77
+ text: str,
78
+ start_delay: float = 3.0,
79
+ mod_delay: float = 0.1,
80
+ keypress_delay: float = 0.1,
81
+ layout: str = 'EN_US',
82
+ end_line: bool = False,
83
+ on_error=None,
84
+ ) -> None:
85
+ """
86
+ Types the given text using the keyboard module after a defined start delay, with an optional delay between keypresses.
87
+
88
+ :param text: The text to be typed.
89
+ :param start_delay: The delay before typing starts in seconds. Default is 3.0 seconds.
90
+ :param keypress_delay: The delay between keypresses in seconds. Default is 0.1 seconds.
91
+ :param layout: The keyboard layout to use. Default is 'EN_US'.
92
+ :param end_line: Should end the paste with a ENTER press.
93
+ :param on_error: Optional callback invoked if typing fails after the start delay.
94
+ """
95
+
96
+ def type_with_delay_callback(dt):
97
+ try:
98
+ special_chars_shift = SPECIAL_CHARS_SHIFT.get(layout, SPECIAL_CHARS_SHIFT["EN_US"])
99
+ special_chars_alt_gr = SPECIAL_CHARS_ALT_GR.get(layout, SPECIAL_CHARS_ALT_GR["EN_US"])
100
+ type_string(
101
+ text,
102
+ delay=keypress_delay,
103
+ mod_delay=mod_delay,
104
+ layout=layout,
105
+ end_line=end_line,
106
+ special_chars_shift=special_chars_shift,
107
+ special_chars_alt_gr=special_chars_alt_gr,
108
+ )
109
+ except Exception as error:
110
+ if on_error:
111
+ on_error(error)
112
+ else:
113
+ raise
114
+
115
+ Clock.schedule_once(type_with_delay_callback, start_delay)
116
+
117
+
118
+ class KeyboardPasterApp(MDApp):
119
+ layout = StringProperty('EN_US')
120
+ version_text = StringProperty(f"v{app_version}")
121
+ start_delay = ObjectProperty(None)
122
+ layout = 'EN_US'
123
+ _updating_slots = BooleanProperty(False)
124
+
125
+ def __init__(self):
126
+ super().__init__()
127
+ self.config = None
128
+ self.json_config = 'saved_inputs.json'
129
+ # Strong ref: KV ids only keep a WeakProxy, so DropDown was GC'd after dismiss.
130
+ self.profile_dropdown = DropDown()
131
+
132
+ def build(self):
133
+ self.theme_cls.primary_palette = "Amber"
134
+ self.theme_cls.accent_palette = "Orange"
135
+ self.theme_cls.theme_style = "Dark"
136
+ Window.clearcolor = (0.08, 0.08, 0.08, 1)
137
+
138
+ self.read_config()
139
+
140
+ self.detect_keyboard_layout()
141
+ Clock.schedule_once(self.set_profile, 1)
142
+ self.title = f"Keyboard Paster v{app_version}"
143
+ Window.size = (WINDOW_WIDTH, self._height_for_slot_count(1))
144
+ icon = app_icon_path()
145
+ if icon is not None:
146
+ Window.set_icon(str(icon))
147
+
148
+ with importlib.resources.path(keyboardpaster, "keyboardpaster_app.kv") as kv_file_path:
149
+ return Builder.load_file(str(kv_file_path))
150
+
151
+ def on_stop(self):
152
+ pass
153
+
154
+ @staticmethod
155
+ def _height_for_slot_count(slot_count: int) -> int:
156
+ n = max(1, slot_count)
157
+ slots_area = SLOT_AREA_PADDING_V + n * SLOT_HEIGHT + max(0, n - 1) * SLOT_SPACING
158
+ return HEADER_HEIGHT + BODY_TOP_PADDING + slots_area + FOOTER_HEIGHT
159
+
160
+ def update_window_height(self):
161
+ desired = self._height_for_slot_count(len(self._ordered_slots()))
162
+ height = min(desired, MAX_WINDOW_HEIGHT)
163
+ if Window.size[0] != WINDOW_WIDTH or Window.size[1] != height:
164
+ Window.size = (WINDOW_WIDTH, height)
165
+
166
+ def _slot_container(self):
167
+ return self.root.ids["input_fields_container"]
168
+
169
+ def _ordered_slots(self):
170
+ # Kivy children are reverse insertion order.
171
+ return list(reversed(self._slot_container().children))
172
+
173
+ @staticmethod
174
+ def _slot_text_input(slot):
175
+ for child in slot.children:
176
+ if isinstance(child, TextInput):
177
+ return child
178
+ return None
179
+
180
+ @staticmethod
181
+ def _slot_secret_checkbox(slot):
182
+ for child in slot.children:
183
+ if isinstance(child, MDCheckbox) and getattr(child, "secret_checkbox", False):
184
+ return child
185
+ return None
186
+
187
+ def _renumber_slots(self):
188
+ for index, slot in enumerate(self._ordered_slots()):
189
+ slot.text_input_id = f"input_text_{index}"
190
+
191
+ def add_slot(self, text: str = "", secret: bool = False):
192
+ container = self._slot_container()
193
+ index = len(container.children)
194
+ slot = Factory.InputFieldButton()
195
+ slot.text_input_id = f"input_text_{index}"
196
+ container.add_widget(slot)
197
+
198
+ text_input = self._slot_text_input(slot)
199
+ secret_cb = self._slot_secret_checkbox(slot)
200
+
201
+ previous = self._updating_slots
202
+ self._updating_slots = True
203
+ try:
204
+ if text_input is not None:
205
+ text_input.text = text
206
+ text_input.password = bool(secret)
207
+ if secret_cb is not None:
208
+ secret_cb.active = bool(secret)
209
+ finally:
210
+ self._updating_slots = previous
211
+
212
+ return slot
213
+
214
+ def clear_slots(self):
215
+ self._slot_container().clear_widgets()
216
+
217
+ def ensure_trailing_empty_slot(self, *_args):
218
+ if self._updating_slots:
219
+ return
220
+
221
+ self._updating_slots = True
222
+ try:
223
+ slots = self._ordered_slots()
224
+ if not slots:
225
+ self.add_slot()
226
+ self._renumber_slots()
227
+ return
228
+
229
+ # Remove extra trailing empty slots (keep exactly one empty at the end).
230
+ while len(slots) > 1:
231
+ last = slots[-1]
232
+ prev = slots[-2]
233
+ last_text = self._slot_text_input(last)
234
+ prev_text = self._slot_text_input(prev)
235
+ last_empty = last_text is None or not last_text.text.strip()
236
+ prev_empty = prev_text is None or not prev_text.text.strip()
237
+ if last_empty and prev_empty:
238
+ self._slot_container().remove_widget(last)
239
+ slots = self._ordered_slots()
240
+ continue
241
+ break
242
+
243
+ last = self._ordered_slots()[-1]
244
+ last_text = self._slot_text_input(last)
245
+ if last_text is not None and last_text.text.strip():
246
+ self.add_slot()
247
+
248
+ self._renumber_slots()
249
+ finally:
250
+ self._updating_slots = False
251
+ self.update_window_height()
252
+
253
+ def on_slot_text(self, _instance, _value):
254
+ self.ensure_trailing_empty_slot()
255
+
256
+ def read_config(self):
257
+ try:
258
+ with open(self.json_config, "r", encoding='utf-8') as file:
259
+ self.config = json.load(file)
260
+ except FileNotFoundError:
261
+ self.config = None
262
+ except AttributeError:
263
+ self.config = None
264
+ except json.JSONDecodeError:
265
+ self.config = None
266
+ except ValueError:
267
+ self.config = None
268
+ except KeyError:
269
+ self.config = None
270
+
271
+ if not self.config:
272
+ self.config = {'profiles': {'DEFAULT': {}}}
273
+ elif 'profiles' in self.config:
274
+ pass
275
+ elif 'input_text_0' in self.config:
276
+ values = self.config
277
+ self.config = {'profiles': {'DEFAULT': values}}
278
+ else:
279
+ self.config = {'profiles': {'DEFAULT': {}}}
280
+
281
+ def write_config(self):
282
+ with open(self.json_config, "w", encoding='utf-8') as file:
283
+ json.dump(self.config, file)
284
+
285
+ @staticmethod
286
+ def close_dialog(button_instance):
287
+ # Navigate up the widget tree until we find an MDDialog instance
288
+ current_widget = button_instance
289
+ while current_widget and not isinstance(current_widget, MDDialog):
290
+ current_widget = current_widget.parent
291
+
292
+ # If we found an MDDialog instance, dismiss it
293
+ if isinstance(current_widget, MDDialog):
294
+ current_widget.dismiss()
295
+
296
+ def add_profile(self, profile_name, button_instance):
297
+ # Access the text from the input box
298
+ print(f"New profile name: {profile_name}")
299
+ self.config['profiles'][profile_name] = dict()
300
+ self.set_profile(None, profile_name)
301
+
302
+ # Close the dialog using the button instance
303
+ self.close_dialog(button_instance)
304
+
305
+ def create_new_profile(self):
306
+ self.close_dropdown()
307
+
308
+ profile_input = MDTextField(
309
+ hint_text="Enter profile name",
310
+ size_hint_x=0.8,
311
+ pos_hint={"center_x": 0.5}
312
+ )
313
+
314
+ dialog = MDDialog(
315
+ title="New Profile",
316
+ type="custom",
317
+ content_cls=profile_input,
318
+ buttons=[
319
+ MDRaisedButton(
320
+ text="CANCEL",
321
+ on_release=self.close_dialog
322
+ ),
323
+ MDRaisedButton(
324
+ text="OK",
325
+ on_release=lambda x: self.add_profile(profile_input.text, x)
326
+ )
327
+ ],
328
+ )
329
+ dialog.open()
330
+
331
+ def close_dropdown(self):
332
+ self.profile_dropdown.dismiss()
333
+
334
+ def handle_dropdown(self, profile):
335
+ self.set_profile(None, profile)
336
+ self.close_dropdown()
337
+
338
+ def generate_profile_selector(self):
339
+ profile_dropdown = self.profile_dropdown
340
+ profile_selector = self.root.ids['profile_selector']
341
+ try:
342
+ profiles = list(self.config['profiles'].keys())
343
+ except KeyError:
344
+ profiles = ['DEFAULT']
345
+ max_length = 7
346
+
347
+ profile_dropdown.clear_widgets()
348
+
349
+ btn = MDRaisedButton()
350
+ btn.text = '- New -'
351
+ btn.size_hint_y = None
352
+ btn.bind(on_release=lambda x: self.create_new_profile())
353
+ profile_dropdown.add_widget(btn)
354
+
355
+ for profile in profiles:
356
+ if len(profile) > max_length:
357
+ max_length = len(profile)
358
+
359
+ if profile != profile_selector.text.strip():
360
+ btn = MDRaisedButton()
361
+ btn.text = profile
362
+ btn.size_hint_y = None
363
+ btn.bind(on_release=lambda x, p=profile: self.handle_dropdown(p))
364
+ profile_dropdown.add_widget(btn)
365
+
366
+ number_of_whitespaces = max_length - len(profile_selector.text.strip())
367
+ profile_selector.text = profile_selector.text + (number_of_whitespaces + 1) * " "
368
+
369
+ def set_profile(self, dt=None, profile=None):
370
+ profile_selector = self.root.ids['profile_selector']
371
+
372
+ if not self.config or 'profiles' not in self.config or not self.config['profiles']:
373
+ self.config = self.config or {}
374
+ self.config['profiles'] = {'DEFAULT': {}}
375
+
376
+ if profile is None:
377
+ profile_name = list(self.config['profiles'].keys())[0]
378
+ else:
379
+ profile_name = profile
380
+
381
+ profile_selector.text = profile_name
382
+ self.load_inputs()
383
+ self.generate_profile_selector()
384
+
385
+ def load_inputs(self):
386
+ profile_selector = self.root.ids['profile_selector']
387
+ profile_name = profile_selector.text.strip()
388
+
389
+ profile_values = {}
390
+ if self.config and profile_name in self.config.get('profiles', {}):
391
+ profile_values = self.config['profiles'][profile_name] or {}
392
+
393
+ def sort_key(item):
394
+ key = item[0]
395
+ try:
396
+ return int(str(key).rsplit("_", 1)[-1])
397
+ except ValueError:
398
+ return 0
399
+
400
+ filled = []
401
+ for _name, value in sorted(profile_values.items(), key=sort_key):
402
+ if isinstance(value, (list, tuple)) and len(value) >= 2:
403
+ text, secret_state = value[0], value[1]
404
+ else:
405
+ text, secret_state = value, False
406
+ if text:
407
+ filled.append((text, bool(secret_state)))
408
+
409
+ self.clear_slots()
410
+ self._updating_slots = True
411
+ try:
412
+ for text, secret_state in filled:
413
+ self.add_slot(text=text, secret=secret_state)
414
+ finally:
415
+ self._updating_slots = False
416
+ self.ensure_trailing_empty_slot()
417
+ self.update_window_height()
418
+
419
+ def save_inputs(self):
420
+ profile_selector = self.root.ids['profile_selector']
421
+ profile_name = profile_selector.text.strip()
422
+
423
+ input_fields = {}
424
+ for slot in self._ordered_slots():
425
+ text_input = self._slot_text_input(slot)
426
+ if text_input is None or not text_input.text:
427
+ continue
428
+ secret_cb = self._slot_secret_checkbox(slot)
429
+ secret_state = bool(secret_cb.active) if secret_cb is not None else False
430
+ input_fields[slot.text_input_id] = (text_input.text, secret_state)
431
+
432
+ if not self.config:
433
+ self.config = {'profiles': {}}
434
+ if 'profiles' not in self.config:
435
+ self.config['profiles'] = {}
436
+ self.config['profiles'][profile_name] = input_fields
437
+ self.write_config()
438
+
439
+ def type_text(self, input_text, _checkbox):
440
+ if not input_text:
441
+ # print("No text found")
442
+ return
443
+
444
+ if _checkbox.state == "down":
445
+ end_line = True
446
+ else:
447
+ end_line = False
448
+
449
+ start_delay = float(self.root.ids["start_delay"].value)
450
+ mod_delay = float(self.root.ids["mod_delay"].value)
451
+ type_string_with_delay(
452
+ input_text,
453
+ start_delay=start_delay,
454
+ mod_delay=mod_delay,
455
+ keypress_delay=0.02,
456
+ layout=self.layout,
457
+ end_line=end_line,
458
+ on_error=self.show_type_error,
459
+ )
460
+
461
+ def show_type_error(self, error):
462
+ MDDialog(title="Kan ikke sende tastaturinput", text=str(error)).open()
463
+
464
+ @staticmethod
465
+ def copy_text(input_text, _checkbox):
466
+ if not input_text:
467
+ # print("No text found")
468
+ return
469
+
470
+ cmd = 'echo ' + input_text.strip() + '|clip'
471
+ subprocess.check_call(cmd, shell=True)
472
+
473
+ @staticmethod
474
+ def hide_text(_input_text, _checkbox):
475
+ if _checkbox.state == "down":
476
+ _input_text.password = True
477
+ else:
478
+ _input_text.password = False
479
+
480
+ def set_layout(self, layout):
481
+ self.layout = layout
482
+
483
+ def update_slider_label(self, value):
484
+ rounded_value = round(value, 1)
485
+ self.root.ids["mod_delay"].value = rounded_value
486
+
487
+ def detect_keyboard_layout(self):
488
+ layout_code = get_keyboard_layout()
489
+
490
+ if bool(re.match(r'da|dk', layout_code, re.I)):
491
+ self.layout = 'DA_DK'
492
+ else: # Default to English (US) layout
493
+ self.layout = 'EN_US'
494
+
495
+
496
+ def main():
497
+ ensure_user_desktop_entry()
498
+ autoupdate()
499
+ KeyboardPasterApp().run()
500
+
501
+
502
+ if __name__ == "__main__":
503
+ main()
@@ -0,0 +1,20 @@
1
+ import os
2
+
3
+ from keyboardpaster.shared_resources import APP_WM_CLASS, app_icon_path
4
+
5
+
6
+ def configure_kivy_environment() -> None:
7
+ if getattr(configure_kivy_environment, "_done", False):
8
+ return
9
+
10
+ os.environ["SDL_MOUSE_FOCUS_CLICKTHROUGH"] = "1"
11
+ os.environ.setdefault("SDL_VIDEO_X11_WMCLASS", APP_WM_CLASS)
12
+ os.environ.setdefault("SDL_VIDEO_WAYLAND_WMCLASS", APP_WM_CLASS)
13
+
14
+ icon = app_icon_path()
15
+ if icon is not None:
16
+ from kivy.config import Config
17
+
18
+ Config.set("kivy", "window_icon", str(icon))
19
+
20
+ configure_kivy_environment._done = True # type: ignore[attr-defined]