summaryrefslogtreecommitdiff
path: root/attoconf/help.py
blob: 777becb718ae35400701db80473a8a023ef68b3c (plain) (blame)
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
#   Copyright 2013 Ben Longbons <b.r.longbons@gmail.com>
#
#   This file is part of attoconf.
#
#   attoconf 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.
#
#   attoconf 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 attoconf.  If not, see <http://www.gnu.org/licenses/>.



def put_line_in_width(file, line, width, indent):
    ''' Print a line with wrapping.
    '''
    line = line.rstrip(' ')
    if len(line) <= width or ' ' not in line.lstrip(' '):
        # this is not just an optimization
        file.writelines([line, '\n'])
        return
    line += ' '
    indents = indent * ' '

    initial_spaces = len(line) - len(line.lstrip(' '))
    while line.lstrip():
        space = line.rfind(' ', initial_spaces, width + 1)
        if space == -1:
            space = line.find(' ', initial_spaces)
        if space == -1:
            space = len(line)
        file.writelines([line[:space], '\n'])
        line = indents + line[space+1:].lstrip(' ')
        initial_spaces = indent


class HelpSection(object):
    ''' A help section contains some header lines and some related options.
    '''
    __slots__ = (
            'headers',
            'options',
    )
    def __init__(self):
        ''' Create an empty help section.
        '''
        self.headers = []
        self.options = []

    def add_text(self, text, hidden):
        ''' Add a header line.
        '''
        self.headers.append((hidden, text))

    def add_option(self, name, text, hidden):
        ''' Add an option with its description.
        '''
        self.options.append((hidden, name, text))

    def print(self, file, hidden, width):
        ''' Format (some of) the help text prettily.
        '''
        if self.options:
            for oh, name, ht in self.options:
                if oh <= hidden:
                    break
            else:
                return False

        # options longer than this will be split into multiple lines
        split_width = width / 4 - 2
        # longest option
        longest_opt = 0
        for oh, name, ht in self.options:
            if oh > hidden:
                continue
            l = len(name) + 2
            if l > split_width:
                continue
            if l > longest_opt:
                longest_opt = l
        if longest_opt == 0:
            longest_opt = int(split_width)

        for oh, ht in self.headers:
            if oh > hidden:
                continue
            put_line_in_width(file, ht, width, 0)

        for oh, name, ht in self.options:
            if oh > hidden:
                continue
            # no, it's not really that simple
            if len(name) > longest_opt:
                file.writelines(['  ', name, '\n'])
                name = ''
            line = '  %-*s%s' % (longest_opt, name, ht)
            put_line_in_width(file, line, width, longest_opt + 2)

        return True


def detect_terminal_width(fd, DEFAULT_WIDTH=float('inf')):
    ''' Detect the width of a terminal.
    '''
    if not isinstance(fd, int):
        fd = getattr(fd, 'fileno', lambda: -1)()
    if fd == -1:
        return DEFAULT_WIDTH

    import fcntl
    import termios
    try:
        buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, b'xx' * 4)
    except IOError as e:
        import errno
        if e.errno != errno.ENOTTY:
            raise
        return DEFAULT_WIDTH
    import struct
    ws_row, ws_col, ws_xpixel, ws_ypixel = struct.unpack('HHHH', buf)
    return ws_col


class Help(object):
    ''' Help collects the set of flavor text and option descriptions
        related to arguments.
    '''
    __slots__ = (
            'sections',
    )
    def __init__(self):
        ''' Help does not take any arguments during construction.
        '''
        self.sections = []

    def add_text(self, help, hidden):
        ''' Add a header line.

            This creates a new section if the last line wasn't a header.
        '''
        if not self.sections or self.sections[-1].options:
            self.sections.append(HelpSection())
        self.sections[-1].add_text(help, hidden)

    def add_option(self, name, help, hidden):
        ''' Add an option with its description to the current section.

            This creates a new section only if it is first.
        '''
        if not self.sections:
            self.sections.append(HelpSection())
        self.sections[-1].add_option(name, help, hidden)

    def print(self, file, hidden, width=0):
        ''' Print all the help at the given level of hidden-ness.
        '''
        if width == 0:
            width = detect_terminal_width(file)
        if width < 20:
            width = 80

        for s in self.sections:
            if s.print(file, hidden, width):
                file.write('\n')