omdev 0.0.0.dev440__py3-none-any.whl → 0.0.0.dev486__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.
Potentially problematic release.
This version of omdev might be problematic. Click here for more details.
- omdev/.omlish-manifests.json +17 -29
- omdev/__about__.py +9 -7
- omdev/amalg/gen/gen.py +49 -6
- omdev/amalg/gen/imports.py +1 -1
- omdev/amalg/gen/manifests.py +1 -1
- omdev/amalg/gen/resources.py +1 -1
- omdev/amalg/gen/srcfiles.py +13 -3
- omdev/amalg/gen/strip.py +1 -1
- omdev/amalg/gen/types.py +1 -1
- omdev/amalg/gen/typing.py +1 -1
- omdev/amalg/info.py +32 -0
- omdev/cache/data/actions.py +1 -1
- omdev/cache/data/specs.py +1 -1
- omdev/cexts/_boilerplate.cc +2 -3
- omdev/cexts/cmake.py +4 -1
- omdev/ci/cli.py +1 -2
- omdev/cmdlog/cli.py +1 -2
- omdev/dataclasses/_dumping.py +1960 -0
- omdev/dataclasses/_template.py +22 -0
- omdev/dataclasses/cli.py +6 -1
- omdev/dataclasses/codegen.py +340 -60
- omdev/dataclasses/dumping.py +200 -0
- omdev/interp/uv/provider.py +1 -0
- omdev/interp/venvs.py +1 -0
- omdev/irc/messages/base.py +50 -0
- omdev/irc/messages/formats.py +92 -0
- omdev/irc/messages/messages.py +775 -0
- omdev/irc/messages/parsing.py +99 -0
- omdev/irc/numerics/__init__.py +0 -0
- omdev/irc/numerics/formats.py +97 -0
- omdev/irc/numerics/numerics.py +865 -0
- omdev/irc/numerics/types.py +59 -0
- omdev/irc/protocol/LICENSE +11 -0
- omdev/irc/protocol/__init__.py +61 -0
- omdev/irc/protocol/consts.py +6 -0
- omdev/irc/protocol/errors.py +30 -0
- omdev/irc/protocol/message.py +21 -0
- omdev/irc/protocol/nuh.py +55 -0
- omdev/irc/protocol/parsing.py +158 -0
- omdev/irc/protocol/rendering.py +153 -0
- omdev/irc/protocol/tags.py +102 -0
- omdev/irc/protocol/utils.py +30 -0
- omdev/manifests/_dumping.py +125 -25
- omdev/markdown/__init__.py +0 -0
- omdev/markdown/incparse.py +116 -0
- omdev/markdown/tokens.py +51 -0
- omdev/packaging/marshal.py +8 -8
- omdev/packaging/requires.py +6 -6
- omdev/packaging/specifiers.py +2 -1
- omdev/packaging/versions.py +4 -4
- omdev/packaging/wheelfile.py +2 -0
- omdev/precheck/blanklines.py +66 -0
- omdev/precheck/caches.py +1 -1
- omdev/precheck/imports.py +14 -1
- omdev/precheck/main.py +4 -3
- omdev/precheck/unicode.py +39 -15
- omdev/py/asts/__init__.py +0 -0
- omdev/py/asts/parents.py +28 -0
- omdev/py/asts/toplevel.py +123 -0
- omdev/py/asts/visitors.py +18 -0
- omdev/py/attrdocs.py +1 -1
- omdev/py/bracepy.py +12 -4
- omdev/py/reprs.py +32 -0
- omdev/py/srcheaders.py +1 -1
- omdev/py/tokens/__init__.py +0 -0
- omdev/py/tools/mkrelimp.py +1 -1
- omdev/py/tools/pipdepup.py +629 -0
- omdev/pyproject/pkg.py +190 -45
- omdev/pyproject/reqs.py +31 -9
- omdev/pyproject/tools/__init__.py +0 -0
- omdev/pyproject/tools/aboutdeps.py +55 -0
- omdev/pyproject/venvs.py +8 -1
- omdev/rs/__init__.py +0 -0
- omdev/scripts/ci.py +398 -80
- omdev/scripts/interp.py +193 -35
- omdev/scripts/lib/inject.py +74 -27
- omdev/scripts/lib/logs.py +75 -27
- omdev/scripts/lib/marshal.py +67 -25
- omdev/scripts/pyproject.py +941 -90
- omdev/tools/git/cli.py +10 -0
- omdev/tools/json/processing.py +5 -2
- omdev/tools/jsonview/cli.py +31 -5
- omdev/tools/pawk/pawk.py +2 -2
- omdev/tools/pip.py +8 -0
- omdev/tui/__init__.py +0 -0
- omdev/tui/apps/__init__.py +0 -0
- omdev/tui/apps/edit/__init__.py +0 -0
- omdev/tui/apps/edit/main.py +163 -0
- omdev/tui/apps/irc/__init__.py +0 -0
- omdev/tui/apps/irc/__main__.py +4 -0
- omdev/tui/apps/irc/app.py +278 -0
- omdev/tui/apps/irc/client.py +187 -0
- omdev/tui/apps/irc/commands.py +175 -0
- omdev/tui/apps/irc/main.py +26 -0
- omdev/tui/apps/markdown/__init__.py +0 -0
- omdev/tui/apps/markdown/__main__.py +11 -0
- omdev/{ptk → tui/apps}/markdown/cli.py +5 -7
- omdev/tui/rich/__init__.py +34 -0
- omdev/tui/rich/console2.py +20 -0
- omdev/tui/rich/markdown2.py +186 -0
- omdev/tui/textual/__init__.py +226 -0
- omdev/tui/textual/app2.py +11 -0
- omdev/tui/textual/autocomplete/LICENSE +21 -0
- omdev/tui/textual/autocomplete/__init__.py +33 -0
- omdev/tui/textual/autocomplete/matching.py +226 -0
- omdev/tui/textual/autocomplete/paths.py +202 -0
- omdev/tui/textual/autocomplete/widget.py +612 -0
- omdev/tui/textual/drivers2.py +55 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev486.dist-info}/METADATA +11 -9
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev486.dist-info}/RECORD +119 -73
- omdev/ptk/__init__.py +0 -103
- omdev/ptk/apps/ncdu.py +0 -167
- omdev/ptk/confirm.py +0 -60
- omdev/ptk/markdown/LICENSE +0 -22
- omdev/ptk/markdown/__init__.py +0 -10
- omdev/ptk/markdown/__main__.py +0 -11
- omdev/ptk/markdown/border.py +0 -94
- omdev/ptk/markdown/markdown.py +0 -390
- omdev/ptk/markdown/parser.py +0 -42
- omdev/ptk/markdown/styles.py +0 -29
- omdev/ptk/markdown/tags.py +0 -299
- omdev/ptk/markdown/utils.py +0 -366
- omdev/pyproject/cexts.py +0 -110
- /omdev/{ptk/apps → irc}/__init__.py +0 -0
- /omdev/{tokens → irc/messages}/__init__.py +0 -0
- /omdev/{tokens → py/tokens}/all.py +0 -0
- /omdev/{tokens → py/tokens}/tokenizert.py +0 -0
- /omdev/{tokens → py/tokens}/utils.py +0 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev486.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev486.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev486.dist-info}/licenses/LICENSE +0 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev486.dist-info}/top_level.txt +0 -0
|
@@ -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()
|
|
File without changes
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import sys
|
|
3
3
|
|
|
4
|
-
from ... import
|
|
5
|
-
from .markdown import Markdown
|
|
6
|
-
from .styles import MARKDOWN_STYLE
|
|
4
|
+
from ... import rich
|
|
7
5
|
|
|
8
6
|
|
|
9
7
|
##
|
|
@@ -20,10 +18,10 @@ def _main() -> None:
|
|
|
20
18
|
else:
|
|
21
19
|
src = sys.stdin.read()
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
21
|
+
console = rich.Console()
|
|
22
|
+
markdown = rich.Markdown(src)
|
|
23
|
+
console.print(markdown)
|
|
24
|
+
print()
|
|
27
25
|
|
|
28
26
|
|
|
29
27
|
if __name__ == '__main__':
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ruff: noqa: F401
|
|
2
|
+
# flake8: noqa: F401
|
|
3
|
+
from omlish import lang as _lang
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
with _lang.auto_proxy_init(globals()):
|
|
7
|
+
##
|
|
8
|
+
|
|
9
|
+
from rich import console # noqa
|
|
10
|
+
from rich import live # noqa
|
|
11
|
+
from rich import markdown # noqa
|
|
12
|
+
from rich import repr # noqa
|
|
13
|
+
from rich import text # noqa
|
|
14
|
+
from rich.console import Console # noqa
|
|
15
|
+
from rich.live import Live # noqa
|
|
16
|
+
from rich.markdown import Markdown # noqa
|
|
17
|
+
from rich.text import Text # noqa
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
|
|
21
|
+
from .console2 import ( # noqa
|
|
22
|
+
console_render,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from .markdown2 import ( # noqa
|
|
26
|
+
configure_markdown_parser,
|
|
27
|
+
markdown_from_tokens,
|
|
28
|
+
flatten_tokens_filter,
|
|
29
|
+
flatten_tokens,
|
|
30
|
+
|
|
31
|
+
MarkdownLiveStream,
|
|
32
|
+
NaiveMarkdownLiveStream,
|
|
33
|
+
IncrementalMarkdownLiveStream,
|
|
34
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import typing as ta
|
|
3
|
+
|
|
4
|
+
from omlish import lang
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
with lang.auto_proxy_import(globals()):
|
|
8
|
+
import rich.console
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
##
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def console_render(obj: ta.Any, **kwargs: ta.Any) -> str:
|
|
15
|
+
temp_console = rich.console.Console(
|
|
16
|
+
file=(out := io.StringIO()),
|
|
17
|
+
**kwargs,
|
|
18
|
+
)
|
|
19
|
+
temp_console.print(obj)
|
|
20
|
+
return out.getvalue()
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import typing as ta
|
|
3
|
+
|
|
4
|
+
from omlish import lang
|
|
5
|
+
|
|
6
|
+
from ...markdown.incparse import IncrementalMarkdownParser
|
|
7
|
+
from ...markdown.tokens import flatten_tokens as _flatten_tokens
|
|
8
|
+
from .console2 import console_render
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
with lang.auto_proxy_import(globals()):
|
|
12
|
+
import markdown_it as md # noqa
|
|
13
|
+
import markdown_it.token # noqa
|
|
14
|
+
import rich.console
|
|
15
|
+
import rich.live
|
|
16
|
+
import rich.markdown
|
|
17
|
+
import rich.text
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def configure_markdown_parser(parser: ta.Optional['md.MarkdownIt'] = None) -> 'md.MarkdownIt':
|
|
24
|
+
if parser is None:
|
|
25
|
+
parser = md.MarkdownIt()
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
parser
|
|
29
|
+
.enable('strikethrough')
|
|
30
|
+
.enable('table')
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def markdown_from_tokens(tokens: ta.Sequence['md.token.Token']) -> 'rich.markdown.Markdown':
|
|
35
|
+
rmd = rich.markdown.Markdown('')
|
|
36
|
+
rmd.parsed = tokens # type: ignore[assignment]
|
|
37
|
+
return rmd
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def flatten_tokens_filter(token: 'md.token.Token') -> bool:
|
|
41
|
+
return (
|
|
42
|
+
token.type != 'fence' and
|
|
43
|
+
token.tag != 'img'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def flatten_tokens(tokens: ta.Iterable['md.token.Token']) -> ta.Iterable['md.token.Token']:
|
|
48
|
+
return _flatten_tokens(tokens, filter=flatten_tokens_filter)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MarkdownLiveStream(lang.ExitStacked, lang.Abstract):
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
*,
|
|
58
|
+
parser: ta.Optional['md.MarkdownIt'] = None,
|
|
59
|
+
console: ta.Optional['rich.console.Console'] = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
super().__init__()
|
|
62
|
+
|
|
63
|
+
if console is None:
|
|
64
|
+
console = rich.console.Console()
|
|
65
|
+
self._console = console
|
|
66
|
+
|
|
67
|
+
if parser is None:
|
|
68
|
+
parser = configure_markdown_parser()
|
|
69
|
+
self._parser = parser
|
|
70
|
+
|
|
71
|
+
self._lines_printed_to_scrollback = 0
|
|
72
|
+
|
|
73
|
+
_live: 'rich.live.Live' # noqa
|
|
74
|
+
|
|
75
|
+
def _enter_contexts(self) -> None:
|
|
76
|
+
super()._enter_contexts()
|
|
77
|
+
|
|
78
|
+
self._live = self._enter_context(rich.live.Live(
|
|
79
|
+
rich.text.Text(''),
|
|
80
|
+
console=self._console,
|
|
81
|
+
refresh_per_second=10,
|
|
82
|
+
))
|
|
83
|
+
|
|
84
|
+
def _console_render(self, obj: ta.Any) -> list[str]:
|
|
85
|
+
return console_render(
|
|
86
|
+
obj,
|
|
87
|
+
force_terminal=True,
|
|
88
|
+
width=self._console.width,
|
|
89
|
+
).splitlines()
|
|
90
|
+
|
|
91
|
+
def _console_render_markdown(self, src: str) -> list[str]:
|
|
92
|
+
return self._console_render(markdown_from_tokens(self._parser.parse(src)))
|
|
93
|
+
|
|
94
|
+
@abc.abstractmethod
|
|
95
|
+
def feed(self, s: str) -> None:
|
|
96
|
+
raise NotImplementedError
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class NaiveMarkdownLiveStream(MarkdownLiveStream):
|
|
100
|
+
_accumulated = ''
|
|
101
|
+
|
|
102
|
+
def feed(self, s: str) -> None:
|
|
103
|
+
self._accumulated += s
|
|
104
|
+
all_lines = self._console_render_markdown(self._accumulated)
|
|
105
|
+
|
|
106
|
+
# Calculate how many lines fit in the live window
|
|
107
|
+
available_height = self._console.height - 2
|
|
108
|
+
|
|
109
|
+
# Determine which lines overflow and need to be printed to scrollback
|
|
110
|
+
total_lines = len(all_lines)
|
|
111
|
+
if total_lines > available_height:
|
|
112
|
+
# Lines that should be in scrollback
|
|
113
|
+
lines_for_scrollback = total_lines - available_height
|
|
114
|
+
|
|
115
|
+
# Print any new lines that weren't already printed
|
|
116
|
+
if lines_for_scrollback > self._lines_printed_to_scrollback:
|
|
117
|
+
new_lines_to_print = all_lines[self._lines_printed_to_scrollback:lines_for_scrollback]
|
|
118
|
+
for line in new_lines_to_print:
|
|
119
|
+
self._live.console.print(rich.text.Text.from_ansi(line))
|
|
120
|
+
self._lines_printed_to_scrollback = lines_for_scrollback
|
|
121
|
+
|
|
122
|
+
# Show only the bottom portion in the live window
|
|
123
|
+
visible_lines = all_lines[-available_height:]
|
|
124
|
+
|
|
125
|
+
else:
|
|
126
|
+
visible_lines = all_lines
|
|
127
|
+
|
|
128
|
+
# Update the live display
|
|
129
|
+
self._live.update(rich.text.Text.from_ansi('\n'.join(visible_lines)))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class IncrementalMarkdownLiveStream(MarkdownLiveStream):
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
*,
|
|
136
|
+
parser: ta.Optional['md.MarkdownIt'] = None,
|
|
137
|
+
console: ta.Optional['rich.console.Console'] = None,
|
|
138
|
+
) -> None:
|
|
139
|
+
super().__init__(
|
|
140
|
+
parser=parser,
|
|
141
|
+
console=console,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
self._inc_parser = IncrementalMarkdownParser(parser=self._parser)
|
|
145
|
+
|
|
146
|
+
def feed(self, s: str) -> None:
|
|
147
|
+
ip_out = self._inc_parser.feed2(s)
|
|
148
|
+
|
|
149
|
+
if ip_out.new_stable:
|
|
150
|
+
# try:
|
|
151
|
+
# srs = getattr(self, '_srs')
|
|
152
|
+
# except AttributeError:
|
|
153
|
+
# setattr(self, '_srs', srs := [])
|
|
154
|
+
# from ...markdown.tokens import token_repr, flatten_tokens
|
|
155
|
+
# srs.extend(map(token_repr, flatten_tokens(ip_out.new_stable)))
|
|
156
|
+
|
|
157
|
+
stable_lines = self._console_render(markdown_from_tokens(ip_out.new_stable))
|
|
158
|
+
stable_lines.append(' ') # FIXME: lame hack
|
|
159
|
+
self._live.console.print(rich.text.Text.from_ansi('\n'.join(stable_lines), no_wrap=True))
|
|
160
|
+
self._lines_printed_to_scrollback = max(0, self._lines_printed_to_scrollback - len(stable_lines))
|
|
161
|
+
|
|
162
|
+
unstable_lines = self._console_render(markdown_from_tokens(ip_out.unstable))
|
|
163
|
+
|
|
164
|
+
# Calculate how many lines fit in the live window
|
|
165
|
+
available_height = self._console.height - 2
|
|
166
|
+
|
|
167
|
+
# Determine which lines overflow and need to be printed to scrollback
|
|
168
|
+
total_lines = len(unstable_lines)
|
|
169
|
+
if total_lines > available_height:
|
|
170
|
+
# Lines that should be in scrollback
|
|
171
|
+
lines_for_scrollback = total_lines - available_height
|
|
172
|
+
|
|
173
|
+
# Print any new lines that weren't already printed
|
|
174
|
+
if lines_for_scrollback > self._lines_printed_to_scrollback:
|
|
175
|
+
new_lines_to_print = unstable_lines[self._lines_printed_to_scrollback:lines_for_scrollback]
|
|
176
|
+
self._live.console.print(rich.text.Text.from_ansi('\n'.join(new_lines_to_print)))
|
|
177
|
+
self._lines_printed_to_scrollback = lines_for_scrollback
|
|
178
|
+
|
|
179
|
+
# Show only the bottom portion in the live window
|
|
180
|
+
visible_lines = unstable_lines[-available_height:]
|
|
181
|
+
|
|
182
|
+
else:
|
|
183
|
+
visible_lines = unstable_lines
|
|
184
|
+
|
|
185
|
+
# Update the live display
|
|
186
|
+
self._live.update(rich.text.Text.from_ansi('\n'.join(visible_lines)))
|