curses-utils2 0.3.2__tar.gz → 0.4.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: curses-utils2
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: A curses utils
5
5
  Project-URL: homepage, https://github.com/shamilbi/curses-utils2
6
6
  Author-email: Shamil Bikineyev <shamilbi@gmail.com>
@@ -0,0 +1 @@
1
+ __version__ = '0.4.0'
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import curses
4
+ from typing import Protocol
5
+
6
+ from .proto import WinProto
7
+ from .win import win_addstr
8
+
9
+
10
+ class ListProto(Protocol):
11
+ def get_record_str(self, i: int) -> str:
12
+ pass
13
+
14
+ def records_len(self) -> int:
15
+ pass
16
+
17
+ def refresh_win_deps(self):
18
+ pass
19
+
20
+
21
+ class ListV2(WinProto):
22
+ '''
23
+ A projection of an array of records r0...rX to a window lines of string s0...sY
24
+ where si = get_record_str(j) = rj -> str, s(i+1) = get_record_str(j+1), j < records_len()
25
+ and changing cursor (i) leads to call refresh_win_deps()
26
+ '''
27
+
28
+ def __init__(
29
+ self,
30
+ proto: ListProto,
31
+ current_color: int = curses.A_BOLD,
32
+ ):
33
+ self.proto = proto
34
+ self.current_color = current_color
35
+
36
+ self.cur = 0 # cursor y
37
+ self.idx = 0 # source index
38
+
39
+ self.win: curses.window | None = None
40
+
41
+ def set_win(self, win: curses.window | None):
42
+ self.win = win
43
+ if win:
44
+ win.keypad(True)
45
+
46
+ def refresh(self):
47
+ if not self.win:
48
+ self.proto.refresh_win_deps()
49
+ return
50
+ self.win.erase()
51
+ len_ = self.proto.records_len()
52
+ if len_:
53
+ rows, _ = self.win.getmaxyx()
54
+ if not self.idx < len_: # deleted
55
+ self.idx = len_ - 1
56
+ if not self.cur < rows: # new win < old win
57
+ self.cur = rows - 1
58
+ if (rows - self.cur) > (di := len_ - self.idx):
59
+ # gap at bottom
60
+ self.cur = rows - di
61
+ self.cur = min(self.cur, self.idx)
62
+ for i in range(rows):
63
+ idx = self.idx - self.cur + i
64
+ if not idx < len_:
65
+ break
66
+ s = self.proto.get_record_str(idx)
67
+ if i == self.cur:
68
+ win_addstr(self.win, i, 0, s, attr=self.current_color)
69
+ else:
70
+ win_addstr(self.win, i, 0, s)
71
+ self.win.move(self.cur, 0)
72
+ self.win.refresh()
73
+ self.proto.refresh_win_deps()
74
+
75
+ def scroll_top(self):
76
+ self.idx = self.cur = 0
77
+ self.refresh()
78
+
79
+ def scroll_bottom(self):
80
+ if not self.win:
81
+ return
82
+ len_ = self.proto.records_len()
83
+ if not len_:
84
+ return
85
+ rows, _ = self.win.getmaxyx()
86
+ self.cur = min(rows - 1, len_ - 1)
87
+ self.idx = len_ - 1
88
+ self.refresh()
89
+
90
+ def scroll_down(self):
91
+ if not self.win:
92
+ return
93
+ len_ = self.proto.records_len()
94
+ if not len_ or not self.idx + 1 < len_:
95
+ return
96
+ rows, _ = self.win.getmaxyx()
97
+ prev_s = self.proto.get_record_str(self.idx)
98
+ next_s = self.proto.get_record_str(self.idx + 1)
99
+ win_addstr(self.win, self.cur, 0, prev_s)
100
+ if self.cur + 1 < rows:
101
+ self.cur += 1
102
+ else:
103
+ self.win.move(0, 0)
104
+ self.win.deleteln()
105
+ self.cur = rows - 1
106
+ win_addstr(self.win, self.cur, 0, next_s, attr=self.current_color)
107
+ self.idx += 1
108
+ self.win.refresh()
109
+ self.proto.refresh_win_deps()
110
+
111
+ def scroll_up(self):
112
+ if not self.win:
113
+ return
114
+ len_ = self.proto.records_len()
115
+ if not len_ or self.idx - 1 < 0:
116
+ return
117
+ prev_s = self.proto.get_record_str(self.idx)
118
+ next_s = self.proto.get_record_str(self.idx - 1)
119
+ win_addstr(self.win, self.cur, 0, prev_s)
120
+ if self.cur > 0:
121
+ self.cur -= 1
122
+ else:
123
+ self.win.move(0, 0)
124
+ self.win.insdelln(1)
125
+ win_addstr(self.win, self.cur, 0, next_s, attr=self.current_color)
126
+ self.idx -= 1
127
+ self.win.refresh()
128
+ self.proto.refresh_win_deps()
129
+
130
+ def scroll_page_down(self):
131
+ if not self.win:
132
+ return
133
+ len_ = self.proto.records_len()
134
+ if not len_:
135
+ return
136
+ rows, _ = self.win.getmaxyx()
137
+ idx = self.idx + rows
138
+ if idx < len_:
139
+ self.idx = idx
140
+ self.refresh()
141
+ else:
142
+ idx = len_ - 1
143
+ delta = idx - self.idx
144
+ if not delta:
145
+ self.scroll_bottom()
146
+ elif self.cur + delta < rows:
147
+ self.cur += delta
148
+ self.idx = idx
149
+ self.refresh()
150
+ else:
151
+ self.scroll_bottom()
152
+
153
+ def scroll_page_up(self):
154
+ if not self.win:
155
+ return
156
+ len_ = self.proto.records_len()
157
+ if not len_:
158
+ return
159
+ rows, _ = self.win.getmaxyx()
160
+ idx = self.idx - rows
161
+ if not idx < 0:
162
+ self.idx = idx
163
+ self.refresh()
164
+ else:
165
+ self.scroll_top()
166
+
167
+ def handle_input(self, ch: int) -> bool:
168
+ char = chr(ch)
169
+ if char.upper() == 'J' or ch == curses.KEY_DOWN: # Down or J
170
+ self.scroll_down()
171
+ elif char.upper() == 'K' or ch == curses.KEY_UP: # Up or K
172
+ self.scroll_up()
173
+ elif char == 'g' or ch == curses.KEY_HOME: # Move to top
174
+ self.scroll_top()
175
+ elif char == 'G' or ch == curses.KEY_END: # Move to last item
176
+ self.scroll_bottom()
177
+ elif ch == curses.KEY_NPAGE: # Page down
178
+ self.scroll_page_down()
179
+ elif ch == curses.KEY_PPAGE: # Page up
180
+ self.scroll_page_up()
181
+ else:
182
+ return False
183
+ return True
@@ -0,0 +1,56 @@
1
+ import curses
2
+
3
+ from .list1_v2 import ListProto, ListV2
4
+
5
+
6
+ class List1mProto(ListProto):
7
+ def record_move_up(self, i: int) -> bool: # type: ignore[empty-body]
8
+ pass
9
+
10
+ def record_move_down(self, i: int) -> bool: # type: ignore[empty-body]
11
+ pass
12
+
13
+
14
+ class List1mV2(ListV2):
15
+ 'List + move_up(), move_down()'
16
+
17
+ def __init__(
18
+ self,
19
+ proto: List1mProto,
20
+ current_color: int = curses.A_BOLD,
21
+ ):
22
+ super().__init__(proto, current_color)
23
+ self.proto: List1mProto = proto
24
+
25
+ def move_up(self, i: int):
26
+ if not self.proto.record_move_up(i):
27
+ return
28
+ if self.idx > 0:
29
+ self.idx -= 1
30
+ if self.cur > 0:
31
+ self.cur -= 1
32
+ self.refresh()
33
+
34
+ def move_down(self, i: int):
35
+ if not self.win:
36
+ return
37
+ if not self.proto.record_move_down(i):
38
+ return
39
+ len_ = self.proto.records_len()
40
+ rows, _ = self.win.getmaxyx()
41
+ if self.idx < len_ - 1:
42
+ self.idx += 1
43
+ if self.cur < rows - 1:
44
+ self.cur += 1
45
+ self.refresh()
46
+
47
+ def handle_input(self, ch: int) -> bool:
48
+ if super().handle_input(ch):
49
+ return True
50
+ if ch == curses.KEY_SR: # Shift-Up
51
+ self.move_up(self.idx)
52
+ elif ch == curses.KEY_SF: # Shift-Down
53
+ self.move_down(self.idx)
54
+ else:
55
+ return False
56
+ return True
@@ -0,0 +1,208 @@
1
+ from __future__ import annotations
2
+
3
+ import curses
4
+ from collections.abc import Generator
5
+ from typing import Protocol
6
+
7
+ from .proto import WinProto
8
+ from .win import win_addstr
9
+
10
+
11
+ class ListProto3(Protocol):
12
+ def get_record_str(self, i: int) -> Generator[str]:
13
+ 'yields <height> lines'
14
+
15
+ def records_len(self) -> int:
16
+ pass
17
+
18
+ def refresh_win_deps(self):
19
+ pass
20
+
21
+
22
+ class List3v2(WinProto):
23
+ 'List + every record projects to <height> window lines'
24
+
25
+ def __init__(
26
+ self,
27
+ proto: ListProto3,
28
+ height: int = 1,
29
+ current_color: int = curses.A_BOLD,
30
+ ):
31
+ self.height = height
32
+ self.proto = proto
33
+ self.current_color = current_color
34
+
35
+ self.cur = 0 # cursor y, = 0..(maxy // height), real cursor = cur * height
36
+ self.idx = 0 # source index
37
+
38
+ self.win: curses.window | None = None
39
+
40
+ def set_win(self, win: curses.window | None):
41
+ self.win = win
42
+ if win:
43
+ win.keypad(True)
44
+
45
+ def addstr(self, i: int, g: Generator[str], attr: int = 0):
46
+ if not self.win:
47
+ return
48
+ i2 = i * self.height
49
+ j = -1
50
+ for j, s in enumerate(g):
51
+ if j >= self.height:
52
+ break
53
+ if not j:
54
+ # first line
55
+ win_addstr(self.win, i2 + j, 0, s, attr=attr | curses.A_BOLD)
56
+ else:
57
+ # rest of lines
58
+ win_addstr(self.win, i2 + j, 0, s, attr=attr | curses.A_DIM)
59
+ if j < self.height - 1:
60
+ # print rest lines
61
+ from_ = j + 1
62
+ for j in range(from_, self.height):
63
+ win_addstr(self.win, i2 + j, 0, '', attr=attr)
64
+
65
+ def refresh(self):
66
+ if not self.win:
67
+ self.proto.refresh_win_deps()
68
+ return
69
+ self.win.erase()
70
+ len_ = self.proto.records_len()
71
+ if len_:
72
+ rows, _ = self.win.getmaxyx()
73
+ rows //= self.height
74
+ if not self.idx < len_: # deleted
75
+ self.idx = len_ - 1
76
+ if not self.cur < rows: # new win < old win
77
+ self.cur = rows - 1
78
+ if (rows - self.cur) > (di := len_ - self.idx):
79
+ # gap at bottom
80
+ self.cur = rows - di
81
+ self.cur = min(self.cur, self.idx)
82
+ for i in range(rows):
83
+ idx = self.idx - self.cur + i
84
+ if not idx < len_:
85
+ break
86
+ s = self.proto.get_record_str(idx)
87
+ if i == self.cur:
88
+ self.addstr(i, s, attr=self.current_color)
89
+ else:
90
+ self.addstr(i, s)
91
+ self.win.move(self.cur * self.height, 0)
92
+ self.win.refresh()
93
+ self.proto.refresh_win_deps()
94
+
95
+ def scroll_top(self):
96
+ self.idx = self.cur = 0
97
+ self.refresh()
98
+
99
+ def scroll_bottom(self):
100
+ if not self.win:
101
+ return
102
+ len_ = self.proto.records_len()
103
+ if not len_:
104
+ return
105
+ rows, _ = self.win.getmaxyx()
106
+ rows //= self.height
107
+ self.cur = min(rows - 1, len_ - 1)
108
+ self.idx = len_ - 1
109
+ self.refresh()
110
+
111
+ def scroll_down(self):
112
+ if not self.win:
113
+ return
114
+ len_ = self.proto.records_len()
115
+ if not len_ or not self.idx + 1 < len_:
116
+ return
117
+ rows, _ = self.win.getmaxyx()
118
+ rows //= self.height
119
+ prev_s = self.proto.get_record_str(self.idx)
120
+ next_s = self.proto.get_record_str(self.idx + 1)
121
+ self.addstr(self.cur, prev_s)
122
+ if self.cur + 1 < rows:
123
+ self.cur += 1
124
+ else:
125
+ self.win.move(0, 0)
126
+ self.win.deleteln()
127
+ self.win.deleteln()
128
+ self.cur = rows - 1
129
+ self.addstr(self.cur, next_s, attr=self.current_color)
130
+ self.idx += 1
131
+ self.win.refresh()
132
+ self.proto.refresh_win_deps()
133
+
134
+ def scroll_up(self):
135
+ if not self.win:
136
+ return
137
+ len_ = self.proto.records_len()
138
+ if not len_ or self.idx - 1 < 0:
139
+ return
140
+ prev_s = self.proto.get_record_str(self.idx)
141
+ next_s = self.proto.get_record_str(self.idx - 1)
142
+ self.addstr(self.cur, prev_s)
143
+ if self.cur > 0:
144
+ self.cur -= 1
145
+ else:
146
+ self.win.move(0, 0)
147
+ self.win.insdelln(self.height)
148
+ self.addstr(self.cur, next_s, attr=self.current_color)
149
+ self.idx -= 1
150
+ self.win.refresh()
151
+ self.proto.refresh_win_deps()
152
+
153
+ def scroll_page_down(self):
154
+ if not self.win:
155
+ return
156
+ len_ = self.proto.records_len()
157
+ if not len_:
158
+ return
159
+ rows, _ = self.win.getmaxyx()
160
+ rows //= self.height
161
+ idx = self.idx + rows
162
+ if idx < len_:
163
+ self.idx = idx
164
+ self.refresh()
165
+ else:
166
+ idx = len_ - 1
167
+ delta = idx - self.idx
168
+ if not delta:
169
+ self.scroll_bottom()
170
+ elif self.cur + delta < rows:
171
+ self.cur += delta
172
+ self.idx = idx
173
+ self.refresh()
174
+ else:
175
+ self.scroll_bottom()
176
+
177
+ def scroll_page_up(self):
178
+ if not self.win:
179
+ return
180
+ len_ = self.proto.records_len()
181
+ if not len_:
182
+ return
183
+ rows, _ = self.win.getmaxyx()
184
+ rows //= self.height
185
+ idx = self.idx - rows
186
+ if idx >= 0:
187
+ self.idx = idx
188
+ self.refresh()
189
+ else:
190
+ self.scroll_top()
191
+
192
+ def handle_input(self, ch: int) -> bool:
193
+ char = chr(ch)
194
+ if char.upper() == 'J' or ch == curses.KEY_DOWN: # Down or J
195
+ self.scroll_down()
196
+ elif char.upper() == 'K' or ch == curses.KEY_UP: # Up or K
197
+ self.scroll_up()
198
+ elif char == 'g' or ch == curses.KEY_HOME: # Move to top
199
+ self.scroll_top()
200
+ elif char == 'G' or ch == curses.KEY_END: # Move to last item
201
+ self.scroll_bottom()
202
+ elif ch == curses.KEY_NPAGE: # Page down
203
+ self.scroll_page_down()
204
+ elif ch == curses.KEY_PPAGE: # Page up
205
+ self.scroll_page_up()
206
+ else:
207
+ return False
208
+ return True
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ import curses
4
+ from typing import Protocol
5
+
6
+
7
+ class WinProto(Protocol):
8
+ def set_win(self, win: curses.window | None):
9
+ pass
10
+
11
+ def refresh(self):
12
+ pass
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import curses
4
+
5
+ from .proto import WinProto
6
+ from .win import win_addstr
7
+
8
+
9
+ class WinBox:
10
+ '''
11
+ win (box) + internal win (body)
12
+
13
+ '''
14
+
15
+ def __init__(self, body: WinProto, offy: int = 0, offx: int = 0):
16
+ 'offy: y offset, offx: x offset'
17
+ self.body = body
18
+ self.offy, self.offx = offy, offx
19
+
20
+ def set_win(self, win: curses.window | None):
21
+ # pylint: disable=attribute-defined-outside-init
22
+ self.win = win
23
+
24
+ if not win:
25
+ self.body.set_win(None)
26
+ return
27
+
28
+ # body win
29
+ rows, cols = win.getmaxyx()
30
+ rows2 = rows - 2 - self.offy # 2 - border
31
+ cols2 = cols - 2 - 2 * self.offx # 2 - border
32
+ if rows2 < 0 or cols < 2:
33
+ self.win = None
34
+ self.body.set_win(None)
35
+ return
36
+ if rows2 < 1 or cols2 < 1:
37
+ self.body.set_win(None)
38
+ return
39
+ win2 = win.derwin(
40
+ rows2,
41
+ cols2,
42
+ 1 + self.offy, # 1 - border
43
+ 1 + self.offx, # 1 - border
44
+ )
45
+ self.body.set_win(win2)
46
+
47
+ def refresh(self, header: str = ''):
48
+ if not self.win:
49
+ return
50
+
51
+ self.win.erase()
52
+ if header:
53
+ # put header above body
54
+ win_addstr(self.win, 1, 1 + self.offx, header, border=1 + self.offx)
55
+ self.win.box()
56
+ self.win.refresh()
57
+
58
+ self.body.refresh()
@@ -1 +0,0 @@
1
- __version__ = '0.3.2'
File without changes
File without changes