/
thrawn.py
286 lines (235 loc) · 9.21 KB
/
thrawn.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
import sys
import os
import logging
import json
from Xlib import X, XK, display
from Xlib.ext import record
from Xlib.protocol import rq
from PyQt5.QtCore import Qt, QThread
from PyQt5.QtWidgets import (QApplication, QWidget, QDesktopWidget, QLineEdit,
QLabel)
class ThrawnConfig:
def __init__(self):
self.tconfig_map = {}
self.tconfig_load()
def tconfig_save_default(self):
self.tconfig_map = {'Terminal': 'gnome-terminal',
'Terminal exec option flag': '-x',
'Height': 24,
'Focus keys': ['Control_L', 'Shift_L']}
self.tconfig_save()
@property
def focus_keymap(self):
return self.tconfig_map['Focus keys']
@focus_keymap.setter
def focus_keymap(self, focus_keymap):
self.tconfig_map['Focus keys'] = focus_keymap
self.tconfig_save()
@property
def terminal(self):
return self.tconfig_map['Terminal']
@terminal.setter
def terminal(self, terminal):
self.tconfig_map['Terminal'] = terminal
self.tconfig_save()
@property
def terminal_exec_flag(self):
return self.tconfig_map['Terminal exec option flag']
@terminal_exec_flag.setter
def terminal_exec_flag(self, terminal_exec_flag):
self.tconfig_map['Terminal exec option flag'] = terminal_exec_flag
self.tconfig_save()
@property
def height(self):
return self.tconfig_map['Height']
@height.setter
def height(self, height):
self.tconfig_map['Height'] = height
self.tconfig_save()
def tconfig_save(self):
tconfig_path = self.get_tconfig_path()
tconfig_path += '/thrawn/thrawn.conf'
self.dir_check(os.path.dirname(tconfig_path))
with open(tconfig_path, 'w') as f:
json.dump(self.tconfig_map, f)
def tconfig_load(self):
tconfig_path = self.get_tconfig_path()
try:
with open(tconfig_path + '/thrawn/thrawn.conf') as f:
self.tconfig_map = json.load(f)
except FileNotFoundError:
logging.warning('configuration file not found, \
writing a default one')
self.tconfig_save_default()
def dir_check(self, directory):
if not os.path.exists(directory):
os.makedirs(directory)
def get_tconfig_path(self):
xdg_home = os.getenv('XDG_config_HOME')
if xdg_home:
tconfig_path = xdg_home
else:
home = os.getenv('HOME')
if not home:
logging.critical('HOME variable not defined')
sys.exit(1)
else:
tconfig_path = home + '/.config'
return tconfig_path
# FROM https://gist.github.com/whym/402801#file-keylogger-py
class XInputThread(QThread):
def __init__(self, panel, tconfig):
QThread.__init__(self)
self.local_dpy = display.Display()
self.record_dpy = display.Display()
self.tconfig = tconfig
self.panel = panel
self.keymap = self.tconfig.focus_keymap
self.received_keys = list()
def lookup_keysym(self, keysym):
for name in dir(XK):
if name[:3] == "XK_" and getattr(XK, name) == keysym:
return name[3:]
return '[{key}]'.format(key=keysym)
def record_callback(self, reply):
if reply.category != record.FromServer:
return
elif reply.client_swapped:
logging.warning("Received swapped protocol data, cowardly ignored")
return
elif not len(reply.data) or ord(str(reply.data[0])) < 2:
# not an event
return
data = reply.data
while len(data):
event, data = rq.EventField(None).parse_binary_value(
data, self.record_dpy.display, None, None)
if event.type in [X.KeyPress, X.KeyRelease]:
keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
if keysym:
received_key = self.lookup_keysym(keysym)
if received_key in self.keymap :
if received_key not in self.received_keys:
self.received_keys.append(received_key)
if len(self.received_keys) == 2:
self.panel.activateWindow()
self.received_keys.clear()
def run(self):
# Check if the extension is present
if not self.record_dpy.has_extension("RECORD"):
logging.critical("RECORD extension not found")
sys.exit(1)
# Create a recording context; we only want key and mouse events
ctx = self.record_dpy.record_create_context(
0,
[record.AllClients],
[{
'core_requests': (0, 0),
'core_replies': (0, 0),
'ext_requests': (0, 0, 0, 0),
'ext_replies': (0, 0, 0, 0),
'delivered_events': (0, 0),
'device_events': (X.KeyPress, X.KeyPress),
'errors': (0, 0),
'client_started': False,
'client_died': False
}])
# Enable the context; this only returns after
# a call to record_disable_context,
# while calling the callback function in the meantime
self.record_dpy.record_enable_context(ctx, self.record_callback)
# Finally free the context
self.record_dpy.record_free_context(ctx)
self.exec_()
class Panel(QWidget):
def __init__(self, tconfig):
super().__init__()
self.tconfig = tconfig
self.init_ui()
self.x_input_thread = XInputThread(self, tconfig)
self.x_input_thread.start()
def init_ui(self):
screen = QDesktopWidget().availableGeometry()
self.resize(screen.width(), self.tconfig.height)
self.setWindowFlags(Qt.FramelessWindowHint)
self.move(0, 0)
command_label = CommandsLabel(self, self.tconfig)
command_line_edit = CommandLineEdit(self, command_label,
self.tconfig)
class BuiltInCommands:
@staticmethod
def built_in_commands_dict():
built_in_commands = {}
met_names = list((el for el in BuiltInCommands.__dict__ if
el.find('__') and el.find('built_in_commands_dict')))
for el in met_names:
built_in_commands[el] = 'BuiltInCommands.' + el + '()'
return built_in_commands
@staticmethod
def thrawn_quit():
sys.exit(0)
@staticmethod
def test():
print('test')
class CommandLineEdit(QLineEdit):
def __init__(self, parent, command_label, tconfig):
super().__init__(parent)
self.tconfig = tconfig
self.command_label = command_label
self.exec_list = self.get_exec_list()
self.init_ui()
self.init_signals()
def init_ui(self):
self.resize(300, self.tconfig.height)
self.move(0, 0)
self.setFocus(Qt.OtherFocusReason)
def init_signals(self):
self.returnPressed.connect(self.command_choose)
self.textChanged.connect(self.change_command_label_text)
def command_choose(self):
completion_list = self.get_completion()
if self.text() in completion_list:
self.command_run(self.text())
elif completion_list:
self.command_run(completion_list[0])
else:
self.command_run(self.text())
def command_run(self, command):
os.popen(self.tconfig.terminal + ' ' +
self.tconfig.terminal_exec_flag + ' ' +
command)
def get_exec_list(self):
exec_list = []
path = os.getenv('PATH')
if path:
list_dir = path.split(':')
for directory in list_dir:
for _, _, list_file in os.walk(directory):
exec_list += list_file
else:
logging.critical('PATH environment variable not set')
sys.exit(1)
return exec_list
def get_completion(self):
match_list = []
if self.text():
match_list.extend(el for el in self.exec_list if self.text() in el)
return match_list
def change_command_label_text(self):
completion_list = self.get_completion()
self.command_label.setText(' '.join(completion_list))
class CommandsLabel(QLabel):
def __init__(self, parent, tconfig):
super().__init__(parent)
self.tconfig = tconfig
self.init_ui()
def init_ui(self):
screen = QDesktopWidget().availableGeometry()
self.move(310, 0)
self.resize(screen.width() - self.width(), self.tconfig.height)
if __name__ == '__main__':
app = QApplication(sys.argv)
tconfig = ThrawnConfig()
panel = Panel(tconfig)
panel.show()
sys.exit(app.exec_())