omdev 0.0.0.dev461__tar.gz → 0.0.0.dev463__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.
- {omdev-0.0.0.dev461/omdev.egg-info → omdev-0.0.0.dev463}/PKG-INFO +2 -2
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/.omlish-manifests.json +1 -1
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/cmake.py +4 -1
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/scripts/pyproject.py +6 -0
- omdev-0.0.0.dev463/omdev/tui/apps/irc/__main__.py +4 -0
- omdev-0.0.0.dev463/omdev/tui/apps/irc/app.py +278 -0
- omdev-0.0.0.dev463/omdev/tui/apps/irc/client.py +187 -0
- omdev-0.0.0.dev463/omdev/tui/apps/irc/commands.py +175 -0
- omdev-0.0.0.dev463/omdev/tui/apps/irc/main.py +26 -0
- omdev-0.0.0.dev463/omdev/tui/apps/markdown/__init__.py +0 -0
- omdev-0.0.0.dev463/omdev/tui/textual/__init__.py +210 -0
- omdev-0.0.0.dev463/omdev/tui/textual/app2.py +11 -0
- omdev-0.0.0.dev463/omdev/tui/textual/drivers2.py +55 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463/omdev.egg-info}/PKG-INFO +2 -2
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev.egg-info/SOURCES.txt +9 -1
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev.egg-info/requires.txt +1 -1
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/pyproject.toml +2 -2
- omdev-0.0.0.dev461/omdev/tui/textual/__init__.py +0 -9
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/LICENSE +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/MANIFEST.in +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/README.md +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/__about__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/cli/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/cli/main.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/gen/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/gen/gen.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/gen/imports.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/gen/manifests.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/gen/resources.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/gen/srcfiles.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/gen/strip.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/gen/types.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/gen/typing.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/amalg/sources.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/compute/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/compute/cache.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/compute/contexts.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/compute/currents.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/compute/fns.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/compute/resolvers.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/compute/storage.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/compute/types.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/data/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/data/actions.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/data/cache.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/data/consts.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/data/defaults.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/data/manifests.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cache/data/specs.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/capi/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/capi/darwin/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/capi/darwin/aps.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/capi/darwin/ax.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/capi/darwin/cf.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/capi/darwin/cg.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/capi/linux/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/capi/linux/x11.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cc/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cc/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cc/cdeps.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cc/cdeps.toml +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cc/cli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cc/srclangs.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_boilerplate.cc +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/LICENSE +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/build_ext.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/compilers/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/compilers/ccompiler.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/compilers/options.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/compilers/unixccompiler.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/dir_util.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/errors.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/extension.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/file_util.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/modified.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/spawn.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/sysconfig.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/util.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/_distutils/version.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/build.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/importhook.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/magic.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cexts/scan.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/cache.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/ci.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/cli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/compose.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/consts.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/docker/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/docker/buildcaching.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/docker/cache.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/docker/cacheserved/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/docker/cacheserved/cache.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/docker/cacheserved/manifests.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/docker/cmds.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/docker/dataserver.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/docker/imagepulling.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/docker/inject.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/docker/packing.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/docker/repositories.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/docker/utils.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/api/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/api/clients.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/api/v1/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/api/v1/api.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/api/v1/client.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/api/v2/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/api/v2/api.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/api/v2/azure.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/api/v2/client.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/bootstrap.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/cache.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/cli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/env.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/github/inject.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/inject.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/requirements.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/shell.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/ci/utils.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cli/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cli/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cli/_pathhack.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cli/clicli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cli/install.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cli/main.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cli/managers.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cli/types.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/clipboard/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/clipboard/clipboard.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/clipboard/darwin_cf.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/clipboard/linux_x11.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cmake.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cmdlog/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cmdlog/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cmdlog/_cmdlog.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cmdlog/cli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/cmdlog/cmdlog.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/dataclasses/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/dataclasses/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/dataclasses/cli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/dataclasses/codegen.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/dataserver/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/dataserver/handlers.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/dataserver/http.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/dataserver/routes.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/dataserver/server.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/dataserver/targets.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/git/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/git/magic.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/git/revisions.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/git/shallow.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/git/status.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/home/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/home/paths.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/home/secrets.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/home/shadow.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/imgur.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/intellij/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/intellij/cli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/intellij/ides.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/intellij/open.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/cli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/default.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/inject.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/inspect.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/providers/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/providers/base.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/providers/inject.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/providers/running.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/providers/standalone.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/providers/system.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/pyenv/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/pyenv/inject.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/pyenv/install.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/pyenv/provider.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/pyenv/pyenv.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/resolvers.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/types.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/uv/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/uv/inject.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/uv/provider.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/uv/uv.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/interp/venvs.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/messages/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/messages/base.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/messages/formats.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/messages/messages.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/messages/parsing.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/numerics/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/numerics/formats.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/numerics/numerics.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/numerics/types.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/protocol/LICENSE +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/protocol/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/protocol/consts.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/protocol/errors.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/protocol/message.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/protocol/nuh.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/protocol/parsing.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/protocol/rendering.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/protocol/tags.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/irc/protocol/utils.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/magic/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/magic/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/magic/cli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/magic/find.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/magic/magic.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/magic/prepare.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/magic/styles.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/manifests/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/manifests/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/manifests/_dumping.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/manifests/building.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/manifests/dumping.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/manifests/main.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/markdown/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/markdown/incparse.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/markdown/tokens.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/mypy/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/mypy/debug.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/mypy/report.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/building.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/compression.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/data.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/datarefs.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/dataserver.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/loading.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/media.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/pack/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/pack/packing.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/pack/repositories.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/pack/unpacking.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/repositories.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/oci/tars.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/packaging/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/packaging/marshal.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/packaging/names.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/packaging/requires.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/packaging/revisions.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/packaging/specifiers.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/packaging/versions.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/packaging/wheelfile.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/pip.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/precheck/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/precheck/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/precheck/base.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/precheck/blanklines.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/precheck/caches.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/precheck/git.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/precheck/imports.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/precheck/lite.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/precheck/main.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/precheck/manifests.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/precheck/scripts.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/precheck/unicode.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/attrdocs.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/bracepy.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/classdot.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/docstrings/LICENSE +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/docstrings/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/docstrings/attrdoc.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/docstrings/common.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/docstrings/epydoc.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/docstrings/google.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/docstrings/numpydoc.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/docstrings/parser.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/docstrings/rest.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/findimports.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/scripts/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/scripts/bumpversion.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/scripts/execstat.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/scripts/importtrace.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/srcheaders.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/tools/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/tools/importscan.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/py/tools/mkrelimp.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/pyproject/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/pyproject/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/pyproject/cexts.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/pyproject/cli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/pyproject/configs.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/pyproject/inject.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/pyproject/pkg.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/pyproject/reqs.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/pyproject/resources/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/pyproject/resources/docker-dev.sh +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/pyproject/resources/python.sh +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/pyproject/venvs.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/scripts/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/scripts/ci.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/scripts/interp.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/scripts/lib/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/scripts/lib/inject.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/scripts/lib/logs.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/scripts/lib/marshal.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/scripts/slowcat.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/scripts/tmpexec.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tagstrings.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tokens/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tokens/all.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tokens/tokenizert.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tokens/utils.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/cloc.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/diff.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/doc.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/docker.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/git/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/git/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/git/cli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/git/cloning.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/git/consts.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/git/messages.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/json/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/json/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/json/cli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/json/formats.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/json/io.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/json/parsing.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/json/processing.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/json/rendering.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/jsonview/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/jsonview/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/jsonview/cli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/jsonview/resources/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/jsonview/resources/jsonview.css +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/jsonview/resources/jsonview.js +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/linehisto.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/mkenv.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/notebook.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/pawk/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/pawk/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/pawk/pawk.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/pip.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/prof.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/qr.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/shadow.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/shell.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tools/sqlrepl.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tui/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tui/apps/__init__.py +0 -0
- {omdev-0.0.0.dev461/omdev/tui/apps/markdown → omdev-0.0.0.dev463/omdev/tui/apps/irc}/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tui/apps/markdown/__main__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tui/apps/markdown/cli.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tui/rich/__init__.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tui/rich/console2.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev/tui/rich/markdown2.py +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev.egg-info/dependency_links.txt +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev.egg-info/entry_points.txt +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/omdev.egg-info/top_level.txt +0 -0
- {omdev-0.0.0.dev461 → omdev-0.0.0.dev463}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: omdev
|
|
3
|
-
Version: 0.0.0.
|
|
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.
|
|
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"
|
|
@@ -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)
|
|
@@ -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
|
+
}
|