-
Notifications
You must be signed in to change notification settings - Fork 2k
/
clink.lua
703 lines (590 loc) · 22.6 KB
/
clink.lua
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
-- default script for clink, called by init.bat when injecting clink
-- !!! THIS FILE IS OVERWRITTEN WHEN CMDER IS UPDATED
-- !!! Use "%CMDER_ROOT%\config\<whatever>.lua" to add your lua startup scripts
-- luacheck: globals CMDER_SESSION
-- luacheck: globals uah_color cwd_color lamb_color clean_color dirty_color conflict_color unknown_color
-- luacheck: globals prompt_homeSymbol prompt_lambSymbol prompt_type prompt_useHomeSymbol prompt_useUserAtHost
-- luacheck: globals prompt_singleLine prompt_includeVersionControl
-- luacheck: globals prompt_overrideGitStatusOptIn prompt_overrideSvnStatusOptIn
-- luacheck: globals clink io.popenyield os.isdir settings.get
-- At first, load the original clink.lua file
-- this is needed as we set the script path to this dir and therefore the original
-- clink.lua is not loaded.
local clink_lua_file = clink.get_env('CMDER_ROOT')..'\\vendor\\clink\\clink.lua'
dofile(clink_lua_file)
-- now add our own things...
local function get_uah_color()
return uah_color or "\x1b[1;33;49m" -- Green = uah = [user]@[hostname]
end
local function get_cwd_color()
return cwd_color or "\x1b[1;32;49m" -- Yellow cwd = Current Working Directory
end
local function get_lamb_color()
return lamb_color or "\x1b[1;30;49m" -- Light Grey = Lambda Color
end
local function get_clean_color()
return clean_color or "\x1b[37;1m" -- White, Bold
end
local function get_dirty_color()
return dirty_color or "\x1b[33;3m" -- Yellow, Italic
end
local function get_conflict_color()
return conflict_color or "\x1b[31;1m" -- Red, Bold
end
local function get_unknown_color()
return unknown_color or "\x1b[37;1m" -- White, Bold
end
---
-- Escapes special characters in a string.gsub `find` parameter, so that it
-- can be matched as a literal plain text string, i.e. disable Lua pattern
-- matching. See "Patterns" (https://www.lua.org/manual/5.2/manual.html#6.4.1).
-- @param {string} text Text to escape
-- @returns {string} Escaped text
---
local function escape_gsub_find_arg(text)
return text and text:gsub("([-+*?.%%()%[%]$^])", "%%%1") or ""
end
---
-- Escapes special characters in a string.gsub `replace` parameter, so that it
-- can be replaced as a literal plain text string, i.e. disable Lua pattern
-- matching. See "Patterns" (https://www.lua.org/manual/5.2/manual.html#6.4.1).
-- @param {string} text Text to escape
-- @returns {string} Escaped text
---
local function escape_gsub_replace_arg(text)
return text and text:gsub("%%", "%%%%") or ""
end
---
-- Perform string.sub, but disable Lua pattern matching and just treat both
-- the `find` and `replace` parameters as a literal plain text replacement.
-- @param {string} str Text in which to perform find and replace
-- @param {string} find Text to find (plain text; not a Lua pattern)
-- @param {string} replace Replacement text (plain text; not a Lua pattern)
-- @returns {string} Copy of the input `str` with `find` replaced by `replace`
---
local function gsub_plain(str, find, replace)
return string.gsub(str, escape_gsub_find_arg(find), escape_gsub_replace_arg(replace))
end
-- Extracts only the folder name from the input Path
-- Ex: Input C:\Windows\System32 returns System32
---
local function get_folder_name(path)
local reversePath = string.reverse(path)
local slashIndex = string.find(reversePath, "\\")
return string.sub(path, string.len(path) - slashIndex + 2)
end
---
-- Forward/backward compatibility for Clink asynchronous prompt filtering.
-- With Clink v1.2.10 and higher this lets git status run in the background and
-- refresh the prompt when it finishes, to eliminate waits in large git repos.
---
local io_popenyield
local clink_promptcoroutine
local cached_info = {}
if clink.promptcoroutine and io.popenyield then
io_popenyield = io.popenyield
clink_promptcoroutine = clink.promptcoroutine
else
io_popenyield = io.popen
clink_promptcoroutine = function (func)
return func(false)
end
end
---
-- Global variable so other Lua scripts can detect whether they're in a Cmder
-- shell session.
---
CMDER_SESSION = true
---
-- Setting the prompt in clink means that commands which rewrite the prompt do
-- not destroy our own prompt. It also means that started cmds (or batch files
-- which echo) don't get the ugly '{lamb}' shown.
---
local function set_prompt_filter()
-- get_cwd() is differently encoded than the clink.prompt.value, so everything other than
-- pure ASCII will get garbled. So try to parse the current directory from the original prompt
-- and only if that doesn't work, use get_cwd() directly.
-- The matching relies on the default prompt which ends in X:\PATH\PATH>
-- (no network path possible here!)
local old_prompt = clink.prompt.value
local cwd = old_prompt:match('.*(.:[^>]*)>')
if cwd == nil then cwd = clink.get_cwd() end
-- environment systems like pythons virtualenv change the PROMPT and usually
-- set some variable. But the variables are differently named and we would never
-- get them all, so try to parse the env name out of the PROMPT.
-- envs are usually put in round or square parentheses and before the old prompt
local env = old_prompt:match('.*%(([^%)]+)%).+:')
-- also check for square brackets
if env == nil then env = old_prompt:match('.*%[([^%]]+)%].+:') end
-- Much of the below was 'borrowed' from https://github.com/AmrEldib/cmder-powerline-prompt
-- Symbol displayed for the home dir in the prompt.
if not prompt_homeSymbol then
prompt_homeSymbol = "~"
end
-- Symbol displayed in the new line below the prompt.
if not prompt_lambSymbol then
prompt_lambSymbol = "λ"
end
if not prompt_type then
prompt_type = "full"
end
if prompt_useHomeSymbol == nil then
prompt_useHomeSymbol = false
end
if prompt_useUserAtHost == nil then
prompt_useUserAtHost = false
end
if prompt_singleLine == nil then
prompt_singleLine = false
end
if prompt_includeVersionControl == nil then
prompt_includeVersionControl = true
end
if prompt_type == 'folder' then
cwd = get_folder_name(cwd)
end
if prompt_useHomeSymbol and string.find(cwd, clink.get_env("HOME")) then
cwd = gsub_plain(cwd, clink.get_env("HOME"), prompt_homeSymbol)
end
local uah = ''
if prompt_useUserAtHost then
uah = clink.get_env("USERNAME") .. "@" .. clink.get_env("COMPUTERNAME") .. ' '
end
local cr = "\n"
if prompt_singleLine then
cr = ' '
end
cr = "\x1b[0m" .. cr
if env ~= nil then env = "("..env..") " else env = "" end
if uah ~= '' then uah = get_uah_color() .. uah end
if cwd ~= '' then cwd = get_cwd_color() .. cwd end
local version_control = prompt_includeVersionControl and "{git}{hg}{svn}" or ""
local prompt = "{uah}{cwd}" .. version_control .. cr .. get_lamb_color() .. "{env}{lamb}\x1b[0m "
prompt = gsub_plain(prompt, "{uah}", uah)
prompt = gsub_plain(prompt, "{cwd}", cwd)
prompt = gsub_plain(prompt, "{env}", env)
clink.prompt.value = gsub_plain(prompt, "{lamb}", prompt_lambSymbol)
end
local function percent_prompt_filter()
clink.prompt.value = gsub_plain(clink.prompt.value, "{percent}", "%")
end
---
-- Resolves closest directory location for specified directory.
-- Navigates subsequently up one level and tries to find specified directory
-- @param {string} path Path to directory will be checked. If not provided
-- current directory will be used
-- @param {string} dirname Directory name to search for
-- @return {string} Path to specified directory or nil if such dir not found
local function get_dir_contains(path, dirname)
-- return parent path for specified entry (either file or directory)
local function pathname(path) -- luacheck: ignore 432
local prefix = ""
local i = path:find("[\\/:][^\\/:]*$")
if i then
prefix = path:sub(1, i-1)
end
return prefix
end
-- Navigates up one level
local function up_one_level(path) -- luacheck: ignore 432
if path == nil then path = '.' end
if path == '.' then path = clink.get_cwd() end
return pathname(path)
end
-- Checks if provided directory contains git directory
local function has_specified_dir(path, specified_dir) -- luacheck: ignore 432
if path == nil then path = '.' end
local found_dirs = clink.find_dirs(path..'/'..specified_dir)
if #found_dirs > 0 then return true end
return false
end
-- Set default path to current directory
if path == nil then path = '.' end
-- If we're already have .git directory here, then return current path
if has_specified_dir(path, dirname) then
return path..'/'..dirname
else
-- Otherwise go up one level and make a recursive call
local parent_path = up_one_level(path)
if parent_path == path then
return nil
else
return get_dir_contains(parent_path, dirname)
end
end
end
-- adapted from from clink-completions' git.lua
local function get_git_dir(path)
-- return parent path for specified entry (either file or directory)
local function pathname(path) -- luacheck: ignore 432
local prefix = ""
local i = path:find("[\\/:][^\\/:]*$")
if i then
prefix = path:sub(1, i-1)
end
return prefix
end
-- Checks if provided directory contains git directory
local function has_git_dir(dir)
return clink.is_dir(dir..'/.git') and dir..'/.git'
end
local function has_git_file(dir)
local gitfile = io.open(dir..'/.git')
if not gitfile then return false end
local line = gitfile:read() or ''
local git_dir = line:match('gitdir: (.*)')
gitfile:close()
if os.isdir then -- only available in Clink v1.0.0 and higher
if git_dir and os.isdir(git_dir) then
return git_dir
end
end
return git_dir and dir..'/'..git_dir
end
-- Set default path to current directory
if not path or path == '.' then path = clink.get_cwd() end
-- Calculate parent path now otherwise we won't be
-- able to do that inside of logical operator
local parent_path = pathname(path)
return has_git_dir(path)
or has_git_file(path)
-- Otherwise go up one level and make a recursive call
or (parent_path ~= path and get_git_dir(parent_path) or nil)
end
local function get_hg_dir(path)
return get_dir_contains(path, '.hg')
end
local function get_svn_dir(path)
return get_dir_contains(path, '.svn')
end
---
-- Find out current branch
-- @return {nil|git branch name}
---
local function get_git_branch(git_dir)
git_dir = git_dir or get_git_dir()
-- If git directory not found then we're probably outside of repo
-- or something went wrong. The same is when head_file is nil
local head_file = git_dir and io.open(git_dir..'/HEAD')
if not head_file then return end
local HEAD = head_file:read()
head_file:close()
-- If HEAD is missing, something is wrong.
if not HEAD then return end
-- if HEAD matches branch expression, then we're on named branch
-- otherwise it is a detached commit
local branch_name = HEAD:match('ref: refs/heads/(.+)')
return branch_name or 'HEAD detached at '..HEAD:sub(1, 7)
end
---
-- Find out current branch information
-- @return {false|mercurial branch information}
---
local function get_hg_branch()
-- Return the branch information. The default is to get just the
-- branch name, but you could e.g. use the "hg-prompt" extension to
-- get more information, such as any applied mq patches. Here's an
-- example of that:
-- local cmd = "hg prompt \"{branch}{status}{|{patch}}{update}\""
local cmd = "hg branch 2>nul"
local file = io.popen(cmd)
if not file then
return false
end
for line in file:lines() do
local m = line:match("(.+)$")
if m then
file:close()
return m
end
end
file:close()
return false
end
---
-- Find out current branch
-- @return {false|svn branch name}
---
local function get_svn_branch()
local file = io_popenyield("svn info 2>nul")
if not file then
return false
end
for line in file:lines() do
local m = line:match("^Relative URL:")
if m then
file:close()
return line:sub(line:find("/")+1,line:len())
end
end
file:close()
return false
end
---
-- Get the status and conflict status of working dir
-- @return {bool <status>, bool <is_conflict>}
---
local function get_git_status()
local file = io_popenyield("git --no-optional-locks status --porcelain 2>nul")
if not file then
return {}
end
local conflict_found = false
local is_status = true
for line in file:lines() do
local code = line:sub(1, 2)
-- print (string.format("code: %s, line: %s", code, line))
if code == "DD" or code == "AU" or code == "UD" or code == "UA" or code == "DU" or code == "AA" or code == "UU" then -- luacheck: no max line length
is_status = false
conflict_found = true
break
-- unversioned files are ignored, comment out 'code ~= "!!"' to unignore them
elseif code ~= "!!" and code ~= "??" then
is_status = false
end
end
file:close()
return { status = is_status, conflict = conflict_found }
end
---
-- Get the status of working dir
-- @return {bool}
---
local function get_svn_status()
local file = io_popenyield("svn status -q")
if not file then
return { error = true }
end
for line in file:lines() do -- luacheck: ignore 512, no unused
file:close()
return { clean = false }
end
file:close()
return { clean = true }
end
---
-- Get the status of working dir
-- @return {bool}
---
local last_git_status_time = nil
local last_git_status_setting = true
local function get_git_status_setting()
local time = os.clock()
local last_time = last_git_status_time
last_git_status_time = time
if last_time and time >= 0 and time - last_time < 10 then
return last_git_status_setting
end
-- When async prompt filtering is available, check the
-- prompt_overrideGitStatusOptIn config setting for whether to ignore the
-- cmder.status and cmder.cmdstatus git config opt-in settings.
if clink.promptcoroutine and io.popenyield and settings.get("prompt.async") then
if prompt_overrideGitStatusOptIn then
last_git_status_setting = true
return true
end
end
local gitStatusConfig = io_popenyield("git --no-pager config cmder.status 2>nul")
if gitStatusConfig then
for line in gitStatusConfig:lines() do
if string.match(line, 'false') then
gitStatusConfig:close()
last_git_status_setting = false
return false
end
end
gitStatusConfig:close()
end
local gitCmdStatusConfig = io_popenyield("git --no-pager config cmder.cmdstatus 2>nul")
if gitCmdStatusConfig then
for line in gitCmdStatusConfig:lines() do
if string.match(line, 'false') then
gitCmdStatusConfig:close()
last_git_status_setting = false
return false
end
end
gitCmdStatusConfig:close()
end
last_git_status_setting = true
return true
end
---
-- Use a prompt coroutine to get git status in the background.
-- Cache the info so we can reuse it next time to reduce flicker.
---
local function get_git_info_table()
local info = clink_promptcoroutine(function ()
-- Use git status if allowed.
local cmderGitStatusOptIn = get_git_status_setting()
return cmderGitStatusOptIn and get_git_status() or {}
end)
if not info then
info = cached_info.git_info or {}
else
cached_info.git_info = info
end
return info
end
local function git_prompt_filter()
-- Don't do any git processing if the prompt doesn't want to show git info.
if not clink.prompt.value:find("{git}") then
return false
end
-- Colors for git status
local colors = {
clean = get_clean_color(),
dirty = get_dirty_color(),
conflict = get_conflict_color(),
nostatus = get_unknown_color()
}
local git_dir = get_git_dir()
local color
if git_dir then
local branch = get_git_branch(git_dir)
if branch then
-- If in a different repo or branch than last time, discard cached info.
if cached_info.git_dir ~= git_dir or cached_info.git_branch ~= branch then
cached_info.git_info = nil
cached_info.git_dir = git_dir
cached_info.git_branch = branch
end
-- If we're inside of git repo then try to detect current branch
-- Has branch => therefore it is a git folder, now figure out status
local gitInfo = get_git_info_table()
local gitStatus = gitInfo.status
local gitConflict = gitInfo.conflict
if gitStatus == nil then
color = colors.nostatus
elseif gitStatus then
color = colors.clean
else
color = colors.dirty
end
if gitConflict then
color = colors.conflict
end
clink.prompt.value = gsub_plain(clink.prompt.value, "{git}", " "..color.."("..branch..")")
return false
end
end
-- No git present or not in git file
clink.prompt.value = gsub_plain(clink.prompt.value, "{git}", "")
return false
end
local function hg_prompt_filter()
-- Don't do any hg processing if the prompt doesn't want to show hg info.
if not clink.prompt.value:find("{hg}") then
return false
end
local hg_dir = get_hg_dir()
if hg_dir then
-- Colors for mercurial status
local colors = {
clean = get_clean_color(),
dirty = get_dirty_color(),
nostatus = get_unknown_color()
}
local output = get_hg_branch()
-- strip the trailing newline from the branch name
local n = #output
while n > 0 and output:find("^%s", n) do n = n - 1 end
local branch = output:sub(1, n)
if branch ~= nil and
string.sub(branch,1,7) ~= "abort: " and -- not an HG working copy
(not string.find(branch, "is not recognized")) then -- 'hg' not in path
local color = colors.clean
local pipe = io.popen("hg status -amrd 2>&1")
if pipe then
output = pipe:read('*all')
pipe:close()
if output ~= nil and output ~= "" then color = colors.dirty end
end
local result = color .. "(" .. branch .. ")"
clink.prompt.value = gsub_plain(clink.prompt.value, "{hg}", " "..result)
return false
end
end
-- No hg present or not in hg repo
clink.prompt.value = gsub_plain(clink.prompt.value, "{hg}", "")
end
local function svn_prompt_filter()
-- Don't do any svn processing if the prompt doesn't want to show svn info.
if not clink.prompt.value:find("{svn}") then
return false
end
-- Colors for svn status
local colors = {
clean = get_clean_color(),
dirty = get_dirty_color(),
nostatus = get_unknown_color()
}
local svn_dir = get_svn_dir()
if svn_dir then
-- if we're inside of svn repo then try to detect current branch
local branch = get_svn_branch()
if branch then
-- If in a different repo or branch than last time, discard cached info
if cached_info.svn_dir ~= svn_dir or cached_info.svn_branch ~= branch then
cached_info.svn_info = nil
cached_info.svn_dir = svn_dir
cached_info.svn_branch = branch
end
-- Get the svn status using coroutine if available and option is enabled. Otherwise use a blocking call
local svnStatus
if clink.promptcoroutine and io.popenyield and settings.get("prompt.async") and prompt_overrideSvnStatusOptIn then -- luacheck: no max line length
svnStatus = clink_promptcoroutine(function ()
return get_svn_status()
end)
-- If the status result is pending, use the cached version instead, otherwise store it to the cache
if svnStatus == nil then
svnStatus = cached_info.svn_info
else
cached_info.svn_info = svnStatus
end
else
svnStatus = get_svn_status()
end
local color
if not svnStatus or svnStatus.error then
color = colors.nostatus
elseif svnStatus.clean then
color = colors.clean
else
color = colors.dirty
end
clink.prompt.value = gsub_plain(clink.prompt.value, "{svn}", " "..color.."("..branch..")")
return false
end
end
-- No svn present or not in svn file
clink.prompt.value = gsub_plain(clink.prompt.value, "{svn}", "")
return false
end
-- insert the set_prompt at the very beginning so that it runs first
clink.prompt.register_filter(set_prompt_filter, 1)
clink.prompt.register_filter(hg_prompt_filter, 50)
clink.prompt.register_filter(git_prompt_filter, 50)
clink.prompt.register_filter(svn_prompt_filter, 50)
clink.prompt.register_filter(percent_prompt_filter, 51)
local completions_dir = clink.get_env('CMDER_ROOT')..'/vendor/clink-completions/'
-- Execute '.init.lua' first to ensure package.path is set properly
dofile(completions_dir..'.init.lua')
for _,lua_module in ipairs(clink.find_files(completions_dir..'*.lua')) do
-- Skip files that starts with _. This could be useful if some files should be ignored
if not string.match(lua_module, '^_.*') then
local filename = completions_dir..lua_module
-- use dofile instead of require because require caches loaded modules
-- so config reloading using Alt-Q won't reload updated modules.
dofile(filename)
end
end
if clink.get_env('CMDER_USER_CONFIG') then
local cmder_config_dir = clink.get_env('CMDER_ROOT')..'/config/'
for _,lua_module in ipairs(clink.find_files(cmder_config_dir..'*.lua')) do
local filename = cmder_config_dir..lua_module
-- use dofile instead of require because require caches loaded modules
-- so config reloading using Alt-Q won't reload updated modules.
dofile(filename)
end
end