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

# 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/>. 

 

"""Module containing command managers (SearchRunner and CommandRunner).""" 

 

import traceback 

import re 

 

import attr 

from PyQt5.QtCore import pyqtSlot, QUrl, QObject 

 

from qutebrowser.config import config 

from qutebrowser.commands import cmdexc, cmdutils 

from qutebrowser.utils import message, objreg, qtutils, usertypes, utils 

from qutebrowser.misc import split 

 

 

last_command = {} 

 

 

@attr.s 

class ParseResult: 

 

"""The result of parsing a commandline.""" 

 

cmd = attr.ib() 

args = attr.ib() 

cmdline = attr.ib() 

 

 

def _current_url(tabbed_browser): 

"""Convenience method to get the current url.""" 

try: 

return tabbed_browser.current_url() 

except qtutils.QtValueError as e: 

msg = "Current URL is invalid" 

if e.reason: 

msg += " ({})".format(e.reason) 

msg += "!" 

raise cmdexc.CommandError(msg) 

 

 

def replace_variables(win_id, arglist): 

"""Utility function to replace variables like {url} in a list of args.""" 

variables = { 

'url': lambda: _current_url(tabbed_browser).toString( 

QUrl.FullyEncoded | QUrl.RemovePassword), 

'url:pretty': lambda: _current_url(tabbed_browser).toString( 

QUrl.DecodeReserved | QUrl.RemovePassword), 

'clipboard': utils.get_clipboard, 

'primary': lambda: utils.get_clipboard(selection=True), 

} 

for key in list(variables): 

modified_key = '{' + key + '}' 

variables[modified_key] = lambda x=modified_key: x 

values = {} 

args = [] 

tabbed_browser = objreg.get('tabbed-browser', scope='window', 

window=win_id) 

 

def repl_cb(matchobj): 

"""Return replacement for given match.""" 

var = matchobj.group("var") 

if var not in values: 

values[var] = variables[var]() 

return values[var] 

repl_pattern = re.compile("{(?P<var>" + "|".join(variables.keys()) + ")}") 

 

try: 

for arg in arglist: 

# using re.sub with callback function replaces all variables in a 

# single pass and avoids expansion of nested variables (e.g. 

# "{url}" from clipboard is not expanded) 

args.append(repl_pattern.sub(repl_cb, arg)) 

except utils.ClipboardError as e: 

raise cmdexc.CommandError(e) 

return args 

 

 

class CommandParser: 

 

"""Parse qutebrowser commandline commands. 

 

Attributes: 

_partial_match: Whether to allow partial command matches. 

""" 

 

def __init__(self, partial_match=False): 

self._partial_match = partial_match 

 

def _get_alias(self, text, default=None): 

"""Get an alias from the config. 

 

Args: 

text: The text to parse. 

default : Default value to return when alias was not found. 

 

Return: 

The new command string if an alias was found. Default value 

otherwise. 

""" 

parts = text.strip().split(maxsplit=1) 

try: 

alias = config.val.aliases[parts[0]] 

except KeyError: 

return default 

 

try: 

new_cmd = '{} {}'.format(alias, parts[1]) 

except IndexError: 

new_cmd = alias 

128 ↛ 129line 128 didn't jump to line 129, because the condition on line 128 was never true if text.endswith(' '): 

new_cmd += ' ' 

return new_cmd 

 

def _parse_all_gen(self, text, *args, aliases=True, **kwargs): 

"""Split a command on ;; and parse all parts. 

 

If the first command in the commandline is a non-split one, it only 

returns that. 

 

Args: 

text: Text to parse. 

aliases: Whether to handle aliases. 

*args/**kwargs: Passed to parse(). 

 

Yields: 

ParseResult tuples. 

""" 

text = text.strip().lstrip(':').strip() 

if not text: 

raise cmdexc.NoSuchCommandError("No command given") 

 

if aliases: 

text = self._get_alias(text, text) 

 

if ';;' in text: 

# Get the first command and check if it doesn't want to have ;; 

# split. 

first = text.split(';;')[0] 

result = self.parse(first, *args, **kwargs) 

if result.cmd.no_cmd_split: 

sub_texts = [text] 

else: 

sub_texts = [e.strip() for e in text.split(';;')] 

else: 

sub_texts = [text] 

for sub in sub_texts: 

yield self.parse(sub, *args, **kwargs) 

 

def parse_all(self, *args, **kwargs): 

"""Wrapper over _parse_all_gen.""" 

return list(self._parse_all_gen(*args, **kwargs)) 

 

def parse(self, text, *, fallback=False, keep=False): 

"""Split the commandline text into command and arguments. 

 

Args: 

text: Text to parse. 

fallback: Whether to do a fallback splitting when the command was 

unknown. 

keep: Whether to keep special chars and whitespace 

 

Return: 

A ParseResult tuple. 

""" 

cmdstr, sep, argstr = text.partition(' ') 

 

if not cmdstr and not fallback: 

raise cmdexc.NoSuchCommandError("No command given") 

 

if self._partial_match: 

cmdstr = self._completion_match(cmdstr) 

 

try: 

cmd = cmdutils.cmd_dict[cmdstr] 

except KeyError: 

if not fallback: 

raise cmdexc.NoSuchCommandError( 

'{}: no such command'.format(cmdstr)) 

cmdline = split.split(text, keep=keep) 

return ParseResult(cmd=None, args=None, cmdline=cmdline) 

 

args = self._split_args(cmd, argstr, keep) 

if keep and args: 

cmdline = [cmdstr, sep + args[0]] + args[1:] 

elif keep: 

cmdline = [cmdstr, sep] 

else: 

cmdline = [cmdstr] + args[:] 

 

return ParseResult(cmd=cmd, args=args, cmdline=cmdline) 

 

def _completion_match(self, cmdstr): 

"""Replace cmdstr with a matching completion if there's only one match. 

 

Args: 

cmdstr: The string representing the entered command so far 

 

Return: 

cmdstr modified to the matching completion or unmodified 

""" 

matches = [cmd for cmd in sorted(cmdutils.cmd_dict, key=len) 

if cmdstr in cmd] 

if len(matches) == 1: 

cmdstr = matches[0] 

elif len(matches) > 1 and config.val.completion.use_best_match: 

cmdstr = matches[0] 

return cmdstr 

 

def _split_args(self, cmd, argstr, keep): 

"""Split the arguments from an arg string. 

 

Args: 

cmd: The command we're currently handling. 

argstr: An argument string. 

keep: Whether to keep special chars and whitespace 

 

Return: 

A list containing the split strings. 

""" 

if not argstr: 

return [] 

elif cmd.maxsplit is None: 

return split.split(argstr, keep=keep) 

else: 

# If split=False, we still want to split the flags, but not 

# everything after that. 

# We first split the arg string and check the index of the first 

# non-flag args, then we re-split again properly. 

# example: 

# 

# input: "--foo -v bar baz" 

# first split: ['--foo', '-v', 'bar', 'baz'] 

# 0 1 2 3 

# second split: ['--foo', '-v', 'bar baz'] 

# (maxsplit=2) 

split_args = split.simple_split(argstr, keep=keep) 

flag_arg_count = 0 

for i, arg in enumerate(split_args): 

arg = arg.strip() 

if arg.startswith('-'): 

if arg in cmd.flags_with_args: 

flag_arg_count += 1 

else: 

maxsplit = i + cmd.maxsplit + flag_arg_count 

return split.simple_split(argstr, keep=keep, 

maxsplit=maxsplit) 

 

# If there are only flags, we got it right on the first try 

# already. 

return split_args 

 

 

class CommandRunner(QObject): 

 

"""Parse and run qutebrowser commandline commands. 

 

Attributes: 

_win_id: The window this CommandRunner is associated with. 

""" 

 

def __init__(self, win_id, partial_match=False, parent=None): 

super().__init__(parent) 

self._parser = CommandParser(partial_match=partial_match) 

self._win_id = win_id 

 

def run(self, text, count=None): 

"""Parse a command from a line of text and run it. 

 

Args: 

text: The text to parse. 

count: The count to pass to the command. 

""" 

record_last_command = True 

record_macro = True 

 

mode_manager = objreg.get('mode-manager', scope='window', 

window=self._win_id) 

cur_mode = mode_manager.mode 

 

for result in self._parser.parse_all(text): 

if result.cmd.no_replace_variables: 

args = result.args 

else: 

args = replace_variables(self._win_id, result.args) 

result.cmd.run(self._win_id, args, count=count) 

 

if result.cmdline[0] == 'repeat-command': 

record_last_command = False 

 

if result.cmdline[0] in ['record-macro', 'run-macro', 

'set-cmd-text']: 

record_macro = False 

 

if record_last_command: 

last_command[cur_mode] = (text, count) 

 

if record_macro and cur_mode == usertypes.KeyMode.normal: 

macro_recorder = objreg.get('macro-recorder') 

macro_recorder.record_command(text, count) 

 

@pyqtSlot(str, int) 

@pyqtSlot(str) 

def run_safely(self, text, count=None): 

"""Run a command and display exceptions in the statusbar.""" 

try: 

self.run(text, count) 

except cmdexc.Error as e: 

message.error(str(e), stack=traceback.format_exc())