omdev 0.0.0.dev461__py3-none-any.whl → 0.0.0.dev463__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.
@@ -27,7 +27,7 @@
27
27
  "module": ".cexts.cmake",
28
28
  "attr": "_CLI_MODULE",
29
29
  "file": "omdev/cexts/cmake.py",
30
- "line": 332,
30
+ "line": 335,
31
31
  "value": {
32
32
  "!.cli.types.CliModule": {
33
33
  "name": "cmake",
omdev/cexts/cmake.py CHANGED
@@ -266,7 +266,10 @@ class CmakeProjectGen:
266
266
  '-g',
267
267
  '-c',
268
268
  ],
269
- ['-std=c++20'],
269
+ [
270
+ '$<$<COMPILE_LANGUAGE:C>:-std=c11>',
271
+ '$<$<COMPILE_LANGUAGE:CXX>:-std=c++20>',
272
+ ],
270
273
  ),
271
274
  ))
272
275
 
@@ -2972,6 +2972,12 @@ def format_num_bytes(num_bytes: int) -> str:
2972
2972
 
2973
2973
  ##
2974
2974
  # A workaround for typing deficiencies (like `Argument 2 to NewType(...) must be subclassable`).
2975
+ #
2976
+ # Note that this problem doesn't happen at runtime - it happens in mypy:
2977
+ #
2978
+ # mypy <(echo "import typing as ta; MyCallback = ta.NewType('MyCallback', ta.Callable[[], None])")
2979
+ # /dev/fd/11:1:22: error: Argument 2 to NewType(...) must be subclassable (got "Callable[[], None]") [valid-newtype]
2980
+ #
2975
2981
 
2976
2982
 
2977
2983
  @dc.dataclass(frozen=True)
File without changes
@@ -0,0 +1,4 @@
1
+ if __name__ == '__main__':
2
+ from .main import _main
3
+
4
+ _main()
@@ -0,0 +1,278 @@
1
+ """
2
+ TODO:
3
+ - use omdev.irc obv
4
+ - readliney input
5
+ - command suggest / autocomplete
6
+ - disable command palette
7
+ - grow input box for multiline
8
+ - styled text
9
+ """
10
+ import shlex
11
+ import typing as ta
12
+
13
+ from omlish import lang
14
+
15
+ from ... import textual
16
+ from .client import IrcClient
17
+ from .commands import ALL_COMMANDS
18
+ from .commands import IrcCommand
19
+
20
+
21
+ ##
22
+
23
+
24
+ class IrcWindow:
25
+ """Represents a chat window."""
26
+
27
+ def __init__(self, name: str) -> None:
28
+ super().__init__()
29
+
30
+ self.name: str = name
31
+ self.lines: list[str] = []
32
+ self.unread: int = 0
33
+ self.displayed_line_count: int = 0 # Track how many lines are currently displayed
34
+
35
+ def add_line(self, line: str) -> None:
36
+ self.lines.append(line)
37
+ self.unread += 1
38
+
39
+
40
+ class IrcApp(textual.App):
41
+ """IRC client application."""
42
+
43
+ _commands: ta.ClassVar[ta.Mapping[str, IrcCommand]] = ALL_COMMANDS
44
+
45
+ CSS = """
46
+ #messages {
47
+ height: 1fr;
48
+ overflow-y: auto;
49
+ border: none;
50
+ padding: 0;
51
+ }
52
+
53
+ #status {
54
+ height: 1;
55
+ background: $primary;
56
+ color: $text;
57
+ border: none;
58
+ padding: 0;
59
+ }
60
+
61
+ #input {
62
+ dock: bottom;
63
+ border: none;
64
+ padding: 0;
65
+ }
66
+ """
67
+
68
+ BINDINGS: ta.ClassVar[ta.Sequence[textual.Binding]] = [
69
+ textual.Binding('ctrl+n', 'next_window', 'Next Window', show=False),
70
+ textual.Binding('ctrl+p', 'prev_window', 'Previous Window', show=False),
71
+ ]
72
+
73
+ def __init__(
74
+ self,
75
+ *,
76
+ startup_commands: ta.Sequence[str] | None = None,
77
+ ) -> None:
78
+ super().__init__()
79
+
80
+ self._client: IrcClient | None = None
81
+ self._windows: dict[str, IrcWindow] = {'system': IrcWindow('system')}
82
+ self._window_order: list[str] = ['system']
83
+ self._current_window_idx: int = 0
84
+ self._current_channel: str | None = None
85
+ self._startup_commands: ta.Sequence[str] = startup_commands or []
86
+
87
+ @property
88
+ def client(self) -> IrcClient | None:
89
+ return self._client
90
+
91
+ @property
92
+ def current_channel(self) -> str | None:
93
+ return self._current_channel
94
+
95
+ #
96
+
97
+ def compose(self) -> textual.ComposeResult:
98
+ text_area = textual.TextArea(id='messages', read_only=True, show_line_numbers=False)
99
+ text_area.cursor_blink = False
100
+ yield text_area
101
+ yield textual.Static('', id='status')
102
+ yield textual.Input(placeholder='Enter command or message', id='input', select_on_focus=False)
103
+
104
+ async def on_mount(self) -> None:
105
+ """Initialize on mount."""
106
+
107
+ self._client = IrcClient(self.on_irc_message)
108
+ self.update_display()
109
+ self.query_one('#input').focus()
110
+
111
+ # Show connection prompt
112
+ await self.add_message('system', 'IRC Client - Use /connect <server> <port> <nickname>')
113
+ await self.add_message('system', 'Example: /connect irc.libera.chat 6667 mynick')
114
+
115
+ # Execute startup commands
116
+ for cmd in self._startup_commands:
117
+ # Add leading slash if not present
118
+ if not cmd.startswith('/'):
119
+ cmd = '/' + cmd
120
+ await self.add_message('system', f'Executing: {cmd}')
121
+ await self.handle_command(cmd)
122
+
123
+ async def on_key(self, event: textual.Key) -> None:
124
+ """Handle key events - redirect typing to input when messages area is focused."""
125
+
126
+ focused = self.focused
127
+ if focused and focused.id == 'messages':
128
+ # If a printable character or common input key is pressed, focus the input and forward event
129
+ if event.is_printable or event.key in ('space', 'backspace', 'delete'):
130
+ input_widget = self.query_one('#input', textual.Input)
131
+ input_widget.focus()
132
+ # Post the key event to the input widget so it handles it naturally
133
+ input_widget.post_message(textual.Key(event.key, event.character))
134
+ # Stop the event from being processed by the messages widget
135
+ event.stop()
136
+
137
+ async def on_input_submitted(self, event: textual.Input.Submitted) -> None:
138
+ """Handle user input."""
139
+
140
+ text = event.value.strip()
141
+ event.input.value = ''
142
+
143
+ if not text:
144
+ return
145
+
146
+ # Handle commands
147
+ if text.startswith('/'):
148
+ await self.handle_command(text)
149
+ else:
150
+ # Send message to current channel
151
+ if self._current_channel and self._client and self._client.connected:
152
+ await self._client.privmsg(self._current_channel, text)
153
+ await self.add_message(self._current_channel, f'<{self._client.nickname}> {text}')
154
+ else:
155
+ await self.add_message('system', 'Not in a channel or not connected')
156
+
157
+ async def handle_command(self, text: str) -> None:
158
+ """Handle IRC commands."""
159
+
160
+ try:
161
+ parts = shlex.split(text)
162
+ except ValueError as e:
163
+ await self.add_message('system', f'Invalid command syntax: {e}')
164
+ return
165
+
166
+ if not parts:
167
+ return
168
+
169
+ cmd = parts[0].lstrip('/').lower()
170
+ argv = parts[1:]
171
+
172
+ command = self._commands.get(cmd)
173
+ if command:
174
+ await command.run(self, argv)
175
+ else:
176
+ await self.add_message('system', f'Unknown command: /{cmd}')
177
+
178
+ def action_next_window(self) -> None:
179
+ """Switch to next window."""
180
+
181
+ if len(self._window_order) > 1:
182
+ self._current_window_idx = (self._current_window_idx + 1) % len(self._window_order)
183
+ self.update_display()
184
+
185
+ def action_prev_window(self) -> None:
186
+ """Switch to previous window."""
187
+
188
+ if len(self._window_order) > 1:
189
+ self._current_window_idx = (self._current_window_idx - 1) % len(self._window_order)
190
+ self.update_display()
191
+
192
+ def get_or_create_window(self, name: str) -> IrcWindow:
193
+ """Get or create a window."""
194
+
195
+ if name not in self._windows:
196
+ self._windows[name] = IrcWindow(name)
197
+ self._window_order.append(name)
198
+ return self._windows[name]
199
+
200
+ def switch_to_window(self, name: str) -> None:
201
+ """Switch to a specific window."""
202
+
203
+ if name in self._window_order:
204
+ self._current_window_idx = self._window_order.index(name)
205
+ self.update_display()
206
+
207
+ async def add_message(self, window_name: str, message: str) -> None:
208
+ """Add a message to a window."""
209
+
210
+ window = self.get_or_create_window(window_name)
211
+ timestamp = lang.utcnow().strftime('%H:%M')
212
+ window.add_line(f'[{timestamp}] {message}')
213
+ self.update_display()
214
+
215
+ async def on_irc_message(self, window_name: str, message: str) -> None:
216
+ """Callback for IRC messages."""
217
+
218
+ await self.add_message(window_name, message)
219
+
220
+ _last_window: str | None = None
221
+
222
+ def update_display(self) -> None:
223
+ """Update the display."""
224
+
225
+ current_window_name = self._window_order[self._current_window_idx]
226
+ current_window = self._windows[current_window_name]
227
+
228
+ # Update current channel for sending messages
229
+ self._current_channel = current_window_name if current_window_name.startswith('#') else None
230
+
231
+ # Mark as read
232
+ current_window.unread = 0
233
+
234
+ # Update messages display
235
+ messages_widget = self.query_one('#messages', textual.TextArea)
236
+
237
+ # Check if we switched windows or need full reload
238
+ window_changed = self._last_window != current_window_name
239
+ self._last_window = current_window_name
240
+
241
+ lines_to_show = current_window.lines[-100:] # Last 100 lines
242
+
243
+ if window_changed:
244
+ # Full reload when switching windows
245
+ messages_widget.load_text('\n'.join(lines_to_show))
246
+ current_window.displayed_line_count = len(lines_to_show)
247
+
248
+ else:
249
+ # Append only new lines
250
+ new_line_count = len(lines_to_show) - current_window.displayed_line_count
251
+ if new_line_count > 0:
252
+ new_lines = lines_to_show[-new_line_count:]
253
+ # Get the end position
254
+ doc = messages_widget.document
255
+ end_line = doc.line_count - 1
256
+ end_col = len(doc.get_line(end_line))
257
+ # Append new lines using insert
258
+ prefix = '\n' if len(doc.text) > 0 else ''
259
+ messages_widget.insert(prefix + '\n'.join(new_lines), location=(end_line, end_col))
260
+ current_window.displayed_line_count = len(lines_to_show)
261
+
262
+ # Update status line
263
+ window_list = []
264
+ for i, name in enumerate(self._window_order):
265
+ win = self._windows[name]
266
+ indicator = f'[{i + 1}:{name}]'
267
+ if i == self._current_window_idx:
268
+ indicator = f'[{i + 1}:{name}*]'
269
+ elif win.unread > 0:
270
+ indicator = f'[{i + 1}:{name}({win.unread})]'
271
+ window_list.append(indicator)
272
+
273
+ status_text = ' '.join(window_list)
274
+ self.query_one('#status', textual.Static).update(status_text)
275
+
276
+ async def on_unmount(self) -> None:
277
+ if (cl := self._client) is not None:
278
+ await cl.shutdown()
@@ -0,0 +1,187 @@
1
+ import asyncio
2
+ import ssl
3
+ import typing as ta
4
+
5
+ from omlish import check
6
+
7
+
8
+ ##
9
+
10
+
11
+ class IrcClient:
12
+ """Simple asyncio-based IRC client."""
13
+
14
+ def __init__(
15
+ self,
16
+ callback: ta.Callable[[str, str], ta.Awaitable[None]],
17
+ ) -> None:
18
+ super().__init__()
19
+
20
+ self.reader: asyncio.StreamReader | None = None
21
+ self.writer: asyncio.StreamWriter | None = None
22
+ self.callback: ta.Callable[[str, str], ta.Awaitable[None]] = callback
23
+ self.nickname: str = ''
24
+ self.connected: bool = False
25
+ self.read_task: asyncio.Task | None = None
26
+
27
+ async def shutdown(self) -> None:
28
+ if self.read_task is not None:
29
+ self.read_task.cancel()
30
+ await self.read_task
31
+
32
+ async def connect(
33
+ self,
34
+ server: str,
35
+ port: int,
36
+ nickname: str,
37
+ realname: str = 'Textual IRC',
38
+ use_ssl: bool | None = None,
39
+ ) -> bool:
40
+ """Connect to IRC server."""
41
+
42
+ check.none(self.read_task)
43
+
44
+ try:
45
+ # Auto-detect SSL for common SSL ports if not explicitly specified
46
+ if use_ssl is None:
47
+ use_ssl = port in (6697, 6698, 7000, 7070, 9999)
48
+
49
+ ssl_context = None
50
+ if use_ssl:
51
+ ssl_context = ssl.create_default_context()
52
+
53
+ self.reader, self.writer = await asyncio.open_connection(
54
+ server, port, ssl=ssl_context,
55
+ )
56
+ self.nickname = nickname
57
+ self.connected = True
58
+
59
+ # Send initial IRC handshake
60
+ await self.send_raw(f'NICK {nickname}')
61
+ await self.send_raw(f'USER {nickname} 0 * :{realname}')
62
+
63
+ # Start reading messages
64
+ self.read_task = asyncio.create_task(self._read_loop())
65
+
66
+ return True
67
+
68
+ except Exception as e: # noqa # FIXME
69
+ await self.callback('system', f'Connection error: {e}')
70
+ return False
71
+
72
+ async def send_raw(self, message: str) -> None:
73
+ """Send raw IRC message."""
74
+
75
+ if self.writer:
76
+ self.writer.write(f'{message}\r\n'.encode())
77
+ await self.writer.drain()
78
+
79
+ async def join(self, channel: str) -> None:
80
+ """Join a channel."""
81
+
82
+ await self.send_raw(f'JOIN {channel}')
83
+
84
+ async def part(self, channel: str, message: str = 'Leaving') -> None:
85
+ """Leave a channel."""
86
+
87
+ await self.send_raw(f'PART {channel} :{message}')
88
+
89
+ async def privmsg(self, target: str, message: str) -> None:
90
+ """Send a message to a channel or user."""
91
+
92
+ await self.send_raw(f'PRIVMSG {target} :{message}')
93
+
94
+ async def names(self, channel: str) -> None:
95
+ """Request names list for a channel."""
96
+
97
+ await self.send_raw(f'NAMES {channel}')
98
+
99
+ async def quit(self, message: str = 'Quit') -> None:
100
+ """Quit IRC."""
101
+
102
+ await self.send_raw(f'QUIT :{message}')
103
+ if self.writer:
104
+ self.writer.close()
105
+ await self.writer.wait_closed()
106
+ self.connected = False
107
+
108
+ async def _read_loop(self) -> None:
109
+ """Read messages from server."""
110
+
111
+ try:
112
+ while self.connected and self.reader:
113
+ line = await self.reader.readline()
114
+ if not line:
115
+ break
116
+
117
+ message = line.decode('utf-8', errors='ignore').strip()
118
+ if message:
119
+ await self._handle_message(message)
120
+
121
+ except Exception as e: # noqa # FIXME
122
+ await self.callback('system', f'Read error: {e}')
123
+
124
+ finally:
125
+ self.connected = False
126
+ await self.callback('system', 'Disconnected from server')
127
+
128
+ async def _handle_message(self, message: str) -> None:
129
+ """Parse and handle IRC messages."""
130
+
131
+ # Handle PING
132
+ if message.startswith('PING'):
133
+ pong = message.replace('PING', 'PONG', 1)
134
+ await self.send_raw(pong)
135
+ return
136
+
137
+ # Parse IRC message
138
+ prefix = ''
139
+ if message.startswith(':'):
140
+ prefix, _, message = message[1:].partition(' ')
141
+
142
+ parts = message.split(' ', 2)
143
+ command = parts[0] if parts else ''
144
+ params = parts[1:] if len(parts) > 1 else []
145
+
146
+ # Extract nickname from prefix
147
+ nick = prefix.split('!')[0] if '!' in prefix else prefix
148
+
149
+ # Handle different message types
150
+ if command == 'PRIVMSG' and len(params) >= 2:
151
+ target = params[0]
152
+ text = params[1]
153
+ text = text.removeprefix(':')
154
+
155
+ window = target if target.startswith('#') else nick
156
+ await self.callback(window, f'<{nick}> {text}')
157
+
158
+ elif command == 'JOIN' and params:
159
+ channel = params[0].lstrip(':')
160
+ await self.callback(channel, f'* {nick} has joined {channel}')
161
+
162
+ elif command == 'PART' and params:
163
+ channel = params[0]
164
+ reason = params[1].lstrip(':') if len(params) > 1 else ''
165
+ msg = f'* {nick} has left {channel}'
166
+ if reason:
167
+ msg += f' ({reason})'
168
+ await self.callback(channel, msg)
169
+
170
+ elif command == 'QUIT':
171
+ reason = params[0].lstrip(':') if params else ''
172
+ msg = f'* {nick} has quit'
173
+ if reason:
174
+ msg += f' ({reason})'
175
+ await self.callback('system', msg)
176
+
177
+ elif command == 'NICK' and params:
178
+ new_nick = params[0].lstrip(':')
179
+ await self.callback('system', f'* {nick} is now known as {new_nick}')
180
+
181
+ elif command.isdigit():
182
+ # Numeric replies
183
+ reply_text = ' '.join(params).lstrip(':')
184
+ await self.callback('system', f'[{command}] {reply_text}')
185
+ else:
186
+ # Unhandled messages go to system
187
+ await self.callback('system', message)
@@ -0,0 +1,175 @@
1
+ import abc
2
+ import typing as ta
3
+
4
+ from omlish import check
5
+ from omlish import lang
6
+ from omlish.argparse import all as argparse
7
+
8
+
9
+ if ta.TYPE_CHECKING:
10
+ from .app import IrcApp
11
+
12
+
13
+ ##
14
+
15
+
16
+ class IrcCommand(abc.ABC):
17
+ """Abstract base class for IRC commands."""
18
+
19
+ def __init__(self) -> None:
20
+ super().__init__()
21
+
22
+ self.__parser: argparse.ArgumentParser = argparse.NoExitArgumentParser(
23
+ prog=self.name,
24
+ description=self.description,
25
+ formatter_class=self._HelpFormatter,
26
+ )
27
+ self._configure_parser(self.__parser)
28
+
29
+ #
30
+
31
+ @property
32
+ def name(self) -> str:
33
+ return lang.camel_to_snake(type(self).__name__.removesuffix('IrcCommand'))
34
+
35
+ @property
36
+ def description(self) -> str | None:
37
+ return None
38
+
39
+ class _HelpFormatter(argparse.HelpFormatter):
40
+ def start_section(self, heading):
41
+ return super().start_section(heading.title())
42
+
43
+ def add_usage(self, usage, actions, groups, prefix=None):
44
+ if prefix is None:
45
+ prefix = 'Usage: '
46
+ return super().add_usage(usage, actions, groups, prefix)
47
+
48
+ def _configure_parser(self, parser: argparse.ArgumentParser) -> None:
49
+ pass
50
+
51
+ #
52
+
53
+ @ta.final
54
+ async def run(self, app: 'IrcApp', argv: list[str]) -> None:
55
+ try:
56
+ args = self.__parser.parse_args(argv)
57
+ except argparse.ArgumentError:
58
+ await app.add_message('system', self.__parser.format_help())
59
+ return
60
+
61
+ await self._run_args(app, args)
62
+
63
+ @abc.abstractmethod
64
+ async def _run_args(self, app: 'IrcApp', args: argparse.Namespace) -> None:
65
+ raise NotImplementedError
66
+
67
+
68
+ class ConnectIrcCommand(IrcCommand):
69
+ """Connect to an IRC server."""
70
+
71
+ description: ta.ClassVar[str] = 'Connect to an IRC server'
72
+
73
+ def _configure_parser(self, parser: argparse.ArgumentParser) -> None:
74
+ parser.add_argument('server', help='IRC server hostname')
75
+ parser.add_argument('port', type=int, help='Port number')
76
+ parser.add_argument('nickname', help='Your nickname')
77
+ parser.add_argument(
78
+ '--ssl',
79
+ action='store_true',
80
+ help='Use SSL/TLS (auto-detected for common ports)',
81
+ )
82
+
83
+ async def _run_args(self, app: 'IrcApp', args: argparse.Namespace) -> None:
84
+ use_ssl = args.ssl or None # None triggers auto-detection
85
+ ssl_msg = ' (SSL)' if args.ssl or args.port in (6697, 6698, 7000, 7070, 9999) else ''
86
+ await app.add_message('system', f'Connecting to {args.server}:{args.port}{ssl_msg} as {args.nickname}...')
87
+ success = await check.not_none(app.client).connect(
88
+ args.server,
89
+ args.port,
90
+ args.nickname,
91
+ use_ssl=use_ssl if args.ssl else None,
92
+ )
93
+ if success:
94
+ await app.add_message('system', 'Connected!')
95
+
96
+
97
+ class JoinIrcCommand(IrcCommand):
98
+ """Join a channel."""
99
+
100
+ description: ta.ClassVar[str] = 'Join an IRC channel'
101
+
102
+ def _configure_parser(self, parser: argparse.ArgumentParser) -> None:
103
+ parser.add_argument('channel', help='Channel name (# prefix optional)')
104
+
105
+ async def _run_args(self, app: 'IrcApp', args: argparse.Namespace) -> None:
106
+ channel = args.channel
107
+ if not channel.startswith('#'):
108
+ channel = '#' + channel
109
+
110
+ if app.client and app.client.connected:
111
+ await app.client.join(channel)
112
+ app.get_or_create_window(channel)
113
+ app.switch_to_window(channel)
114
+ else:
115
+ await app.add_message('system', 'Not connected to server')
116
+
117
+
118
+ class PartIrcCommand(IrcCommand):
119
+ """Leave a channel."""
120
+
121
+ description: ta.ClassVar[str] = 'Leave an IRC channel'
122
+
123
+ def _configure_parser(self, parser: argparse.ArgumentParser) -> None:
124
+ parser.add_argument('reason', nargs='*', help='Part message (optional)')
125
+
126
+ async def _run_args(self, app: 'IrcApp', args: argparse.Namespace) -> None:
127
+ if app.current_channel:
128
+ reason = ' '.join(args.reason) if args.reason else 'Leaving'
129
+ if app.client and app.client.connected:
130
+ await app.client.part(app.current_channel, reason)
131
+ else:
132
+ await app.add_message('system', 'Not in a channel')
133
+
134
+
135
+ class NamesIrcCommand(IrcCommand):
136
+ """Request names list for current channel."""
137
+
138
+ description: ta.ClassVar[str] = 'Request names list for current channel'
139
+
140
+ async def _run_args(self, app: 'IrcApp', args: argparse.Namespace) -> None:
141
+ if app.current_channel:
142
+ if app.client and app.client.connected:
143
+ await app.client.names(app.current_channel)
144
+ else:
145
+ await app.add_message('system', 'Not connected to server')
146
+ else:
147
+ await app.add_message('system', 'Not in a channel')
148
+
149
+
150
+ class QuitIrcCommand(IrcCommand):
151
+ """Quit IRC."""
152
+
153
+ description: ta.ClassVar[str] = 'Quit IRC and exit the application'
154
+
155
+ def _configure_parser(self, parser: argparse.ArgumentParser) -> None:
156
+ parser.add_argument('message', nargs='*', help='Quit message (optional)')
157
+
158
+ async def _run_args(self, app: 'IrcApp', args: argparse.Namespace) -> None:
159
+ reason = ' '.join(args.message) if args.message else 'Quit'
160
+ if app.client and app.client.connected:
161
+ await app.client.quit(reason)
162
+ await app.add_message('system', 'Goodbye!')
163
+ app.exit()
164
+
165
+
166
+ ##
167
+
168
+
169
+ ALL_COMMANDS: ta.Mapping[str, IrcCommand] = {
170
+ 'connect': ConnectIrcCommand(),
171
+ 'join': JoinIrcCommand(),
172
+ 'part': PartIrcCommand(),
173
+ 'names': NamesIrcCommand(),
174
+ 'quit': QuitIrcCommand(),
175
+ }
@@ -0,0 +1,26 @@
1
+ from omlish.argparse import all as argparse
2
+
3
+ from .app import IrcApp
4
+
5
+
6
+ ##
7
+
8
+
9
+ def _main() -> None:
10
+ parser = argparse.ArgumentParser(description='Simple IRC client using Textual')
11
+ parser.add_argument(
12
+ '-x',
13
+ action='append',
14
+ dest='commands',
15
+ help='Execute slash command on startup (can be specified multiple times)',
16
+ )
17
+ args = parser.parse_args()
18
+
19
+ app = IrcApp(
20
+ startup_commands=args.commands,
21
+ )
22
+ app.run()
23
+
24
+
25
+ if __name__ == '__main__':
26
+ _main()
@@ -6,4 +6,205 @@ from omlish import lang as _lang
6
6
  with _lang.auto_proxy_init(globals()):
7
7
  ##
8
8
 
9
- pass
9
+ from textual import app # noqa
10
+ from textual import binding # noqa
11
+ from textual import containers # noqa
12
+ from textual import content # noqa
13
+ from textual import driver # noqa
14
+ from textual import events # noqa
15
+ from textual import markup # noqa
16
+ from textual import message # noqa
17
+ from textual import messages # noqa
18
+ from textual import pad # noqa
19
+ from textual import reactive # noqa
20
+ from textual import screen # noqa
21
+ from textual import style # noqa
22
+ from textual import suggester # noqa
23
+ from textual import suggestions # noqa
24
+ from textual import timer # noqa
25
+ from textual import widget # noqa
26
+ from textual import widgets # noqa
27
+ from textual.app import ActionError # noqa
28
+ from textual.app import ActiveModeError # noqa
29
+ from textual.app import App as App_ # noqa
30
+ from textual.app import AppError # noqa
31
+ from textual.app import AutopilotCallbackType # noqa
32
+ from textual.app import CallThreadReturnType # noqa
33
+ from textual.app import CommandCallback # noqa
34
+ from textual.app import ComposeResult # noqa
35
+ from textual.app import InvalidModeError # noqa
36
+ from textual.app import InvalidThemeError # noqa
37
+ from textual.app import ModeError # noqa
38
+ from textual.app import RenderResult # noqa
39
+ from textual.app import ReturnType # noqa
40
+ from textual.app import ScreenError # noqa
41
+ from textual.app import ScreenStackError # noqa
42
+ from textual.app import ScreenType # noqa
43
+ from textual.app import SuspendNotSupported # noqa
44
+ from textual.app import SystemCommand # noqa
45
+ from textual.app import UnknownModeError # noqa
46
+ from textual.app import get_system_commands_provider # noqa
47
+ from textual.binding import ActiveBinding # noqa
48
+ from textual.binding import Binding # noqa
49
+ from textual.binding import BindingError # noqa
50
+ from textual.binding import BindingIDString # noqa
51
+ from textual.binding import BindingType # noqa
52
+ from textual.binding import BindingsMap # noqa
53
+ from textual.binding import InvalidBinding # noqa
54
+ from textual.binding import KeyString # noqa
55
+ from textual.binding import Keymap # noqa
56
+ from textual.binding import KeymapApplyResult # noqa
57
+ from textual.binding import NoBinding # noqa
58
+ from textual.containers import Center # noqa
59
+ from textual.containers import CenterMiddle # noqa
60
+ from textual.containers import Container # noqa
61
+ from textual.containers import Grid # noqa
62
+ from textual.containers import Horizontal # noqa
63
+ from textual.containers import HorizontalGroup # noqa
64
+ from textual.containers import HorizontalScroll # noqa
65
+ from textual.containers import ItemGrid # noqa
66
+ from textual.containers import Middle # noqa
67
+ from textual.containers import Right # noqa
68
+ from textual.containers import ScrollableContainer # noqa
69
+ from textual.containers import Vertical # noqa
70
+ from textual.containers import VerticalGroup # noqa
71
+ from textual.containers import VerticalScroll # noqa
72
+ from textual.content import Content # noqa
73
+ from textual.content import ContentText # noqa
74
+ from textual.content import ContentType # noqa
75
+ from textual.content import EMPTY_CONTENT # noqa
76
+ from textual.content import Span # noqa
77
+ from textual.driver import Driver # noqa
78
+ from textual.events import Action # noqa
79
+ from textual.events import AppBlur # noqa
80
+ from textual.events import AppFocus # noqa
81
+ from textual.events import Blur # noqa
82
+ from textual.events import Callback # noqa
83
+ from textual.events import Click # noqa
84
+ from textual.events import Compose # noqa
85
+ from textual.events import CursorPosition # noqa
86
+ from textual.events import DeliveryComplete # noqa
87
+ from textual.events import DeliveryFailed # noqa
88
+ from textual.events import DescendantBlur # noqa
89
+ from textual.events import DescendantFocus # noqa
90
+ from textual.events import Enter # noqa
91
+ from textual.events import Event # noqa
92
+ from textual.events import Focus # noqa
93
+ from textual.events import Hide # noqa
94
+ from textual.events import Idle # noqa
95
+ from textual.events import InputEvent # noqa
96
+ from textual.events import Key # noqa
97
+ from textual.events import Leave # noqa
98
+ from textual.events import Load # noqa
99
+ from textual.events import Mount # noqa
100
+ from textual.events import MouseCapture # noqa
101
+ from textual.events import MouseDown # noqa
102
+ from textual.events import MouseEvent # noqa
103
+ from textual.events import MouseMove # noqa
104
+ from textual.events import MouseRelease # noqa
105
+ from textual.events import MouseScrollDown # noqa
106
+ from textual.events import MouseScrollLeft # noqa
107
+ from textual.events import MouseScrollRight # noqa
108
+ from textual.events import MouseScrollUp # noqa
109
+ from textual.events import MouseUp # noqa
110
+ from textual.events import Paste # noqa
111
+ from textual.events import Print # noqa
112
+ from textual.events import Ready # noqa
113
+ from textual.events import Resize # noqa
114
+ from textual.events import ScreenResume # noqa
115
+ from textual.events import ScreenSuspend # noqa
116
+ from textual.events import Show # noqa
117
+ from textual.events import Timer as TimerEvent # noqa
118
+ from textual.events import Unmount # noqa
119
+ from textual.markup import MarkupError # noqa
120
+ from textual.markup import MarkupTokenizer # noqa
121
+ from textual.markup import StyleTokenizer # noqa
122
+ from textual.markup import escape # noqa
123
+ from textual.markup import parse_style # noqa
124
+ from textual.markup import to_content # noqa
125
+ from textual.message import Message # noqa
126
+ from textual.messages import CloseMessages # noqa
127
+ from textual.messages import ExitApp # noqa
128
+ from textual.messages import InBandWindowResize # noqa
129
+ from textual.messages import InvokeLater # noqa
130
+ from textual.messages import Layout # noqa
131
+ from textual.messages import Prompt # noqa
132
+ from textual.messages import Prune # noqa
133
+ from textual.messages import ScrollToRegion # noqa
134
+ from textual.messages import TerminalSupportsSynchronizedOutput # noqa
135
+ from textual.messages import Update # noqa
136
+ from textual.messages import UpdateScroll # noqa
137
+ from textual.pad import HorizontalPad # noqa
138
+ from textual.reactive import Initialize # noqa
139
+ from textual.reactive import Reactive # noqa
140
+ from textual.reactive import ReactiveError # noqa
141
+ from textual.reactive import await_watcher # noqa
142
+ from textual.reactive import invoke_watcher # noqa
143
+ from textual.reactive import reactive as reactive_ # noqa
144
+ from textual.reactive import var # noqa
145
+ from textual.screen import ModalScreen # noqa
146
+ from textual.screen import Screen # noqa
147
+ from textual.screen import SystemModalScreen # noqa
148
+ from textual.style import Style # noqa
149
+ from textual.suggester import SuggestFromList # noqa
150
+ from textual.suggester import Suggester # noqa
151
+ from textual.suggester import SuggestionReady # noqa
152
+ from textual.suggestions import get_suggestion # noqa
153
+ from textual.suggestions import get_suggestions # noqa
154
+ from textual.timer import Timer # noqa
155
+ from textual.timer import TimerCallback # noqa
156
+ from textual.widget import Widget # noqa
157
+ from textual.widgets import Button # noqa
158
+ from textual.widgets import Checkbox # noqa
159
+ from textual.widgets import Collapsible # noqa
160
+ from textual.widgets import CollapsibleTitle # noqa
161
+ from textual.widgets import ContentSwitcher # noqa
162
+ from textual.widgets import DataTable # noqa
163
+ from textual.widgets import Digits # noqa
164
+ from textual.widgets import DirectoryTree # noqa
165
+ from textual.widgets import Footer # noqa
166
+ from textual.widgets import Header # noqa
167
+ from textual.widgets import HelpPanel # noqa
168
+ from textual.widgets import Input # noqa
169
+ from textual.widgets import KeyPanel # noqa
170
+ from textual.widgets import Label # noqa
171
+ from textual.widgets import Link # noqa
172
+ from textual.widgets import ListItem # noqa
173
+ from textual.widgets import ListView # noqa
174
+ from textual.widgets import LoadingIndicator # noqa
175
+ from textual.widgets import Log # noqa
176
+ from textual.widgets import Markdown # noqa
177
+ from textual.widgets import MarkdownViewer # noqa
178
+ from textual.widgets import MaskedInput # noqa
179
+ from textual.widgets import OptionList # noqa
180
+ from textual.widgets import Placeholder # noqa
181
+ from textual.widgets import Pretty # noqa
182
+ from textual.widgets import ProgressBar # noqa
183
+ from textual.widgets import RadioButton # noqa
184
+ from textual.widgets import RadioSet # noqa
185
+ from textual.widgets import RichLog # noqa
186
+ from textual.widgets import Rule # noqa
187
+ from textual.widgets import Select # noqa
188
+ from textual.widgets import SelectionList # noqa
189
+ from textual.widgets import Sparkline # noqa
190
+ from textual.widgets import Static # noqa
191
+ from textual.widgets import Switch # noqa
192
+ from textual.widgets import Tab # noqa
193
+ from textual.widgets import TabPane # noqa
194
+ from textual.widgets import TabbedContent # noqa
195
+ from textual.widgets import Tabs # noqa
196
+ from textual.widgets import TextArea # noqa
197
+ from textual.widgets import Tooltip # noqa
198
+ from textual.widgets import Tree # noqa
199
+ from textual.widgets import Welcome # noqa
200
+
201
+ ##
202
+
203
+ from .app2 import ( # noqa
204
+ App,
205
+ )
206
+
207
+ from .drivers2 import ( # noqa
208
+ PendingWritesDriverMixin,
209
+ get_pending_writes_driver_class,
210
+ )
@@ -0,0 +1,11 @@
1
+ import typing as ta
2
+
3
+ from textual.app import App as App_
4
+ from textual.binding import BindingType # noqa
5
+
6
+
7
+ ##
8
+
9
+
10
+ class App(App_):
11
+ BINDINGS: ta.ClassVar[ta.Sequence[BindingType]] = App_.BINDINGS # type: ignore[assignment]
@@ -0,0 +1,55 @@
1
+ import threading
2
+ import typing as ta
3
+
4
+ from omlish import lang
5
+
6
+
7
+ if ta.TYPE_CHECKING:
8
+ from textual.driver import Driver
9
+
10
+
11
+ ##
12
+
13
+
14
+ class PendingWritesDriverMixin:
15
+ def __init__(self, *args: ta.Any, **kwargs: ta.Any) -> None:
16
+ super().__init__(*args, **kwargs)
17
+
18
+ self._pending_primary_buffer_writes: list[str] = []
19
+
20
+ def queue_primary_buffer_write(self, *s: str) -> None:
21
+ self._pending_primary_buffer_writes.extend(s)
22
+
23
+ def write(self, data: str) -> None:
24
+ if (pw := self._pending_primary_buffer_writes):
25
+ data = ''.join([*pw, data])
26
+ pw.clear()
27
+ super().write(data) # type: ignore
28
+
29
+
30
+ _PENDING_WRITES_DRIVER_CLASSES_LOCK = threading.RLock()
31
+ _PENDING_WRITES_DRIVER_CLASSES: dict[type['Driver'], type['Driver']] = {}
32
+
33
+
34
+ def get_pending_writes_driver_class(cls: type['Driver']) -> type['Driver']:
35
+ if issubclass(cls, PendingWritesDriverMixin):
36
+ return cls # noqa
37
+
38
+ try:
39
+ return _PENDING_WRITES_DRIVER_CLASSES[cls]
40
+ except KeyError:
41
+ pass
42
+
43
+ with _PENDING_WRITES_DRIVER_CLASSES_LOCK:
44
+ try:
45
+ return _PENDING_WRITES_DRIVER_CLASSES[cls]
46
+ except KeyError:
47
+ pass
48
+
49
+ cls = _PENDING_WRITES_DRIVER_CLASSES[cls] = lang.new_type( # noqa
50
+ f'PendingWrites{cls.__name__}',
51
+ (PendingWritesDriverMixin, cls),
52
+ {},
53
+ )
54
+
55
+ return cls # noqa
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omdev
3
- Version: 0.0.0.dev461
3
+ Version: 0.0.0.dev463
4
4
  Summary: omdev
5
5
  Author: wrmsr
6
6
  License-Expression: BSD-3-Clause
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Python: >=3.13
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
- Requires-Dist: omlish==0.0.0.dev461
17
+ Requires-Dist: omlish==0.0.0.dev463
18
18
  Provides-Extra: all
19
19
  Requires-Dist: black~=25.9; extra == "all"
20
20
  Requires-Dist: pycparser~=2.23; extra == "all"
@@ -1,4 +1,4 @@
1
- omdev/.omlish-manifests.json,sha256=YqhbZs4Wr3L9DAwcFfhTs8Zw8W8MxPLaxYXp2aqbyzQ,11671
1
+ omdev/.omlish-manifests.json,sha256=cMWrMB14_AFp9mQiTEeoTYL1uaC3zq7ER7-XHGCXY8I,11671
2
2
  omdev/__about__.py,sha256=TwPTq6a1_K0K-oDBQf6x2VmkiD5hi6W8Q7CvmTW9Aic,1223
3
3
  omdev/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  omdev/cmake.py,sha256=gu49t10_syXh_TUJs4POsxeFs8we8Y3XTOOPgIXmGvg,4608
@@ -52,7 +52,7 @@ omdev/cc/srclangs.py,sha256=3u_APHgknuc-BPRQSDISwSzouluKsnLlBxodp-XCl_E,728
52
52
  omdev/cexts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
53
  omdev/cexts/_boilerplate.cc,sha256=tUTFKyMpammFj-7bF8DbN6jv67Vn7qrwCFDoAgarof8,1753
54
54
  omdev/cexts/build.py,sha256=JxEuqSpspnY6kWn8eB89FjcSjF4bpNgVYNeIvwmWehY,2659
55
- omdev/cexts/cmake.py,sha256=7CSL9AsWaJnad94dw9QxbZLirtxdlg7Au7EQ1g7H74g,10050
55
+ omdev/cexts/cmake.py,sha256=rwsUVjOMjNR9nBCMgK-oWLl0aD5WWUYzVQEYcZ5BZGs,10185
56
56
  omdev/cexts/importhook.py,sha256=GZ04bE6tMQ8uQUUzGr22Rt9cwyI6kkoK4t7xpLP2Lrs,3562
57
57
  omdev/cexts/magic.py,sha256=DglhjCXEiL28pFTN4lrmUW6ZLn6HaumR13ptyaFOez4,131
58
58
  omdev/cexts/scan.py,sha256=94UqVgxBrAHn_VyW_ZA3-hB_R6pWIJRWqvtSBEg_O4U,1673
@@ -285,7 +285,7 @@ omdev/pyproject/resources/python.sh,sha256=rFaN4SiJ9hdLDXXsDTwugI6zsw6EPkgYMmtac
285
285
  omdev/scripts/__init__.py,sha256=MKCvUAEQwsIvwLixwtPlpBqmkMXLCnjjXyAXvVpDwVk,91
286
286
  omdev/scripts/ci.py,sha256=dmpJXF0mS37K6psivRDukXEBF8Z6CAuK_PQERDBzFgE,427466
287
287
  omdev/scripts/interp.py,sha256=GELa1TPg0tStMbofHpEYetMrAl3YnInehwler2gdE2I,168564
288
- omdev/scripts/pyproject.py,sha256=wf3hKkkAjQn7qwnY6cHTRJg3_duUi2R8a3foFaNuehQ,349235
288
+ omdev/scripts/pyproject.py,sha256=DNIVc0GALqIW2lM7YUQCmil-60FblcHgAV8Sil06fWM,349535
289
289
  omdev/scripts/slowcat.py,sha256=PwdT-pg62imEEb6kcOozl9_YUi-4KopvjvzWT1OmGb0,2717
290
290
  omdev/scripts/tmpexec.py,sha256=t0nErDRALjTk7H0X8ADjZUIDFjlPNzOOokmjCjBHdzs,1431
291
291
  omdev/scripts/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -335,16 +335,24 @@ omdev/tools/pawk/__main__.py,sha256=VCqeRVnqT1RPEoIrqHFSu4PXVMg4YEgF4qCQm90-eRI,
335
335
  omdev/tools/pawk/pawk.py,sha256=ao5mdrpiSU4AZ8mBozoEaV3UVlmVTnRG9wD9XP70MZE,11429
336
336
  omdev/tui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
337
337
  omdev/tui/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
338
+ omdev/tui/apps/irc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
339
+ omdev/tui/apps/irc/__main__.py,sha256=d23loR_cKfTYZwYiqpt_CmKI7dd5WcYFgIYzqMep75E,68
340
+ omdev/tui/apps/irc/app.py,sha256=v7jw8eKfq84FUrxNeMqbE6DXvhsrbrUgLiqms8JXYNY,9222
341
+ omdev/tui/apps/irc/client.py,sha256=H4oO-TO_2XTMWMMezqCurj46n6ARO25gCEaCCCkQgc8,5869
342
+ omdev/tui/apps/irc/commands.py,sha256=nBzE2A8Q727RYwvv9ULeoCwi0vogK9KPzT36hvsfg48,5642
343
+ omdev/tui/apps/irc/main.py,sha256=ptsSjKE3LDmPERCExivcjTN7SOfYY7WVnIlDXSL_XQA,510
338
344
  omdev/tui/apps/markdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
339
345
  omdev/tui/apps/markdown/__main__.py,sha256=Xy-G2-8Ymx8QMBbRzA4LoiAMZqvtC944mMjFEWd69CA,182
340
346
  omdev/tui/apps/markdown/cli.py,sha256=K1vH7f3ZqLv4xTPluhJBEZH8nx8n42_vXIALEV07Q50,469
341
347
  omdev/tui/rich/__init__.py,sha256=_PcNDlzl7FIa071qni4AlBAf0oXXFHUjEaPxumnuXWs,775
342
348
  omdev/tui/rich/console2.py,sha256=BYYLbbD65If9TvfPI6qUcMQKUWJbuWwykEzPplvkf6A,342
343
349
  omdev/tui/rich/markdown2.py,sha256=fBcjG_34XzUf4WclBL_MxvBj5NUwvLCANhHCx3R0akw,6139
344
- omdev/tui/textual/__init__.py,sha256=SljlE7NeWxHrWG7HyGlrtA0dTuIabXi2KidA8X9Nk5s,131
345
- omdev-0.0.0.dev461.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
346
- omdev-0.0.0.dev461.dist-info/METADATA,sha256=0xextQLtZS5bwFwkQzBFbe1FN2PXm8URpKlDFKlnCMk,5170
347
- omdev-0.0.0.dev461.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
348
- omdev-0.0.0.dev461.dist-info/entry_points.txt,sha256=dHLXFmq5D9B8qUyhRtFqTGWGxlbx3t5ejedjrnXNYLU,33
349
- omdev-0.0.0.dev461.dist-info/top_level.txt,sha256=1nr7j30fEWgLYHW3lGR9pkdHkb7knv1U1ES1XRNVQ6k,6
350
- omdev-0.0.0.dev461.dist-info/RECORD,,
350
+ omdev/tui/textual/__init__.py,sha256=l4lDY7g_spyUpCMYlzmM2jFz92xv4-VzOM88vFlV6zA,9801
351
+ omdev/tui/textual/app2.py,sha256=QNh8dX9lXtvWkUOLX5x6ucJCYqDoKD78VDpaX4ZcGS8,225
352
+ omdev/tui/textual/drivers2.py,sha256=ZVxI9n6cyczjrdjqKOAjE51pF0yppACJOVmqLaWuJuM,1402
353
+ omdev-0.0.0.dev463.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
354
+ omdev-0.0.0.dev463.dist-info/METADATA,sha256=BZD4CAxJpONI1QQgQF1aYu6dr7WvhH_OXomlZy-_guA,5170
355
+ omdev-0.0.0.dev463.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
356
+ omdev-0.0.0.dev463.dist-info/entry_points.txt,sha256=dHLXFmq5D9B8qUyhRtFqTGWGxlbx3t5ejedjrnXNYLU,33
357
+ omdev-0.0.0.dev463.dist-info/top_level.txt,sha256=1nr7j30fEWgLYHW3lGR9pkdHkb7knv1U1ES1XRNVQ6k,6
358
+ omdev-0.0.0.dev463.dist-info/RECORD,,