def draw_choice(self, y: int) -> None: if y + 3 <= self.screen_size.rows: self.draw_choice_boxes(y, *self.choices.values()) return self.clickable_ranges.clear() current_line = '' current_ranges: Dict[str, int] = {} width = self.screen_size.cols - 2 def commit_line(end: str = '\r\n') -> None: nonlocal current_line, y x = extra_for(wcswidth(current_line), width) self.print(' ' * x + current_line, end=end) for letter, sz in current_ranges.items(): self.clickable_ranges[letter] = [Range(x, x + sz - 3, y)] x += sz current_ranges.clear() y += 1 current_line = '' for letter, choice in self.choices.items(): text = choice.text[:choice.idx] text += styled(choice.text[choice.idx], fg=choice.color or 'green', underline='straight' if letter == self.response_on_accept else None) text += choice.text[choice.idx + 1:] text += ' ' sz = wcswidth(text) if sz + wcswidth(current_line) >= width: commit_line() current_line += text current_ranges[letter] = sz if current_line: commit_line(end='')
def write(self, write: Callable[[str], None], prompt: str = '', screen_cols: int = 0) -> None: if self.pending_bell: write('\a') self.pending_bell = False ci = self.current_input if self.is_password: ci = '*' * wcswidth(ci) text = prompt + ci cursor_pos = self.cursor_pos + wcswidth(prompt) if screen_cols: write(SAVE_CURSOR + text + RESTORE_CURSOR) used_lines, last_line_cursor_pos = divmod(cursor_pos, screen_cols) if used_lines == 0: if last_line_cursor_pos: write(move_cursor_by(last_line_cursor_pos, 'right')) else: if used_lines: write(move_cursor_by(used_lines, 'down')) if last_line_cursor_pos: write(move_cursor_by(last_line_cursor_pos, 'right')) else: write(text) write('\r') if cursor_pos: write(move_cursor_by(cursor_pos, 'right')) write(set_cursor_shape('bar'))
def draw_status_line(self) -> None: if self.state < DIFFED: return self.enforce_cursor_state() self.cmd.set_cursor_position(0, self.num_lines) self.cmd.clear_to_eol() if self.state is COMMAND: self.line_edit.write(self.write) elif self.state is MESSAGE: self.cmd.styled(self.message, reverse=True) else: sp = '{:.0%}'.format(self.scroll_pos/self.max_scroll_pos) if self.scroll_pos and self.max_scroll_pos else '0%' scroll_frac = styled(sp, fg=self.opts.margin_fg) if self.current_search is None: counts = '{}{}{}'.format( styled(str(self.added_count), fg=self.opts.highlight_added_bg), styled(',', fg=self.opts.margin_fg), styled(str(self.removed_count), fg=self.opts.highlight_removed_bg) ) else: counts = styled('{} matches'.format(len(self.current_search)), fg=self.opts.margin_fg) suffix = counts + ' ' + scroll_frac prefix = styled(':', fg=self.opts.margin_fg) filler = self.screen_size.cols - wcswidth(prefix) - wcswidth(suffix) text = '{}{}{}'.format(prefix, ' ' * filler, suffix) self.write(text)
def draw_status_line(self) -> None: if self.state.value < State.diffed.value: return self.enforce_cursor_state() self.cmd.set_cursor_position(0, self.num_lines) self.cmd.clear_to_eol() if self.state is State.command: self.line_edit.write(self.write) elif self.state is State.message: self.cmd.styled(self.message, reverse=True) else: sp = f'{self.scroll_pos/self.max_scroll_pos:.0%}' if self.scroll_pos and self.max_scroll_pos else '0%' scroll_frac = styled(sp, fg=self.opts.margin_fg) if self.current_search is None: counts = '{}{}{}'.format( styled(str(self.added_count), fg=self.opts.highlight_added_bg), styled(',', fg=self.opts.margin_fg), styled(str(self.removed_count), fg=self.opts.highlight_removed_bg)) else: counts = styled(f'{len(self.current_search)} matches', fg=self.opts.margin_fg) suffix = f'{counts} {scroll_frac}' prefix = styled(':', fg=self.opts.margin_fg) filler = self.screen_size.cols - wcswidth(prefix) - wcswidth( suffix) text = '{}{}{}'.format(prefix, ' ' * filler, suffix) self.write(text)
def fit_in(text, count): w = wcswidth(text) if w <= count: return text text = text[:count - 1] while wcswidth(text) > count - 1: text = text[:-1] return text + '…'
def test_utils(self): def w(x): return wcwidth(ord(x)) self.ae(tuple(map(w, 'a1\0コニチ ✔')), (1, 1, 0, 2, 2, 2, 1, 1)) self.ae(wcswidth('\u2716\u2716\ufe0f\U0001f337'), 5) self.ae(wcswidth('\033a\033[2mb'), 2) self.ae(wcswidth('\u25b6\ufe0f'), 2) self.ae(wcswidth('\U0001f610\ufe0e'), 1) # Regional indicator symbols (unicode flags) are defined as having # Emoji_Presentation so must have width 2 self.ae(tuple(map(w, '\U0001f1ee\U0001f1f3')), (2, 2)) tpl = truncate_point_for_length self.ae(tpl('abc', 4), 3) self.ae(tpl('abc', 2), 2) self.ae(tpl('abc', 0), 0) self.ae(tpl('a\U0001f337', 2), 1) self.ae(tpl('a\U0001f337', 3), 2) self.ae(tpl('a\U0001f337b', 4), 3) self.ae(sanitize_title('a\0\01 \t\n\f\rb'), 'a b') self.ae(tpl('a\x1b[31mbc', 2), 7) def tp(*data, leftover='', text='', csi='', apc='', ibp=False): text_r, csi_r, apc_r, rest = [], [], [], [] left = '' in_bp = ibp def on_csi(x): nonlocal in_bp if x == '200~': in_bp = True elif x == '201~': in_bp = False csi_r.append(x) for d in data: left = parse_input_from_terminal(text_r.append, rest.append, on_csi, rest.append, rest.append, apc_r.append, left + d, in_bp) self.ae(left, leftover) self.ae(text, ' '.join(text_r)) self.ae(csi, ' '.join(csi_r)) self.ae(apc, ' '.join(apc_r)) self.assertFalse(rest) tp('a\033[200~\033[32mxy\033[201~\033[33ma', text='a \033[32m xy a', csi='200~ 201~ 33m') tp('abc', text='abc') tp('a\033[38:5:12:32mb', text='a b', csi='38:5:12:32m') tp('a\033_x,;(\033\\b', text='a b', apc='x,;(') tp('a\033', '[', 'mb', text='a b', csi='m') tp('a\033[', 'mb', text='a b', csi='m') tp('a\033', '_', 'x\033', '\\b', text='a b', apc='x') tp('a\033_', 'x', '\033', '\\', 'b', text='a b', apc='x') for prefix in ('/tmp', tempfile.gettempdir()): for path in ('a.png', 'x/b.jpg', 'y/../c.jpg'): self.assertTrue(is_path_in_temp_dir(os.path.join(prefix, path))) for path in ('/home/xy/d.png', '/tmp/../home/x.jpg'): self.assertFalse(is_path_in_temp_dir(os.path.join(path)))
def _right(self) -> None: if not self.current_input: self.cursor_pos = 0 return max_pos = wcswidth(self.current_input) if self.cursor_pos >= max_pos: self.cursor_pos = max_pos return before, after = self.split_at_cursor(1) self.cursor_pos += 1 + int(wcswidth(before) == self.cursor_pos)
def render_path_in_width(path: str, width: int) -> str: if os.altsep: path = path.replace(os.altsep, os.sep) if wcswidth(path) <= width: return path parts = path.split(os.sep) reduced = os.sep.join(map(reduce_to_single_grapheme, parts[:-1])) path = os.path.join(reduced, parts[-1]) if wcswidth(path) <= width: return path x = truncate_point_for_length(path, width - 1) return path[:x] + '…'
def draw_yesno(self, y: int) -> None: yes = styled('Y', fg='green') + 'es' no = styled('N', fg='red') + 'o' if y + 3 <= self.screen_size.rows: self.draw_choice_boxes(y, Choice('Yes', 0, 'green', 'y'), Choice('No', 0, 'red', 'n')) return sep = ' ' * 3 text = yes + sep + no w = wcswidth(text) x = extra_for(w, self.screen_size.cols - 2) nx = x + wcswidth(yes) + len(sep) self.clickable_ranges = {'y': [Range(x, x + wcswidth(yes) - 1, y)], 'n': [Range(nx, nx + wcswidth(no) - 1, y)]} self.print(' ' * x + text, end='')
def test_utils(self): def w(x): return wcwidth(ord(x)) self.ae(tuple(map(w, 'a1\0コニチ ✔')), (1, 1, 0, 2, 2, 2, 1, 1)) self.ae(wcswidth('\u2716\u2716\ufe0f\U0001f337'), 5) self.ae(wcswidth('\033a\033[2mb'), 2) tpl = truncate_point_for_length self.ae(tpl('abc', 4), 3) self.ae(tpl('abc', 2), 2) self.ae(tpl('abc', 0), 0) self.ae(tpl('a\U0001f337', 2), 1) self.ae(tpl('a\U0001f337', 3), 2) self.ae(tpl('a\U0001f337b', 4), 3) self.ae(sanitize_title('a\0\01 \t\n\f\rb'), 'a b') self.ae(tpl('a\x1b[31mbc', 2), 7) def tp(*data, leftover='', text='', csi='', apc='', ibp=False): text_r, csi_r, apc_r, rest = [], [], [], [] left = '' in_bp = ibp def on_csi(x): nonlocal in_bp if x == '200~': in_bp = True elif x == '201~': in_bp = False csi_r.append(x) for d in data: left = parse_input_from_terminal(text_r.append, rest.append, on_csi, rest.append, rest.append, apc_r.append, left + d, in_bp) self.ae(left, leftover) self.ae(text, ' '.join(text_r)) self.ae(csi, ' '.join(csi_r)) self.ae(apc, ' '.join(apc_r)) self.assertFalse(rest) tp('a\033[200~\033[32mxy\033[201~\033[33ma', text='a \033[32m xy a', csi='200~ 201~ 33m') tp('abc', text='abc') tp('a\033[38:5:12:32mb', text='a b', csi='38:5:12:32m') tp('a\033_x,;(\033\\b', text='a b', apc='x,;(') tp('a\033', '[', 'mb', text='a b', csi='m') tp('a\033[', 'mb', text='a b', csi='m') tp('a\033', '_', 'x\033', '\\b', text='a b', apc='x') tp('a\033_', 'x', '\033', '\\', 'b', text='a b', apc='x')
def write(self, write: Callable[[str], None], prompt: str = '') -> None: if self.pending_bell: write('\a') self.pending_bell = False write(prompt) write(self.current_input) write('\r\x1b[{}C'.format(self.cursor_pos + wcswidth(prompt)))
def _left(self) -> None: if not self.current_input: self.cursor_pos = 0 return if self.cursor_pos: before, after = self.split_at_cursor(-1) self.cursor_pos = wcswidth(before)
def write(self, write, prompt=''): if self.pending_bell: write('\a') self.pending_bell = False write(prompt) write(self.current_input) write('\r\x1b[{}C'.format(self.cursor_pos + wcswidth(prompt)))
def cell(i: int, idx: str, c: str, desc: str) -> Generator[str, None, None]: yield colored(idx, 'green') + ' ' yield colored(c, 'gray', True) w = wcswidth(c) if w < 2: yield ' ' * (2 - w)
def _draw_line(self, current_line: AbsoluteLine) -> None: y = current_line - self.point.top_line # type: ScreenLine line = self.lines[current_line - 1] clear_eol = '\x1b[m\x1b[K' sgr0 = '\x1b[m' plain = unstyled(line) selection_sgr = '\x1b[38{};48{}m'.format( color_as_sgr(self.opts.selection_foreground), color_as_sgr(self.opts.selection_background)) start, end = self._start_end() # anti-flicker optimization if self.mark_type.line_inside_region(current_line, start, end): self.cmd.set_cursor_position(0, y) self.print('{}{}'.format(selection_sgr, plain), end=clear_eol) return self.cmd.set_cursor_position(0, y) self.print('{}{}'.format(sgr0, line), end=clear_eol) if self.mark_type.line_outside_region(current_line, start, end): return start_x, end_x = self.mark_type.selection_in_line( current_line, start, end, wcswidth(plain)) if start_x is None or end_x is None: return line_slice, half = string_slice(plain, start_x, end_x) self.cmd.set_cursor_position(start_x - (1 if half else 0), y) self.print('{}{}'.format(selection_sgr, line_slice), end='')
def format_completions(self, substitution: str, matches: Sequence[str], longest_match_length: int) -> None: import readline print() files, dirs = [], [] for m in matches: if m.endswith('/'): if len(m) > 1: m = m[:-1] dirs.append(m) else: files.append(m) ss = screen_size_function()() if dirs: print(styled('Directories', bold=True, fg_intense=True)) print_table(dirs, ss, self.dircolors) if files: print(styled('Files', bold=True, fg_intense=True)) print_table(files, ss, self.dircolors) buf = readline.get_line_buffer() x = readline.get_endidx() buflen = wcswidth(buf) print(self.prompt, buf, sep='', end='') if x < buflen: pos = x + self.prompt_len print(f"\r\033[{pos}C", end='') print(sep='', end='', flush=True)
def mark(text, args, Mark, extra_cli_args, *a): if extra_cli_args and extra_cli_args[0] not in button_map: print(f"The key `{extra_cli_args[0]}` is unknown.") print(f"You must specify one of: {', '.join(button_map.keys())}") return if args.type == "emoji" or args.type == "emoji_char_and_name": import demoji if demoji.last_downloaded_timestamp() is None: demoji.download_codes() demoji.set_emoji_pattern() if args.type == "emoji": regex = demoji._EMOJI_PAT else: emoji_name_pattern = r":[a-z0-9_+-]+:" regex = re.compile(r"{}|{}".format(demoji._EMOJI_PAT.pattern, emoji_name_pattern)) args.minimum_match_length = 1 else: pattern, _ = functions_for(args) regex = re.compile(pattern) for idx, (s, e, _) in enumerate( regex_finditer(regex, args.minimum_match_length, text)): lines = text[:s].split("\n") y = len(lines) - 1 x = wcswidth(lines[-1]) mark_text = text[s:e].replace("\n", "").replace("\0", "") yield Mark(idx, s, e, mark_text, {"x": x, "y": y})
def draw_status_line(self): if self.state < DIFFED: return self.cmd.set_cursor_position(0, self.num_lines) self.cmd.clear_to_eol() scroll_frac = styled('{:.0%}'.format(self.scroll_pos / (self.max_scroll_pos or 1)), fg=self.opts.margin_fg) counts = '{}{}{}'.format( styled(str(self.added_count), fg=self.opts.highlight_added_bg), styled(',', fg=self.opts.margin_fg), styled(str(self.removed_count), fg=self.opts.highlight_removed_bg)) suffix = counts + ' ' + scroll_frac prefix = styled(':', fg=self.opts.margin_fg) filler = self.screen_size.cols - wcswidth(prefix) - wcswidth(suffix) text = '{}{}{}'.format(prefix, ' ' * filler, suffix) self.write(text)
def update_prompt(self): self.update_current_char() if self.current_char is None: c, color = '??', 'red' else: c, color = self.current_char, 'green' w = wcswidth(c) self.prompt = self.prompt_template.format(colored(c, color)) self.promt_len = w + len(self.prompt_template) - 2
def backspace(self, num: int = 1) -> bool: before, after = self.split_at_cursor() nbefore = before[:-num] if nbefore != before: self.current_input = nbefore + after self.cursor_pos = wcswidth(nbefore) return True self.pending_bell = True return False
def delete(self, num: int = 1) -> bool: before, after = self.split_at_cursor() nafter = after[num:] if nafter != after: self.current_input = before + nafter self.cursor_pos = wcswidth(before) return True self.pending_bell = True return False
def word_left(self) -> Position: if self.point.x > 0: line = unstyled(self.lines[self.point.line - 1]) pos = truncate_point_for_length(line, self.point.x) pred = (self._is_word_char if self._is_word_char(line[pos - 1]) else self._is_word_separator) new_pos = pos - len(''.join(takewhile(pred, reversed(line[:pos])))) return Position(wcswidth(line[:new_pos]), self.point.y, self.point.top_line) if self.point.y > 0: return Position( wcswidth(unstyled(self.lines[self.point.line - 2])), self.point.y - 1, self.point.top_line) if self.point.top_line > 1: return Position( wcswidth(unstyled(self.lines[self.point.line - 2])), self.point.y, self.point.top_line - 1) return self.point
def draw_long_text(self, text: str) -> int: y = 0 width = self.screen_size.cols - 2 while text: t, text = truncate_at_space(text, width) t = t.strip() self.print(' ' * extra_for(wcswidth(t), width), styled(t, bold=True), sep='') y += 1 return y
def render_progress_in_width( path: str, max_path_length: int = 80, spinner_char: str = '⠋', bytes_per_sec: float = 1024, secs_so_far: float = 100., bytes_so_far: int = 33070, total_bytes: int = 50000, width: int = 80, is_complete: bool = False, ) -> str: unit_style = styled('|', dim=True) sep, trail = unit_style.split('|') if is_complete or bytes_so_far >= total_bytes: ratio = human_size(total_bytes, sep=sep) rate = human_size(int(safe_divide(total_bytes, secs_so_far)), sep=sep) + '/s' eta = styled(render_seconds(secs_so_far), fg='green') else: tb = human_size(total_bytes, sep=' ', max_num_of_decimals=1) val = float(tb.split(' ', 1)[0]) ratio = format_number(val * safe_divide(bytes_so_far, total_bytes), max_num_of_decimals=1) + '/' + tb.replace( ' ', sep) rate = human_size(int(bytes_per_sec), sep=sep) + '/s' bytes_left = total_bytes - bytes_so_far eta_seconds = safe_divide(bytes_left, bytes_per_sec) eta = render_seconds(eta_seconds) lft = f'{spinner_char} ' max_space_for_path = width // 2 - wcswidth(lft) w = min(max_path_length, max_space_for_path) p = lft + render_path_in_width(path, w) w += wcswidth(lft) p = ljust(p, w) q = f'{ratio}{trail}{styled(" @ ", fg="yellow")}{rate}{trail}' q = rjust(q, 25) + ' ' eta = ' ' + eta extra = width - w - wcswidth(q) - wcswidth(eta) if extra > 4: q += render_progress_bar(safe_divide(bytes_so_far, total_bytes), extra) + eta else: q += eta.strip() return p + q
def add_text(self, text: str) -> None: if self.current_input: x = truncate_point_for_length( self.current_input, self.cursor_pos) if self.cursor_pos else 0 self.current_input = self.current_input[: x] + text + self.current_input[ x:] else: self.current_input = text self.cursor_pos += wcswidth(text)
def commit_line(end: str = '\r\n') -> None: nonlocal current_line, y x = extra_for(wcswidth(current_line), width) self.print(' ' * x + current_line, end=end) for letter, sz in current_ranges.items(): self.clickable_ranges[letter] = [Range(x, x + sz - 3, y)] x += sz current_ranges.clear() y += 1 current_line = ''
def draw_screen(self) -> None: self.cmd.clear_screen() y = 1 if self.cli_opts.message: self.cmd.styled(self.cli_opts.message, bold=True) y += wcswidth(self.cli_opts.message) // self.screen_size.cols self.print() if self.cli_opts.type == 'yesno': self.draw_yesno(y) else: self.draw_choice(y)
def print_line(add_borders: Callable[[str], str], *items: Tuple[str, str], is_last: bool = False) -> None: nonlocal y texts = [] positions = [] x = 0 for (letter, text) in items: positions.append((letter, x, wcswidth(text) + 2)) text = add_borders(text) if letter == self.response_on_accept: text = highlight(text, only_edges=add_borders is middle) text += sep x += wcswidth(text) texts.append(text) line = ''.join(texts).rstrip() offset = extra_for(wcswidth(line), width) for (letter, x, sz) in positions: x += offset self.clickable_ranges[letter].append(Range(x, x + sz - 1, y)) self.print(' ' * offset, line, sep='', end='' if is_last else '\r\n') y += 1
def draw_title_bar(self) -> None: entries = [] for name, key, mode in all_modes: entry = ' {} ({}) '.format(name, key) if mode is self.mode: entry = styled(entry, reverse=False, bold=True) entries.append(entry) text = _('Search by:{}').format(' '.join(entries)) extra = self.screen_size.cols - wcswidth(text) if extra > 0: text += ' ' * extra self.print(styled(text, reverse=True))
def word_right(self) -> Position: line = unstyled(self.lines[self.point.line - 1]) pos = truncate_point_for_length(line, self.point.x) if pos < len(line): pred = (self._is_word_char if self._is_word_char(line[pos]) else self._is_word_separator) new_pos = pos + len(''.join(takewhile(pred, line[pos:]))) return Position(wcswidth(line[:new_pos]), self.point.y, self.point.top_line) if self.point.y < self.screen_size.rows - 1: return Position(0, self.point.y + 1, self.point.top_line) if self.point.top_line + self.point.y < len(self.lines): return Position(0, self.point.y, self.point.top_line + 1) return self.point