Coverage for src/twofas/cli_support.py: 100%

26 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-01-29 11:16 +0100

1""" 

2This file contains helpers for the cli. 

3""" 

4 

5import os 

6import typing 

7 

8import configuraptor 

9import questionary 

10from configuraptor import beautify, postpone 

11from typing_extensions import Never 

12 

13from .cli_settings import CliSettings 

14 

15 

16@beautify 

17class AppState(configuraptor.TypedConfig, configuraptor.Singleton): 

18 """ 

19 Global state (settings from config + run-specific variables such as --verbose). 

20 """ 

21 

22 verbose: bool = False 

23 settings: CliSettings = postpone() 

24 

25 

26state = AppState.load({}) 

27 

28P = typing.ParamSpec("P") 

29R = typing.TypeVar("R") 

30 

31 

32@typing.overload 

33def clear(fn: typing.Callable[P, R]) -> typing.Callable[P, R]: 

34 """ 

35 When calling clear with parens, you get the same callable back. 

36 """ 

37 

38 

39@typing.overload 

40def clear(fn: None = None) -> typing.Callable[[typing.Callable[P, R]], typing.Callable[P, R]]: 

41 """ 

42 When calling clear without parens, you'll get the same callable back later. 

43 """ 

44 

45 

46def clear( 

47 fn: typing.Callable[P, R] | None = None 

48) -> typing.Callable[P, R] | typing.Callable[[typing.Callable[P, R]], typing.Callable[P, R]]: # pragma: no cover 

49 """ 

50 Clear the screen before executing a function. 

51 

52 Examples: 

53 @clear 

54 def some_fun(): ... 

55 

56 @clear() 

57 def other_func(): ... 

58 """ 

59 if fn: 

60 

61 def inner(*args: P.args, **kwargs: P.kwargs) -> R: 

62 os.system("clear") # nosec: B605 B607 

63 return fn(*args, **kwargs) 

64 

65 return inner 

66 else: 

67 return clear 

68 

69 

70@clear 

71def exit_with_clear(status_code: int) -> Never: # pragma: no cover 

72 """ 

73 First clear the screen with the @clear decorator, then exit with a specific exit code. 

74 """ 

75 exit(status_code) 

76 

77 

78def generate_custom_style( 

79 main_color: str = "green", # "#673ab7" 

80 secondary_color: str = "#673ab7", # "#f44336" 

81) -> questionary.Style: 

82 """ 

83 Reusable questionary style for all prompts of this tool. 

84 

85 Primary and secondary color can be changed, other styles stay the same for consistency. 

86 """ 

87 return questionary.Style( 

88 [ 

89 ("qmark", f"fg:{main_color} bold"), # token in front of the question 

90 ("question", "bold"), # question text 

91 ("answer", f"fg:{secondary_color} bold"), # submitted answer text behind the question 

92 ("pointer", f"fg:{main_color} bold"), # pointer used in select and checkbox prompts 

93 ("highlighted", f"fg:{main_color} bold"), # pointed-at choice in select and checkbox prompts 

94 ("selected", "fg:#cc5454"), # style for a selected item of a checkbox 

95 ("separator", "fg:#cc5454"), # separator in lists 

96 ("instruction", ""), # user instructions for select, rawselect, checkbox 

97 ("text", ""), # plain text 

98 ("disabled", "fg:#858585 italic"), # disabled choices for select and checkbox prompts 

99 ] 

100 ) 

101 

102 

103def generate_choices(choices: dict[str, str], with_exit: bool = True) -> list[questionary.Choice]: 

104 """ 

105 Turn a dict of label -> value items into a list of Choices with an automatic shortcut key (1 - 9). 

106 

107 If with_exit is True, an option with shortcut key 0 will be added to quit the program. 

108 """ 

109 result = [ 

110 questionary.Choice(key, value, shortcut_key=str(idx)) for idx, (key, value) in enumerate(choices.items(), 1) 

111 ] 

112 

113 if with_exit: 

114 result.append(questionary.Choice("Exit", "exit", shortcut_key="0")) 

115 

116 return result