mud-git 0.0.post1.dev1__py3-none-any.whl

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.
mud/runner.py ADDED
@@ -0,0 +1,500 @@
1
+ import os
2
+ import asyncio
3
+ import subprocess
4
+
5
+ from typing import List, Dict
6
+ from collections import Counter
7
+
8
+ from mud import utils
9
+ from mud.utils import glyphs
10
+ from mud.styles import *
11
+
12
+
13
+ class Runner:
14
+ _force_color_env = {"GIT_PAGER": "cat", "TERM": "xterm-256color", "GIT_CONFIG_PARAMETERS": "'color.ui=always'"}
15
+ _label_color_cache = {}
16
+ _current_color_index = 0
17
+
18
+ def __init__(self, repos):
19
+ self._force_color_env = self._force_color_env | os.environ.copy()
20
+ self._last_printed_lines = 0
21
+ self.repos = repos
22
+
23
+ # `mud info` command implementation
24
+ def info(self, repos: Dict[str, List[str]]) -> None:
25
+ def get_directory_size(directory):
26
+ total_size = 0
27
+ for directory_path, directory_names, file_names in os.walk(directory):
28
+ for f in file_names:
29
+ fp = os.path.join(directory_path, f)
30
+ if os.path.isfile(fp):
31
+ total_size += os.path.getsize(fp)
32
+ return total_size
33
+
34
+ def format_size(size_in_bytes):
35
+ if size_in_bytes >= 1024 ** 3:
36
+ return f'{BOLD}{size_in_bytes / (1024 ** 3):.2f}{RESET} GB{glyphs("space")}{RED}{glyphs("weight")}{RESET}'
37
+ elif size_in_bytes >= 1024 ** 2:
38
+ return f'{BOLD}{size_in_bytes / (1024 ** 2):.2f}{RESET} MB{glyphs("space")}{YELLOW}{glyphs("weight")}{RESET}'
39
+ elif size_in_bytes >= 1024:
40
+ return f'{BOLD}{size_in_bytes / 1024:.2f}{RESET} KB{glyphs("space")}{GREEN}{glyphs("weight")}{RESET}'
41
+ else:
42
+ return f'{BOLD}{size_in_bytes}{RESET} Bytes{glyphs("space")}{BLUE}{glyphs("weight")}{RESET}'
43
+
44
+ def get_git_origin_host_icon(url: str):
45
+ icon = YELLOW + glyphs('git')
46
+
47
+ if 'azure' in url or 'visualstudio' in url:
48
+ icon = BLUE + glyphs('azure')
49
+ elif 'github' in url:
50
+ icon = GRAY + glyphs('github')
51
+ elif 'gitlab' in url:
52
+ icon = YELLOW + glyphs('gitlab')
53
+ elif 'bitbucket' in url:
54
+ icon = CYAN + glyphs('bitbucket')
55
+
56
+ icon += RESET + glyphs('space')
57
+ return icon
58
+
59
+ table = utils.get_table(['Path', 'Commits', 'User Commits', 'Size', 'Labels'])
60
+ table.align['Commits'] = 'r'
61
+ table.align['User Commits'] = 'r'
62
+ table.align['Size'] = 'r'
63
+
64
+ for path, labels in repos.items():
65
+ try:
66
+ origin_url = subprocess.check_output('git remote get-url origin', shell=True, text=True, cwd=path).strip()
67
+ except Exception:
68
+ origin_url = ''
69
+
70
+ formatted_path = f'{get_git_origin_host_icon(origin_url)}{self._get_formatted_path(path)}'
71
+ size = format_size(get_directory_size(path))
72
+ commits = f'{BOLD}{subprocess.check_output("git rev-list --count HEAD", shell=True, text=True, cwd=path).strip()}{RESET} {DIM}commits{RESET}'
73
+ user_commits = f'{GREEN}{BOLD}{subprocess.check_output("git rev-list --count --author=\"$(git config user.name)\" HEAD", shell=True, text=True, cwd=path).strip()}{RESET} {DIM}by you{RESET}'
74
+ colored_labels = self._get_formatted_labels(labels)
75
+
76
+ table.add_row([formatted_path, commits, user_commits, size, colored_labels])
77
+
78
+ utils.print_table(table)
79
+
80
+ # `mud status` command implementation
81
+ def status(self, repos: Dict[str, List[str]]) -> None:
82
+ table = utils.get_table(['Path', 'Branch', 'Origin Sync', 'Status', 'Edits'])
83
+
84
+ for path, labels in repos.items():
85
+ output = self._get_status_porcelain(path)
86
+ files = output.splitlines()
87
+
88
+ formatted_path = self._get_formatted_path(path)
89
+ branch = self._get_branch_status(path)
90
+ origin_sync = self._get_origin_sync(path)
91
+ status = self._get_status_string(files)
92
+
93
+ colored_output = []
94
+
95
+ for file in files:
96
+ file_status = file[:2].strip()
97
+ if file_status.startswith('M') or file_status.startswith('U'):
98
+ color = YELLOW
99
+ elif file_status.startswith('A') or file_status.startswith('C') or file_status.startswith('??') or file_status.startswith('!!'):
100
+ color = BRIGHT_GREEN
101
+ elif file_status.startswith('D'):
102
+ color = RED
103
+ elif file_status.startswith('R'):
104
+ color = BLUE
105
+ else:
106
+ color = CYAN
107
+
108
+ colored_output.append(self._get_formatted_path(file[3:].strip(), color))
109
+
110
+ table.add_row([formatted_path, branch, origin_sync, status, ', '.join(colored_output)])
111
+
112
+ utils.print_table(table)
113
+
114
+ # `mud labels` command implementation
115
+ def labels(self, repos: Dict[str, List[str]]) -> None:
116
+ table = utils.get_table(['Path', 'Labels'])
117
+
118
+ for path, labels in repos.items():
119
+ formatted_path = self._get_formatted_path(path)
120
+ colored_labels = self._get_formatted_labels(labels)
121
+ table.add_row([formatted_path, colored_labels])
122
+
123
+ utils.print_table(table)
124
+
125
+ # `mud log` command implementation
126
+ def log(self, repos: Dict[str, List[str]]) -> None:
127
+ table = utils.get_table(['Path', 'Branch', 'Author', 'Time', 'Message'])
128
+
129
+ for path in repos.keys():
130
+ formatted_path = self._get_formatted_path(path)
131
+ branch = self._get_branch_status(path)
132
+ author = self._get_authors_name(path)
133
+ commit = self._get_commit_message(path)
134
+
135
+ # Commit time
136
+ commit_time_cmd = subprocess.run('git log -1 --pretty=format:%cd --date=relative', shell=True, text=True, cwd=path, capture_output=True)
137
+ commit_time = commit_time_cmd.stdout.strip()
138
+
139
+ table.add_row([formatted_path, branch, author, commit_time, commit])
140
+
141
+ utils.print_table(table)
142
+
143
+ # `mud branch` command implementation
144
+ def branches(self, repos: Dict[str, List[str]]) -> None:
145
+ table = utils.get_table(['Path', 'Branches'])
146
+ all_branches = {}
147
+
148
+ # Preparing branches for sorting to display them in the right order.
149
+ for path in repos.keys():
150
+ raw_branches = [line.strip() for line in subprocess.check_output('git branch', shell=True, text=True, cwd=path).split('\n') if line.strip()]
151
+ for branch in raw_branches:
152
+ branch = branch.replace(' ', '').replace('*', '')
153
+ if branch not in all_branches:
154
+ all_branches[branch] = 0
155
+ all_branches[branch] += 1
156
+ branch_counter = Counter(all_branches)
157
+
158
+ for path, labels in repos.items():
159
+ formatted_path = self._get_formatted_path(path)
160
+ branches = subprocess.check_output('git branch --color=never', shell=True, text=True, cwd=path).splitlines()
161
+ current_branch = next((branch.lstrip('* ') for branch in branches if branch.startswith('*')), None)
162
+ branches = [branch.lstrip('* ') for branch in branches]
163
+ sorted_branches = sorted(branches, key=lambda x: branch_counter.get(x, 0), reverse=True)
164
+
165
+ if current_branch and current_branch in sorted_branches:
166
+ sorted_branches.remove(current_branch)
167
+ sorted_branches.insert(0, current_branch)
168
+
169
+ formatted_branches = self._get_formatted_branches(sorted_branches, current_branch)
170
+ table.add_row([formatted_path, formatted_branches])
171
+
172
+ utils.print_table(table)
173
+
174
+ # `mud branch` command implementation
175
+ def remote_branches(self, repos: Dict[str, List[str]]) -> None:
176
+ # TODO: merge with branches() function
177
+ table = utils.get_table(['Path', 'Branches'])
178
+ all_branches = {}
179
+
180
+ # Preparing branches for sorting to display them in the right order.
181
+ for path in repos.keys():
182
+ raw_branches = [
183
+ line.lstrip('* ').removeprefix('origin/')
184
+ for line in subprocess.check_output('git branch -r', shell=True, text=True, cwd=path).split('\n')
185
+ if line.strip() and '->' not in line
186
+ ]
187
+ for branch in raw_branches:
188
+ branch = branch.replace(' ', '').replace('*', '')
189
+ if branch not in all_branches:
190
+ all_branches[branch] = 0
191
+ all_branches[branch] += 1
192
+ branch_counter = Counter(all_branches)
193
+
194
+ for path, labels in repos.items():
195
+ formatted_path = self._get_formatted_path(path)
196
+ branches = subprocess.check_output('git branch -r --color=never', shell=True, text=True, cwd=path).splitlines()
197
+ current_branch = next((branch.lstrip('* ') for branch in branches if branch.startswith('*')), None)
198
+ branches = [branch.lstrip('* ').removeprefix('origin/') for branch in branches if '->' not in branch]
199
+ sorted_branches = sorted(branches, key=lambda x: branch_counter.get(x, 0), reverse=True)
200
+
201
+ if current_branch and current_branch in sorted_branches:
202
+ sorted_branches.remove(current_branch)
203
+ sorted_branches.insert(0, current_branch)
204
+
205
+ formatted_branches = self._get_formatted_branches(sorted_branches, current_branch)
206
+ table.add_row([formatted_path, formatted_branches])
207
+
208
+ utils.print_table(table)
209
+
210
+ # `mud tags` command implementation
211
+ def tags(self, repos: Dict[str, List[str]]) -> None:
212
+ COLORS = [196, 202, 208, 214, 220, 226, 118, 154, 190, 33, 39, 45, 51, 87, 93, 99, 105, 111, 27, 63, 69, 75, 81, 87, 123, 129, 135, 141, 147, 183, 189, 225]
213
+
214
+ tag_colors = {}
215
+
216
+ def assign_color(tag: str) -> str:
217
+ if tag not in tag_colors:
218
+ color_code = COLORS[len(tag_colors) % len(COLORS)]
219
+ tag_colors[tag] = f'\033[38;5;{color_code}m'
220
+ return tag_colors[tag]
221
+
222
+ table = utils.get_table(['Path', 'Tags'])
223
+
224
+ for path, labels in repos.items():
225
+ formatted_path = self._get_formatted_path(path)
226
+ tags = sorted([line.strip() for line in subprocess.check_output('git tag', shell=True, text=True, cwd=path).splitlines() if line.strip()], reverse=True)
227
+ tags = [f'{assign_color(tag)}{glyphs("tag")} {RESET}{tag}' for tag in tags]
228
+ tags = ' '.join(tags)
229
+ table.add_row([formatted_path, tags])
230
+
231
+ utils.print_table(table)
232
+
233
+ # `mud <COMMAND>` when run_async = 0 and run_table = 0
234
+ def run_ordered(self, repos: List[str], command: str) -> None:
235
+ for path in repos:
236
+ process = subprocess.run(command, cwd=path, universal_newlines=True, shell=True, capture_output=True, text=True, env=self._force_color_env)
237
+ self._print_process_header(path, command, process.returncode != 0, process.returncode)
238
+ if process.stdout and not process.stdout.isspace():
239
+ print(process.stdout)
240
+ if process.stderr and not process.stderr.isspace():
241
+ print(process.stderr)
242
+
243
+ # `mud <COMMAND>` when run_async = 1 and run_table = 0
244
+ async def run_async(self, repos: List[str], command: str) -> None:
245
+ sem = asyncio.Semaphore(len(repos))
246
+
247
+ async def run_process(path: str) -> None:
248
+ async with sem:
249
+ process = await asyncio.create_subprocess_shell(command, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self._force_color_env)
250
+ stdout, stderr = await process.communicate()
251
+ self._print_process_header(path, command, process.returncode != 0, process.returncode)
252
+ if stderr:
253
+ print(stderr.decode())
254
+ if stdout and not stdout.isspace():
255
+ print(stdout.decode())
256
+
257
+ await asyncio.gather(*(run_process(path) for path in repos))
258
+
259
+ # `mud <COMMAND>` when run_async = 1 and run_table = 1
260
+ async def run_async_table_view(self, repos: List[str], command: str) -> None:
261
+ sem = asyncio.Semaphore(len(repos))
262
+ table = {repo: ['', ''] for repo in repos}
263
+
264
+ async def task(repo: str) -> None:
265
+ async with sem:
266
+ await self._run_process(repo, table, command)
267
+
268
+ tasks = [asyncio.create_task(task(repo)) for repo in repos]
269
+ await asyncio.gather(*tasks)
270
+
271
+ async def _run_process(self, path: str, table: Dict[str, List[str]], command: str) -> None:
272
+ process = await asyncio.create_subprocess_shell(command, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self._force_color_env)
273
+ table[path] = ['', f'{YELLOW}{glyphs("running")}{RESET}']
274
+
275
+ while True:
276
+ line = await process.stdout.readline()
277
+ if not line:
278
+ line = await process.stderr.readline()
279
+ if not line:
280
+ break
281
+ line = line.decode().strip()
282
+ line = table[path][0] if not line.strip() else line
283
+ table[path] = [line, f'{YELLOW}{glyphs("running")}{RESET}']
284
+ self._print_process(table)
285
+
286
+ return_code = await process.wait()
287
+
288
+ if return_code == 0:
289
+ status = f'{GREEN}{glyphs("finished")}{RESET}'
290
+ else:
291
+ status = f'{RED}{glyphs("failed")} Code: {return_code}{RESET}'
292
+
293
+ table[path] = [table[path][0], status]
294
+ self._print_process(table)
295
+
296
+ def _print_process(self, info: Dict[str, List[str]]) -> None:
297
+ table = utils.get_table(['Path', 'Status', 'Output'])
298
+ for path, (line, status) in info.items():
299
+ formatted_path = self._get_formatted_path(path)
300
+ table.add_row([formatted_path, status, line])
301
+
302
+ table_str = utils.table_to_str(table)
303
+ num_lines = table_str.count('\n') + 1
304
+ self._clear_printed_lines()
305
+ utils.print_table(table)
306
+ self._last_printed_lines = num_lines
307
+
308
+ def _clear_printed_lines(self) -> None:
309
+ if self._last_printed_lines > 0:
310
+ for _ in range(self._last_printed_lines):
311
+ # Clear previous line
312
+ print('\033[A\033[K', end='')
313
+ self._last_printed_lines = 0
314
+
315
+ @staticmethod
316
+ def _get_status_porcelain(path: str) -> str:
317
+ try:
318
+ output = subprocess.check_output('git status --porcelain', shell=True, text=True, cwd=path)
319
+ return output
320
+ except Exception as e:
321
+ return str(e)
322
+
323
+ @staticmethod
324
+ def _get_status_string(files: List[str]) -> str:
325
+ modified, added, removed, moved = 0, 0, 0, 0
326
+
327
+ for file in files:
328
+ file = file.lstrip()
329
+ if file.startswith('M') or file.startswith('U'):
330
+ modified += 1
331
+ elif file.startswith('A') or file.startswith('C') or file.startswith('??') or file.startswith('!!'):
332
+ added += 1
333
+ elif file.startswith('D'):
334
+ removed += 1
335
+ elif file.startswith('R'):
336
+ moved += 1
337
+ status = ''
338
+ if added:
339
+ status += f'{BRIGHT_GREEN}{added} {glyphs("added")}{RESET} '
340
+ if modified:
341
+ status += f'{YELLOW}{modified} {glyphs("modified")}{RESET} '
342
+ if moved:
343
+ status += f'{BLUE}{moved} {glyphs("moved")}{RESET} '
344
+ if removed:
345
+ status += f'{RED}{removed} {glyphs("removed")}{RESET} '
346
+ if not files:
347
+ status = f'{GREEN}{glyphs("clear")}{RESET}'
348
+ return status
349
+
350
+ @staticmethod
351
+ def _get_branch_status(path: str) -> str:
352
+ try:
353
+ branch_cmd = subprocess.run('git rev-parse --abbrev-ref HEAD', shell=True, text=True, cwd=path, capture_output=True)
354
+ branch_stdout = branch_cmd.stdout.strip()
355
+ except subprocess.CalledProcessError:
356
+ branch_stdout = 'NA'
357
+ if '/' in branch_stdout:
358
+ branch_path = branch_stdout.split('/')
359
+ icon = Runner._get_branch_icon(branch_path[0])
360
+ return f'{icon}{RESET}{glyphs("space")}{branch_path[0]}{RESET}/{BOLD}{("/".join(branch_path[1:]))}{RESET}'
361
+ elif branch_stdout == 'HEAD':
362
+ # check if we are on tag
363
+ glyph = glyphs('tag')
364
+ color = BRIGHT_MAGENTA
365
+ info_cmd = subprocess.run('git describe --tags --exact-match', shell=True, text=True, cwd=path, capture_output=True)
366
+ info_cmd = info_cmd.stdout.strip()
367
+
368
+ if not info_cmd.strip():
369
+ glyph = glyphs("branch")
370
+ color = CYAN
371
+ info_cmd = subprocess.run('git rev-parse --short HEAD', shell=True, text=True, cwd=path, capture_output=True)
372
+ info_cmd = info_cmd.stdout.strip()
373
+
374
+ return f'{color}{glyph}{RESET}{glyphs("space")}{DIM}{branch_stdout}{RESET}:{info_cmd}'
375
+ else:
376
+ return f'{Runner._get_branch_icon(branch_stdout)}{RESET}{glyphs("space")}{branch_stdout}'
377
+
378
+ @staticmethod
379
+ def _get_origin_sync(path: str) -> str:
380
+ try:
381
+ ahead_behind_cmd = subprocess.run('git rev-list --left-right --count HEAD...@{upstream}', shell=True, text=True, cwd=path, capture_output=True)
382
+ stdout = ahead_behind_cmd.stdout.strip().split()
383
+ except subprocess.CalledProcessError:
384
+ stdout = ['0', '0']
385
+
386
+ origin_sync = ''
387
+ if len(stdout) >= 2:
388
+ ahead, behind = stdout[0], stdout[1]
389
+ if ahead and ahead != '0':
390
+ origin_sync += f'{BRIGHT_GREEN}{glyphs("ahead")} {ahead}{RESET}'
391
+ if behind and behind != '0':
392
+ if origin_sync:
393
+ origin_sync += ' '
394
+ origin_sync += f'{BRIGHT_BLUE}{glyphs("behind")} {behind}{RESET}'
395
+
396
+ if not origin_sync.strip():
397
+ origin_sync = f'{BLUE}{glyphs("synced")}{RESET}'
398
+
399
+ return origin_sync
400
+
401
+ @staticmethod
402
+ def _print_process_header(path: str, command: str, failed: bool, code: int) -> None:
403
+ command = f'{BKG_WHITE}{BLACK}{glyphs("space")}{glyphs("terminal")}{glyphs("space")}{BOLD}{command} {END_BOLD}{WHITE}{RESET}{WHITE}{BKG_BLACK}{glyphs(")")}{RESET}'
404
+ path = f'{BKG_BLACK}{glyphs("space")}{DIM}{glyphs("directory")}{END_DIM}{glyphs("space")}{Runner._get_formatted_path(path)}{glyphs("space")}{RESET}'
405
+ code = f'{BLACK}{BKG_RED if failed else BKG_GREEN}{glyphs(")")}{BRIGHT_WHITE}{glyphs("space")}{glyphs("failed") if failed else glyphs("finished")} {f"{BOLD}{code}" if failed else ""}{glyphs("space")}{RESET}{RED if failed else GREEN}{glyphs(")")}{RESET}'
406
+ print(f'{command}{path}{code}')
407
+
408
+ @staticmethod
409
+ def _get_formatted_path(path: str, color: str = None) -> str:
410
+ collapse_paths = utils.settings.config['mud'].getboolean('collapse_paths', fallback=False)
411
+
412
+ if color is None:
413
+ color = WHITE
414
+
415
+ in_quotes = path.startswith('\"') and path.endswith('\"')
416
+ quote = '\"' if in_quotes else ''
417
+
418
+ if in_quotes:
419
+ path = path.replace('\"', '')
420
+
421
+ def apply_styles(text: str) -> str:
422
+ return color + quote + text + quote + RESET
423
+
424
+ if os.path.isabs(path):
425
+ home = os.path.expanduser('~')
426
+ if path.startswith(home):
427
+ path = path.replace(home, '~', 1)
428
+ if path.startswith('/'):
429
+ path = path[1:]
430
+ parts = path.split('/')
431
+ if collapse_paths:
432
+ return apply_styles((DIM + '/'.join([p[0] for p in parts[:-1]] + [END_DIM + parts[-1]])))
433
+ else:
434
+ return apply_styles((DIM + '/'.join(parts[:-1]) + '/' + END_DIM + parts[-1]))
435
+ if '/' not in path:
436
+ return apply_styles(path)
437
+
438
+ parts = path.split('/')
439
+ if collapse_paths:
440
+ return apply_styles((DIM + '/'.join([p[0] for p in parts[:-1]] + [END_DIM + parts[-1]])))
441
+ else:
442
+ return apply_styles((DIM + '/'.join(parts[:-1]) + '/' + END_DIM + parts[-1]))
443
+
444
+ @staticmethod
445
+ def _get_authors_name(path: str) -> str:
446
+ cmd = subprocess.run('git log -1 --pretty=format:%an', shell=True, text=True, cwd=path, capture_output=True)
447
+ git_config_user_cmd = subprocess.run(['git', 'config', 'user.name'], text=True, capture_output=True)
448
+ committer_color = '' if cmd.stdout.strip() == git_config_user_cmd.stdout.strip() else DIM
449
+ return f'{committer_color}{cmd.stdout.strip()}{RESET}'
450
+
451
+ @staticmethod
452
+ def _get_commit_message(path: str) -> str:
453
+ cmd = subprocess.run('git log -1 --pretty=format:%s', shell=True, text=True, cwd=path, capture_output=True)
454
+ return cmd.stdout.strip()
455
+
456
+ @staticmethod
457
+ def _get_formatted_labels(labels: List[str]) -> str:
458
+ if len(labels) == 0:
459
+ return ''
460
+ colored_labels = ''
461
+ for label in labels:
462
+ color_index = Runner._get_color_index(label) % len(TEXT)
463
+ colored_labels += f'{TEXT[(color_index + 3) % len(TEXT)]}{glyphs("label")}{glyphs("space")}{label}{RESET} '
464
+
465
+ return colored_labels.rstrip()
466
+
467
+ @staticmethod
468
+ def _get_formatted_branches(branches: List[str], current_branch: str) -> str:
469
+ if len(branches) == 0:
470
+ return ''
471
+
472
+ output = ''
473
+ for branch in branches:
474
+ prefix = f'{BOLD}{RED}*{RESET}' if current_branch == branch else ''
475
+ icon = Runner._get_branch_icon(branch.split('/')[0])
476
+ branch = Runner._get_formatted_path(branch, BRIGHT_WHITE)
477
+ output += f'{icon}{glyphs("space")}{prefix}{BRIGHT_WHITE}{branch}{RESET} '
478
+ return output
479
+
480
+ @staticmethod
481
+ def _get_branch_icon(branch_prefix: str) -> str:
482
+ if branch_prefix in ['bugfix', 'bug', 'hotfix']:
483
+ return RED + glyphs('bugfix') + RESET
484
+ elif branch_prefix in ['feature', 'feat', 'develop']:
485
+ return GREEN + glyphs('feature') + RESET
486
+ elif branch_prefix in ['release']:
487
+ return BLUE + glyphs('release') + RESET
488
+ elif branch_prefix in ['master', 'main']:
489
+ return YELLOW + glyphs('master') + RESET
490
+ elif branch_prefix in ['test']:
491
+ return MAGENTA + glyphs('test') + RESET
492
+ else:
493
+ return CYAN + glyphs('branch') + RESET
494
+
495
+ @staticmethod
496
+ def _get_color_index(label: str) -> (str, str):
497
+ if label not in Runner._label_color_cache:
498
+ Runner._label_color_cache[label] = Runner._current_color_index
499
+ Runner._current_color_index = (Runner._current_color_index + 1) % len(BKG)
500
+ return Runner._label_color_cache[label]
mud/settings.py ADDED
@@ -0,0 +1,51 @@
1
+ import os
2
+ import configparser
3
+
4
+ MAIN_SCOPE = 'mud'
5
+ ALIAS_SCOPE = 'alias'
6
+
7
+
8
+ class Settings:
9
+ def __init__(self, file_name: str) -> None:
10
+ self.file_name = file_name
11
+ self.mud_settings = None
12
+ self.alias_settings = None
13
+ self.config = configparser.ConfigParser()
14
+ self.settings_file = os.path.join(os.path.expanduser('~'), self.file_name)
15
+ self.defaults = {
16
+ 'mud': {
17
+ 'config_path': '',
18
+ 'nerd_fonts': True,
19
+ 'run_async': True,
20
+ 'run_table': True,
21
+ 'simplify_branches': True
22
+ },
23
+ 'alias': {
24
+ 'to': 'git checkout',
25
+ 'fetch': 'git fetch',
26
+ 'pull': 'git pull',
27
+ 'push': 'git push'
28
+ }
29
+ }
30
+ self.load_settings()
31
+
32
+ def load_settings(self) -> None:
33
+ if not os.path.exists(self.settings_file):
34
+ self.config.read_dict(self.defaults)
35
+ self.save()
36
+ else:
37
+ self.config.read(self.settings_file)
38
+
39
+ self.mud_settings = {}
40
+ for key in self.defaults[MAIN_SCOPE]:
41
+ if isinstance(self.defaults[MAIN_SCOPE][key], bool):
42
+ self.mud_settings[key] = self.config.getboolean(MAIN_SCOPE, key, fallback=self.defaults[MAIN_SCOPE][key])
43
+ else:
44
+ self.mud_settings[key] = self.config.get(MAIN_SCOPE, key, fallback=self.defaults[MAIN_SCOPE][key])
45
+
46
+ if ALIAS_SCOPE in self.config:
47
+ self.alias_settings = self.config[ALIAS_SCOPE]
48
+
49
+ def save(self) -> None:
50
+ with open(self.settings_file, 'w') as configfile:
51
+ self.config.write(configfile)
mud/styles.py ADDED
@@ -0,0 +1,96 @@
1
+ BOLD = '\033[1m'
2
+ DIM = '\033[2m'
3
+ ITALIC = '\033[3m'
4
+ UNDERLINE = '\033[4m'
5
+ BLINK = '\033[5m'
6
+
7
+ STYLES = [BOLD, DIM, ITALIC, UNDERLINE, BLINK]
8
+
9
+ END_BOLD = '\033[22m'
10
+ END_DIM = '\033[22m'
11
+ END_ITALIC = '\033[23m'
12
+ END_UNDERLINE = '\033[24m'
13
+ END_BLINK = '\033[25m'
14
+
15
+ END = [END_BOLD, END_DIM, END_ITALIC, END_UNDERLINE, END_BLINK]
16
+
17
+ # Text colors
18
+ WHITE = '\033[37m'
19
+ GRAY = '\033[90m'
20
+ BLACK = '\033[30m'
21
+ RED = '\033[31m'
22
+ GREEN = '\033[32m'
23
+ YELLOW = '\033[33m'
24
+ BLUE = '\033[34m'
25
+ MAGENTA = '\033[35m'
26
+ CYAN = '\033[36m'
27
+ BRIGHT_WHITE = '\033[97m'
28
+ BRIGHT_RED = '\033[91m'
29
+ BRIGHT_GREEN = '\033[92m'
30
+ BRIGHT_YELLOW = '\033[93m'
31
+ BRIGHT_BLUE = '\033[94m'
32
+ BRIGHT_MAGENTA = '\033[95m'
33
+ BRIGHT_CYAN = '\033[96m'
34
+
35
+ TEXT = [WHITE, GRAY, BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, BRIGHT_WHITE, BRIGHT_RED, BRIGHT_GREEN, BRIGHT_YELLOW, BRIGHT_BLUE, BRIGHT_MAGENTA, BRIGHT_CYAN]
36
+
37
+ # Background colors
38
+ BKG_WHITE = '\033[47m'
39
+ BKG_MEDIUM_GRAY = '\033[100m'
40
+ BKG_BLACK = '\033[40m'
41
+ BKG_RED = '\033[41m'
42
+ BKG_GREEN = '\033[42m'
43
+ BKG_YELLOW = '\033[43m'
44
+ BKG_BLUE = '\033[44m'
45
+ BKG_MAGENTA = '\033[45m'
46
+ BKG_CYAN = '\033[46m'
47
+ BKG_BRIGHT_WHITE = '\033[107m'
48
+ BKG_BRIGHT_RED = '\033[101m'
49
+ BKG_BRIGHT_GREEN = '\033[102m'
50
+ BKG_BRIGHT_YELLOW = '\033[103m'
51
+ BKG_BRIGHT_BLUE = '\033[104m'
52
+ BKG_BRIGHT_MAGENTA = '\033[105m'
53
+ BKG_BRIGHT_CYAN = '\033[106m'
54
+
55
+ BKG = [BKG_WHITE, BKG_MEDIUM_GRAY, BKG_BLACK, BKG_RED, BKG_GREEN, BKG_YELLOW, BKG_BLUE, BKG_MAGENTA, BKG_CYAN, BKG_BRIGHT_WHITE, BKG_BRIGHT_RED, BKG_BRIGHT_GREEN, BKG_BRIGHT_YELLOW, BKG_BRIGHT_BLUE, BKG_BRIGHT_MAGENTA, BKG_BRIGHT_CYAN]
56
+
57
+ RESET = '\033[0m'
58
+
59
+ URL_START = '\033]8;;'
60
+ URL_TEXT = '\a'
61
+ URL_END = '\033]8;;\a'
62
+
63
+ ALL = BKG + TEXT + STYLES + END + [RESET, URL_START, URL_TEXT, URL_END]
64
+
65
+ GLYPHS = {
66
+ 'ahead': ['\uf062', 'Ahead'],
67
+ 'behind': ['\uf063', 'Behind'],
68
+ 'modified': ['\uf040', '*'],
69
+ 'added': ['\uf067', '+'],
70
+ 'removed': ['\uf1f8', '-'],
71
+ 'moved': ['\uf064', 'M'],
72
+ 'clear': ['\uf00c', 'Clear'],
73
+ 'synced': ['\uf00c', 'Up to date'],
74
+ 'master': ['\uf015', ''],
75
+ 'bugfix': ['\uf188', ''],
76
+ 'release': ['\uf135', ''],
77
+ 'feature': ['\uf0ad', ''],
78
+ 'test': ['\uf0c3', ''],
79
+ 'branch': ['\ue725', ''],
80
+ 'failed': ['\uf00d', 'Failed'],
81
+ 'finished': ['\uf00c', 'Finished'],
82
+ 'running': ['\uf46a', 'Running'],
83
+ 'label': ['\uf435', ''],
84
+ 'tag': ['\uf02b', '>'],
85
+ 'terminal': ['\ue795', ''],
86
+ 'directory': ['\uf4d4', ''],
87
+ '(': ['\uE0B2', ''],
88
+ ')': ['\uE0B0', ' '],
89
+ 'weight': ['\uee94', ''],
90
+ 'space': [' ', ''],
91
+ 'git': ['\uefa0', ''],
92
+ 'github': ['\uf09b', ''],
93
+ 'gitlab': ['\uf296', ''],
94
+ 'azure': ['\uebe8', ''],
95
+ 'bitbucket': ['\ue703', '']
96
+ }