Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: 

 

# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org> 

# 

# This file is part of qutebrowser. 

# 

# qutebrowser is free software: you can redistribute it and/or modify 

# it under the terms of the GNU General Public License as published by 

# the Free Software Foundation, either version 3 of the License, or 

# (at your option) any later version. 

# 

# qutebrowser is distributed in the hope that it will be useful, 

# but WITHOUT ANY WARRANTY; without even the implied warranty of 

# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

# GNU General Public License for more details. 

# 

# You should have received a copy of the GNU General Public License 

# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. 

 

"""Base class for vim-like key sequence parser.""" 

 

import enum 

import re 

import unicodedata 

 

from PyQt5.QtCore import pyqtSignal, QObject 

 

from qutebrowser.config import config 

from qutebrowser.utils import usertypes, log, utils 

 

 

class BaseKeyParser(QObject): 

 

"""Parser for vim-like key sequences and shortcuts. 

 

Not intended to be instantiated directly. Subclasses have to override 

execute() to do whatever they want to. 

 

Class Attributes: 

Match: types of a match between a binding and the keystring. 

partial: No keychain matched yet, but it's still possible in the 

future. 

definitive: Keychain matches exactly. 

none: No more matches possible. 

 

Types: type of a key binding. 

chain: execute() was called via a chain-like key binding 

special: execute() was called via a special key binding 

 

do_log: Whether to log keypresses or not. 

passthrough: Whether unbound keys should be passed through with this 

handler. 

 

Attributes: 

bindings: Bound key bindings 

special_bindings: Bound special bindings (<Foo>). 

_win_id: The window ID this keyparser is associated with. 

_warn_on_keychains: Whether a warning should be logged when binding 

keychains in a section which does not support them. 

_keystring: The currently entered key sequence 

_modename: The name of the input mode associated with this keyparser. 

_supports_count: Whether count is supported 

_supports_chains: Whether keychains are supported 

 

Signals: 

keystring_updated: Emitted when the keystring is updated. 

arg: New keystring. 

request_leave: Emitted to request leaving a mode. 

arg 0: Mode to leave. 

arg 1: Reason for leaving. 

arg 2: Ignore the request if we're not in that mode 

""" 

 

keystring_updated = pyqtSignal(str) 

request_leave = pyqtSignal(usertypes.KeyMode, str, bool) 

do_log = True 

passthrough = False 

 

Match = enum.Enum('Match', ['partial', 'definitive', 'other', 'none']) 

Type = enum.Enum('Type', ['chain', 'special']) 

 

def __init__(self, win_id, parent=None, supports_count=None, 

supports_chains=False): 

super().__init__(parent) 

self._win_id = win_id 

self._modename = None 

self._keystring = '' 

if supports_count is None: 

supports_count = supports_chains 

self._supports_count = supports_count 

self._supports_chains = supports_chains 

self._warn_on_keychains = True 

self.bindings = {} 

self.special_bindings = {} 

config.instance.changed.connect(self._on_config_changed) 

 

def __repr__(self): 

return utils.get_repr(self, supports_count=self._supports_count, 

supports_chains=self._supports_chains) 

 

def _debug_log(self, message): 

"""Log a message to the debug log if logging is active. 

 

Args: 

message: The message to log. 

""" 

if self.do_log: 

log.keyboard.debug(message) 

 

def _handle_special_key(self, e): 

"""Handle a new keypress with special keys (<Foo>). 

 

Return True if the keypress has been handled, and False if not. 

 

Args: 

e: the KeyPressEvent from Qt. 

 

Return: 

True if event has been handled, False otherwise. 

""" 

binding = utils.keyevent_to_string(e) 

if binding is None: 

self._debug_log("Ignoring only-modifier keyeevent.") 

return False 

 

if binding not in self.special_bindings: 

key_mappings = config.val.bindings.key_mappings 

try: 

binding = key_mappings['<{}>'.format(binding)][1:-1] 

except KeyError: 

pass 

 

try: 

cmdstr = self.special_bindings[binding] 

except KeyError: 

self._debug_log("No special binding found for {}.".format(binding)) 

return False 

count, _command = self._split_count(self._keystring) 

self.execute(cmdstr, self.Type.special, count) 

self.clear_keystring() 

return True 

 

def _split_count(self, keystring): 

"""Get count and command from the current keystring. 

 

Args: 

keystring: The key string to split. 

 

Return: 

A (count, command) tuple. 

""" 

if self._supports_count: 

(countstr, cmd_input) = re.fullmatch(r'(\d*)(.*)', 

keystring).groups() 

count = int(countstr) if countstr else None 

if count == 0 and not cmd_input: 

cmd_input = keystring 

count = None 

else: 

cmd_input = keystring 

count = None 

return count, cmd_input 

 

def _handle_single_key(self, e): 

"""Handle a new keypress with a single key (no modifiers). 

 

Separate the keypress into count/command, then check if it matches 

any possible command, and either run the command, ignore it, or 

display an error. 

 

Args: 

e: the KeyPressEvent from Qt. 

 

Return: 

A self.Match member. 

""" 

txt = e.text() 

key = e.key() 

self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) 

 

if len(txt) == 1: 

category = unicodedata.category(txt) 

is_control_char = (category == 'Cc') 

else: 

is_control_char = False 

 

if (not txt) or is_control_char: 

self._debug_log("Ignoring, no text char") 

return self.Match.none 

 

count, cmd_input = self._split_count(self._keystring + txt) 

match, binding = self._match_key(cmd_input) 

if match == self.Match.none: 

mappings = config.val.bindings.key_mappings 

mapped = mappings.get(txt, None) 

if mapped is not None: 

txt = mapped 

count, cmd_input = self._split_count(self._keystring + txt) 

match, binding = self._match_key(cmd_input) 

 

self._keystring += txt 

if match == self.Match.definitive: 

self._debug_log("Definitive match for '{}'.".format( 

self._keystring)) 

self.clear_keystring() 

self.execute(binding, self.Type.chain, count) 

elif match == self.Match.partial: 

self._debug_log("No match for '{}' (added {})".format( 

self._keystring, txt)) 

elif match == self.Match.none: 

self._debug_log("Giving up with '{}', no matches".format( 

self._keystring)) 

self.clear_keystring() 

elif match == self.Match.other: 

pass 

else: 

raise utils.Unreachable("Invalid match value {!r}".format(match)) 

return match 

 

def _match_key(self, cmd_input): 

"""Try to match a given keystring with any bound keychain. 

 

Args: 

cmd_input: The command string to find. 

 

Return: 

A tuple (matchtype, binding). 

matchtype: Match.definitive, Match.partial or Match.none. 

binding: - None with Match.partial/Match.none. 

- The found binding with Match.definitive. 

""" 

if not cmd_input: 

# Only a count, no command yet, but we handled it 

return (self.Match.other, None) 

# A (cmd_input, binding) tuple (k, v of bindings) or None. 

definitive_match = None 

partial_match = False 

# Check definitive match 

try: 

definitive_match = (cmd_input, self.bindings[cmd_input]) 

except KeyError: 

pass 

# Check partial match 

for binding in self.bindings: 

if definitive_match is not None and binding == definitive_match[0]: 

# We already matched that one 

continue 

elif binding.startswith(cmd_input): 

partial_match = True 

break 

if definitive_match is not None: 

return (self.Match.definitive, definitive_match[1]) 

elif partial_match: 

return (self.Match.partial, None) 

else: 

return (self.Match.none, None) 

 

def handle(self, e): 

"""Handle a new keypress and call the respective handlers. 

 

Args: 

e: the KeyPressEvent from Qt 

 

Return: 

True if the event was handled, False otherwise. 

""" 

handled = self._handle_special_key(e) 

 

if handled or not self._supports_chains: 

return handled 

match = self._handle_single_key(e) 

# don't emit twice if the keystring was cleared in self.clear_keystring 

if self._keystring: 

self.keystring_updated.emit(self._keystring) 

return match != self.Match.none 

 

@config.change_filter('bindings') 

def _on_config_changed(self): 

self._read_config() 

 

def _read_config(self, modename=None): 

"""Read the configuration. 

 

Config format: key = command, e.g.: 

<Ctrl+Q> = quit 

 

Args: 

modename: Name of the mode to use. 

""" 

if modename is None: 

if self._modename is None: 

raise ValueError("read_config called with no mode given, but " 

"None defined so far!") 

modename = self._modename 

else: 

self._modename = modename 

self.bindings = {} 

self.special_bindings = {} 

 

for key, cmd in config.key_instance.get_bindings_for(modename).items(): 

assert cmd 

self._parse_key_command(modename, key, cmd) 

 

def _parse_key_command(self, modename, key, cmd): 

"""Parse the keys and their command and store them in the object.""" 

if utils.is_special_key(key): 

self.special_bindings[key[1:-1]] = cmd 

elif self._supports_chains: 

self.bindings[key] = cmd 

elif self._warn_on_keychains: 

log.keyboard.warning("Ignoring keychain '{}' in mode '{}' because " 

"keychains are not supported there." 

.format(key, modename)) 

 

def execute(self, cmdstr, keytype, count=None): 

"""Handle a completed keychain. 

 

Args: 

cmdstr: The command to execute as a string. 

keytype: Type.chain or Type.special 

count: The count if given. 

""" 

raise NotImplementedError 

 

def clear_keystring(self): 

"""Clear the currently entered key sequence.""" 

if self._keystring: 

self._debug_log("discarding keystring '{}'.".format( 

self._keystring)) 

self._keystring = '' 

self.keystring_updated.emit(self._keystring)