diff --git a/README.md b/README.md index 3e8205a..514380c 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,7 @@ class ExampleBackend(Backend): app = Frontend(ExampleBackend) app.run() ``` + +## 预览 + +![](./view.png) diff --git a/main.py b/main.py index 47f23d8..28c38b2 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ import sys +from inspect import cleandoc from asyncio import gather, create_task from loguru import logger @@ -7,8 +8,8 @@ from nonechat.app import Frontend from nonechat.backend import Backend from nonechat.setting import ConsoleSetting -from nonechat.message import Text, ConsoleMessage -from nonechat.info import Event, Robot, MessageEvent +from nonechat.model import Event, Robot, MessageEvent +from nonechat.message import Text, Markdown, ConsoleMessage class ExampleBackend(Backend): @@ -57,16 +58,16 @@ def wrapper(func): app = Frontend( ExampleBackend, ConsoleSetting( - title="Test", - sub_title="This is a test.", - room_title="Room", - icon="🤖", - bg_color=Color(40, 44, 52), + title="Multi-User Chat", + sub_title="支持多用户和频道的聊天应用", + room_title="聊天室", + icon="🤖", # 浅色模式背景色 + dark_bg_color=Color(40, 44, 52), # 暗色模式背景色 (更深一些) title_color=Color(229, 192, 123), header_color=Color(90, 99, 108, 0.6), icon_color=Color.parse("#22b14c"), toolbar_exit="❌", - bot_name="Nonebot", + bot_name="ChatBot", ), ) @@ -77,8 +78,49 @@ async def send_message(message: ConsoleMessage): @app.backend.register() async def on_message(event: MessageEvent): - if str(event.message) == "ping": - await send_message(ConsoleMessage([Text("pong!")])) - + """处理消息事件 - 支持多用户和频道""" + message_text = str(event.message) -app.run() + # 简单的机器人响应逻辑 + if message_text == "ping": + await send_message(ConsoleMessage([Text("pong!")])) + elif message_text == "inspect": + user_name = event.user.nickname + channel_name = event.channel.name + await send_message(ConsoleMessage([Text(f"当前频道: {channel_name}\n当前用户: {user_name}")])) + elif message_text == "help": + help_text = cleandoc( + """ + 🤖 可用命令: + - ping - 测试连接 + - inspect - 查看当前频道和用户 + - help - 显示帮助 + - users - 显示所有用户 + - channels - 显示所有频道 + """ + ) + await send_message(ConsoleMessage([Markdown(help_text)])) + elif message_text == "md": + with open("./README.md", encoding="utf-8") as md_file: + md_text = md_file.read() + await send_message(ConsoleMessage([Markdown(md_text)])) + elif message_text == "users": + users_list = "\n".join([f"{user.avatar} {user.nickname}" for user in app.storage.users]) + await send_message(ConsoleMessage([Text(f"👥 当前用户:\n{users_list}")])) + elif message_text == "channels": + channels_list = "\n".join([f"{channel.emoji} {channel.name}" for channel in app.storage.channels]) + await send_message(ConsoleMessage([Text(f"📺 当前频道:\n{channels_list}")])) + else: + # 在不同频道中有不同的回复 + if app.storage.current_channel: + channel_name = app.storage.current_channel.name + if "技术" in channel_name: + await send_message(ConsoleMessage([Text(f"💻 在{channel_name}中讨论技术话题很有趣!")])) + elif "游戏" in channel_name: + await send_message(ConsoleMessage([Text(f"🎮 {channel_name}中有什么好玩的游戏推荐吗?")])) + else: + await send_message(ConsoleMessage([Text(f"😊 在{channel_name}中收到了你的消息")])) + + +if __name__ == "__main__": + app.run() diff --git a/nonechat/app.py b/nonechat/app.py index 987cd48..b3b9563 100644 --- a/nonechat/app.py +++ b/nonechat/app.py @@ -1,3 +1,4 @@ +import sys import contextlib from datetime import datetime from typing import Any, TextIO, Generic, TypeVar, Optional, cast @@ -7,15 +8,15 @@ from textual.binding import Binding from .backend import Backend -from .storage import Storage from .router import RouterView from .log_redirect import FakeIO from .setting import ConsoleSetting from .views.log_view import LogView from .components.footer import Footer from .components.header import Header +from .storage import Channel, Storage from .message import Text, ConsoleMessage -from .info import User, Event, MessageEvent +from .model import User, Event, MessageEvent from .views.horizontal import HorizontalView TB = TypeVar("TB", bound=Backend) @@ -36,10 +37,17 @@ def __init__(self, backend: type[TB], setting: ConsoleSetting = ConsoleSetting() self.setting = setting self.title = setting.title # type: ignore self.sub_title = setting.sub_title # type: ignore - self.storage = Storage(User("console", setting.user_avatar, setting.user_name)) + + # 创建初始用户 + initial_user = User("console", setting.user_avatar, setting.user_name) + initial_channel = Channel("general", "通用", "默认聊天频道", "💬") + self.storage = Storage(initial_user, initial_channel) + self._fake_output = cast(TextIO, FakeIO(self.storage)) - self._redirect_stdout: Optional[contextlib.redirect_stdout[TextIO]] = None - self._redirect_stderr: Optional[contextlib.redirect_stderr[TextIO]] = None + self._origin_stdout = sys.stdout + self._origin_stderr = sys.stderr + self._textual_stdout: Optional[TextIO] = None + self._textual_stderr: Optional[TextIO] = None self.backend: TB = backend(self) def compose(self): @@ -52,24 +60,23 @@ def on_load(self): def on_mount(self): with contextlib.suppress(Exception): - stdout = contextlib.redirect_stdout(self._fake_output) - stdout.__enter__() - self._redirect_stdout = stdout + self._textual_stdout = sys.stdout + sys.stdout = self._fake_output with contextlib.suppress(Exception): - stderr = contextlib.redirect_stderr(self._fake_output) - stderr.__enter__() - self._redirect_stderr = stderr + self._textual_stderr = sys.stderr + sys.stderr = self._fake_output + + # 应用主题背景色 + self.apply_theme_background() self.backend.on_console_mount() def on_unmount(self): - if self._redirect_stderr is not None: - self._redirect_stderr.__exit__(None, None, None) - self._redirect_stderr = None - if self._redirect_stdout is not None: - self._redirect_stdout.__exit__(None, None, None) - self._redirect_stdout = None + if self._textual_stdout is not None: + sys.stdout = self._origin_stdout + if self._textual_stderr is not None: + sys.stderr = self._origin_stderr self.backend.on_console_unmount() async def call(self, api: str, data: dict[str, Any]): @@ -81,6 +88,7 @@ async def call(self, api: str, data: dict[str, Any]): self_id=self.backend.bot.id, message=data["message"], user=self.backend.bot, + channel=self.storage.current_channel, ) ) elif api == "bell": @@ -97,9 +105,35 @@ async def action_post_message(self, message: str): type="console.message", user=self.storage.current_user, message=ConsoleMessage([Text(message)]), + channel=self.storage.current_channel, ) self.storage.write_chat(msg) await self.backend.post_event(msg) async def action_post_event(self, event: Event): await self.backend.post_event(event) + + def action_toggle_dark(self) -> None: + """切换暗色模式并应用相应背景色""" + # 先调用父类的 toggle_dark 方法 + super().action_toggle_dark() + + # 应用对应的背景色 + self.apply_theme_background() + + def apply_theme_background(self) -> None: + """根据当前主题模式应用背景色设置""" + setting = self.setting + + # 查找需要更新背景色的视图 + try: + horizontal_view = self.query_one(HorizontalView) + if self.current_theme.dark: + horizontal_view.styles.background = setting.dark_bg_color + else: + horizontal_view.styles.background = setting.bg_color + except Exception: + # 视图可能还没有加载 + pass + + # 如果有其他需要设置背景色的组件,可以在这里添加 diff --git a/nonechat/backend.py b/nonechat/backend.py index fb18271..b8ded25 100644 --- a/nonechat/backend.py +++ b/nonechat/backend.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING from abc import ABC, abstractmethod -from .info import Event, Robot +from .model import Event, Robot if TYPE_CHECKING: from .app import Frontend diff --git a/nonechat/components/channel_selector.py b/nonechat/components/channel_selector.py new file mode 100644 index 0000000..77ba2fd --- /dev/null +++ b/nonechat/components/channel_selector.py @@ -0,0 +1,129 @@ +from typing import TYPE_CHECKING, cast + +from textual.widget import Widget +from textual.message import Message +from textual.widgets import Label, Button, ListItem, ListView + +from ..model import Channel + +if TYPE_CHECKING: + from ..app import Frontend + + +class ChannelSelectorPressed(Message): + """频道选择器按钮被按下时发送的消息""" + + def __init__(self, channel: Channel) -> None: + super().__init__() + self.channel = channel + + +class ChannelSelector(Widget): + """频道选择器组件""" + + DEFAULT_CSS = """ + ChannelSelector { + layout: vertical; + height: auto; + width: 100%; + border: round rgba(170, 170, 170, 0.7); + padding: 1; + margin: 1; + } + + ChannelSelector ListView { + height: auto; + width: 100%; + max-height: 85%; + } + + ChannelSelector ListItem { + width: 100%; + margin: 1; + padding: 1; + text-align: center; + } + + ChannelSelector .add-channel-button { + width: 100%; + margin-top: 1; + background: darkgreen; + color: white; + } + """ + + def __init__(self): + super().__init__() + self.channel_items: dict[str, tuple[ListItem, Channel]] = {} + + @property + def app(self) -> "Frontend": + return cast("Frontend", super().app) + + def compose(self): + yield ListView(id="channel-list") + yield Button("➕ 添加频道", classes="add-channel-button", id="add-channel") + + async def on_mount(self): + await self.update_channel_list() + + async def update_channel_list(self): + """更新频道列表""" + channel_list = self.query_one("#channel-list", ListView) + await channel_list.clear() + self.channel_items.clear() + + # 假设从 storage 中获取频道列表 + if hasattr(self.app.storage, "channels"): + for channel in self.app.storage.channels: + label = Label(f"{channel.emoji} {channel.name}") + item = ListItem(label, id=f"channel-{channel.id}") + self.channel_items[channel.id] = (item, channel) + await channel_list.append(item) + + # 标记当前频道 + if ( + hasattr(self.app.storage, "current_channel") + and channel.id == self.app.storage.current_channel.id + ): + item.add_class("current") + else: + item.remove_class("current") + + async def on_button_pressed(self, event: Button.Pressed): + """处理按钮点击事件""" + if event.button.id == "add-channel": + await self._add_new_channel() + + async def on_list_view_selected(self, event: ListView.Selected): + """处理列表项选择事件""" + if event.item and event.item.id and event.item.id.startswith("channel-"): + # 查找对应的频道 + for item, channel in self.channel_items.values(): + if item == event.item: + self.post_message(ChannelSelectorPressed(channel)) + break + + async def _add_new_channel(self): + """添加新频道的逻辑""" + import random + import string + + # 生成随机频道ID + channel_id = "".join(random.choices(string.ascii_letters + string.digits, k=8)) + + # 一些预设的频道 + emojis = ["💬", "📢", "🎮", "🎵", "📚", "💻", "🎨", "🌍", "🔧", "⚡"] + names = ["通用", "公告", "游戏", "音乐", "学习", "技术", "艺术", "世界", "工具", "闪电"] + + new_channel = Channel( + id=channel_id, + name=random.choice(names), + emoji=random.choice(emojis), + description=f"这是一个{random.choice(names)}频道", + ) + + # 假设 storage 有添加频道的方法 + if hasattr(self.app.storage, "add_channel"): + self.app.storage.add_channel(new_channel) + await self.update_channel_list() diff --git a/nonechat/components/chatroom/__init__.py b/nonechat/components/chatroom/__init__.py index 1c6300e..a08b6e1 100644 --- a/nonechat/components/chatroom/__init__.py +++ b/nonechat/components/chatroom/__init__.py @@ -8,7 +8,7 @@ from .history import ChatHistory if TYPE_CHECKING: - from ...app import Frontend + from nonechat.app import Frontend class ChatRoom(Widget): @@ -29,9 +29,10 @@ class ChatRoom(Widget): def __init__(self): super().__init__() self.history = ChatHistory() + self.toolbar = Toolbar() def compose(self): - yield Toolbar(self.app.setting) + yield self.toolbar yield self.history yield InputBox() diff --git a/nonechat/components/chatroom/history.py b/nonechat/components/chatroom/history.py index 3cc59a5..e46a412 100644 --- a/nonechat/components/chatroom/history.py +++ b/nonechat/components/chatroom/history.py @@ -7,9 +7,9 @@ from .message import Timer, Message if TYPE_CHECKING: - from ...app import Frontend - from ...info import MessageEvent - from ...storage import Storage, StateChange + from nonechat.app import Frontend + from nonechat.model import MessageEvent + from nonechat.storage import Storage, StateChange class ChatHistory(Widget): @@ -63,4 +63,15 @@ def action_clear_history(self): self.last_time = None for msg in self.walk_children(): cast(Widget, msg).remove() - self.storage.chat_history.clear() + self.storage.clear_chat_history() + + async def refresh_history(self): + """刷新聊天历史记录显示""" + # 清除当前显示的消息 + self.last_msg = None + self.last_time = None + for msg in self.walk_children(): + cast(Widget, msg).remove() + + # 重新加载当前频道的历史记录 + await self.on_new_message(self.storage.chat_history) diff --git a/nonechat/components/chatroom/input.py b/nonechat/components/chatroom/input.py index 327bf35..7467632 100644 --- a/nonechat/components/chatroom/input.py +++ b/nonechat/components/chatroom/input.py @@ -5,7 +5,7 @@ from textual.binding import Binding if TYPE_CHECKING: - from ...app import Frontend + from nonechat.app import Frontend class InputBox(Widget): diff --git a/nonechat/components/chatroom/message.py b/nonechat/components/chatroom/message.py index f8b1429..9b48f77 100644 --- a/nonechat/components/chatroom/message.py +++ b/nonechat/components/chatroom/message.py @@ -5,8 +5,8 @@ from textual.widgets import Static from rich.console import RenderableType -from ...utils import truncate -from ...info import User, MessageEvent +from nonechat.utils import truncate +from nonechat.model import User, MessageEvent class Timer(Widget): diff --git a/nonechat/components/chatroom/toolbar.py b/nonechat/components/chatroom/toolbar.py index 1be3539..49139f8 100644 --- a/nonechat/components/chatroom/toolbar.py +++ b/nonechat/components/chatroom/toolbar.py @@ -3,13 +3,15 @@ from textual.widget import Widget from textual.widgets import Static +from nonechat.router import RouteChange + from ..action import Action -from ...router import RouteChange if TYPE_CHECKING: + from nonechat.app import Frontend + from nonechat.views.horizontal import HorizontalView + from .history import ChatHistory - from ...setting import ConsoleSetting - from ...views.horizontal import HorizontalView class Toolbar(Widget): @@ -42,21 +44,29 @@ class Toolbar(Widget): } """ - def __init__(self, setting: "ConsoleSetting"): + @property + def app(self) -> "Frontend": + return cast("Frontend", super().app) + + def __init__(self): super().__init__() - self.exit_button = Action(setting.toolbar_exit, id="exit", classes="left") - self.clear_button = Action(setting.toolbar_clear, id="clear", classes="left ml") + setting = self.app.setting + self.toggle_sidebar_button = Action(setting.toolbar_fold, id="toggle-sidebar", classes="left") + self.exit_button = Action(setting.toolbar_exit, id="exit", classes="left ml") + self.center_title = Static(setting.room_title, classes="center") - self.settings_button = Action(setting.toolbar_setting, id="settings", classes="right mr") + # self.settings_button = Action(setting.toolbar_setting, id="settings", classes="right mr") + self.clear_button = Action(setting.toolbar_clear, id="clear", classes="right mr") self.log_button = Action(setting.toolbar_log, id="log", classes="right") def compose(self): yield self.exit_button - yield self.clear_button + yield self.toggle_sidebar_button yield self.center_title - yield self.settings_button + # yield self.settings_button + yield self.clear_button yield self.log_button async def on_action_pressed(self, event: Action.Pressed): @@ -66,8 +76,15 @@ async def on_action_pressed(self, event: Action.Pressed): elif event.action == self.clear_button: history: ChatHistory = cast("ChatHistory", self.app.query_one("ChatHistory")) history.action_clear_history() - elif event.action == self.settings_button: - ... + elif event.action == self.toggle_sidebar_button: + view: HorizontalView = cast("HorizontalView", self.app.query_one("HorizontalView")) + view.action_toggle_sidebar() + if view.show_sidebar: + self.toggle_sidebar_button.update(self.app.setting.toolbar_fold) + else: + self.toggle_sidebar_button.update(self.app.setting.toolbar_expand) + # elif event.action == self.settings_button: + # ... elif event.action == self.log_button: view: HorizontalView = cast("HorizontalView", self.app.query_one("HorizontalView")) if view.can_show_log: diff --git a/nonechat/components/log/__init__.py b/nonechat/components/log/__init__.py index f4fdc9d..07507a1 100644 --- a/nonechat/components/log/__init__.py +++ b/nonechat/components/log/__init__.py @@ -7,9 +7,8 @@ from rich.console import RenderableType if TYPE_CHECKING: - from ...app import Frontend - from ...setting import ConsoleSetting - from ...storage import Storage, StateChange + from nonechat.app import Frontend + from nonechat.storage import Storage, StateChange MAX_LINES = 1000 @@ -27,13 +26,13 @@ class LogPanel(Widget): } """ - def __init__(self, setting: "ConsoleSetting") -> None: + def __init__(self) -> None: super().__init__() self.output = RichLog(max_lines=MAX_LINES, min_width=60, wrap=True, markup=True) - if setting.bg_color: - self.styles.background = setting.bg_color - self.output.styles.background = setting.bg_color + # if setting.bg_color: + # self.styles.background = setting.bg_color + # self.output.styles.background = setting.bg_color @property def storage(self) -> "Storage": diff --git a/nonechat/components/log/toolbar.py b/nonechat/components/log/toolbar.py index 6331ccb..a68f4eb 100644 --- a/nonechat/components/log/toolbar.py +++ b/nonechat/components/log/toolbar.py @@ -1,9 +1,10 @@ from textual.widget import Widget from textual.widgets import Static +from nonechat.router import RouteChange +from nonechat.setting import ConsoleSetting + from ..action import Action -from ...router import RouteChange -from ...setting import ConsoleSetting class Toolbar(Widget): @@ -35,15 +36,15 @@ class Toolbar(Widget): def __init__(self, settings: ConsoleSetting): super().__init__() - self.exit_button = Action(settings.toolbar_exit, id="exit", classes="left") - self.back_button = Action(settings.toolbar_back, id="back", classes="left ml") - self.settings_button = Action(settings.toolbar_setting, id="settings", classes="right") + self.exit_button = Action(settings.toolbar_exit, id="exit", classes="left ml") + self.back_button = Action(settings.toolbar_expand, id="back", classes="left") + # self.settings_button = Action(settings.toolbar_setting, id="settings", classes="right") def compose(self): yield self.exit_button yield self.back_button yield Static("Log", classes="center") - yield self.settings_button + # yield self.settings_button async def on_action_pressed(self, event: Action.Pressed): event.stop() @@ -51,5 +52,5 @@ async def on_action_pressed(self, event: Action.Pressed): self.app.exit() if event.action == self.back_button: self.post_message(RouteChange("main")) - elif event.action == self.settings_button: - ... + # elif event.action == self.settings_button: + # ... diff --git a/nonechat/components/sidebar.py b/nonechat/components/sidebar.py new file mode 100644 index 0000000..0c998f5 --- /dev/null +++ b/nonechat/components/sidebar.py @@ -0,0 +1,93 @@ +from typing import TYPE_CHECKING, cast + +from textual.widget import Widget +from textual.message import Message +from textual.widgets import TabPane, TabbedContent + +from .user_selector import UserSelector, UserSelectorPressed +from .channel_selector import ChannelSelector, ChannelSelectorPressed + +if TYPE_CHECKING: + from ..app import Frontend + + +class SidebarUserChanged(Message): + """侧边栏用户更改消息""" + + def __init__(self, user) -> None: + super().__init__() + self.user = user + + +class SidebarChannelChanged(Message): + """侧边栏频道更改消息""" + + def __init__(self, channel) -> None: + super().__init__() + self.channel = channel + self.direct = channel.id == "_direct" + + +class Sidebar(Widget): + """侧边栏组件,包含用户和频道选择器""" + + DEFAULT_CSS = """ + Sidebar { + width: 25%; + height: 100%; + border-right: solid rgba(170, 170, 170, 0.7); + } + + TabPane { + padding: 0; + } + + UserSelector, ChannelSelector { + margin: 1 0 0 0; + border: none; + max-height: 100%; + } + """ + + def __init__(self): + super().__init__() + self.user_selector = UserSelector() + self.channel_selector = ChannelSelector() + + @property + def app(self) -> "Frontend": + return cast("Frontend", super().app) + + def compose(self): + with TabbedContent(): + with TabPane("👥 用户列表", id="users"): + yield self.user_selector + with TabPane("📺 频道列表", id="channels"): + yield self.channel_selector + + def on_user_selector_pressed(self, event: UserSelectorPressed): + """处理用户选择事件""" + # 更新当前用户 + self.app.storage.set_user(event.user) + + # 更新用���选择器显示 + # self.user_selector.update_user_list() + + # 向父组件发送消息 + self.post_message(SidebarUserChanged(event.user)) + + def on_channel_selector_pressed(self, event: ChannelSelectorPressed): + """处理频道选择事件""" + # 更新当前频道 + self.app.storage.set_channel(event.channel) + + # 更新频道选择器显示 + # self.channel_selector.update_channel_list() + + # 向父组件发送消息 + self.post_message(SidebarChannelChanged(event.channel)) + + async def update_displays(self): + """更新显示""" + await self.user_selector.update_user_list() + await self.channel_selector.update_channel_list() diff --git a/nonechat/components/user_selector.py b/nonechat/components/user_selector.py new file mode 100644 index 0000000..ed74a3b --- /dev/null +++ b/nonechat/components/user_selector.py @@ -0,0 +1,118 @@ +from typing import TYPE_CHECKING, cast + +from textual.widget import Widget +from textual.message import Message +from textual.widgets import Label, Button, ListItem, ListView + +from ..model import User + +if TYPE_CHECKING: + from ..app import Frontend + + +class UserSelectorPressed(Message): + """用户选择器按钮被按下时发送的消息""" + + def __init__(self, user: User) -> None: + super().__init__() + self.user = user + + +class UserSelector(Widget): + """用户选择器组件""" + + DEFAULT_CSS = """ + UserSelector { + layout: vertical; + height: auto; + width: 100%; + border: round rgba(170, 170, 170, 0.7); + padding: 1; + margin: 1; + } + + UserSelector ListView { + height: auto; + width: 100%; + max-height: 85%; + } + + UserSelector ListItem { + width: 100%; + margin: 1; + padding: 1; + text-align: center; + } + + UserSelector .add-user-button { + width: 100%; + margin-top: 1; + background: darkblue; + color: white; + } + """ + + def __init__(self): + super().__init__() + self.user_items: dict[str, tuple[ListItem, User]] = {} + + @property + def app(self) -> "Frontend": + return cast("Frontend", super().app) + + def compose(self): + yield ListView(id="user-list") + yield Button("➕ 添加用户", classes="add-user-button", id="add-user") + + async def on_mount(self): + await self.update_user_list() + + async def update_user_list(self): + """更新用户列表""" + user_list = self.query_one("#user-list", ListView) + await user_list.clear() + self.user_items.clear() + + for user in self.app.storage.users: + label = Label(f"{user.avatar} {user.nickname}") + item = ListItem(label, id=f"user-{user.id}") + self.user_items[user.id] = (item, user) + await user_list.append(item) + + # 标记当前用户 + if user.id == self.app.storage.current_user.id: + item.add_class("current") + else: + item.remove_class("current") + + async def on_button_pressed(self, event: Button.Pressed): + """处理按钮点击事件""" + if event.button.id == "add-user": + await self._add_new_user() + + async def on_list_view_selected(self, event: ListView.Selected): + """处理列表项选择事件""" + if event.item and event.item.id and event.item.id.startswith("user-"): + # 查找对应的用户 + for item, user in self.user_items.values(): + if item == event.item: + self.post_message(UserSelectorPressed(user)) + break + + async def _add_new_user(self): + """添加新用户的逻辑""" + # 先简单实现,稍后再创建对话框 + import random + import string + + # 生成随机用户ID + user_id = "".join(random.choices(string.ascii_letters + string.digits, k=8)) + + # 一些预设的用户 + avatars = ["🟥", "🔴", "🟩", "🟦", "🟨", "🟪", "🟫"] + names = ["用户A", "用户B", "用户C", "Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace"] + + new_user = User(id=user_id, nickname=random.choice(names), avatar=random.choice(avatars)) + + self.app.storage.add_user(new_user) + await self.update_user_list() diff --git a/nonechat/log_redirect.py b/nonechat/log_redirect.py index dfcc4d7..42c8e2a 100644 --- a/nonechat/log_redirect.py +++ b/nonechat/log_redirect.py @@ -33,3 +33,7 @@ def flush(self) -> None: def _write_to_storage(self) -> None: self.storage.write_log(Text.from_ansi("".join(self._buffer), end="", tab_size=4)) + + def read(self) -> str: + self.flush() # 确保所有内容都被写入存储 + return "".join(self._buffer) diff --git a/nonechat/info.py b/nonechat/model.py similarity index 77% rename from nonechat/info.py rename to nonechat/model.py index c2e4321..4f161f3 100644 --- a/nonechat/info.py +++ b/nonechat/model.py @@ -21,12 +21,23 @@ class Robot(User): nickname: str = field(default="Bot") +@dataclass(frozen=True, eq=True) +class Channel: + """频道信息""" + + id: str + name: str + description: str = "" + emoji: str = "💬" + + @dataclass class Event: time: datetime self_id: str type: str user: User + channel: Channel @dataclass diff --git a/nonechat/setting.py b/nonechat/setting.py index 0fe926d..5fb1398 100644 --- a/nonechat/setting.py +++ b/nonechat/setting.py @@ -13,12 +13,14 @@ class ConsoleSetting: icon: Optional[str] = None icon_color: Optional[Color] = None bg_color: Optional[Color] = None + dark_bg_color: Optional[Color] = None header_color: Optional[Color] = None toolbar_exit: str = "⛔" toolbar_clear: str = "🗑️" toolbar_setting: str = "⚙️" toolbar_log: str = "📝" - toolbar_back: str = "⏪" + toolbar_fold: str = "⏪" + toolbar_expand: str = "⏩" user_avatar: str = "👤" user_name: str = "User" bot_avatar: str = "🤖" diff --git a/nonechat/storage/__init__.py b/nonechat/storage/__init__.py index 00fe163..209b624 100644 --- a/nonechat/storage/__init__.py +++ b/nonechat/storage/__init__.py @@ -5,7 +5,7 @@ from textual.message import Message from rich.console import RenderableType -from ..info import User, MessageEvent +from ..model import User, Channel, MessageEvent MAX_LOG_RECORDS = 500 MAX_MSG_RECORDS = 500 @@ -20,18 +20,67 @@ def __init__(self, data: T) -> None: self.data = data +DIRECT = Channel("_direct", "私聊", "私聊频道", "🔏") + + @dataclass class Storage: current_user: User + current_channel: Channel log_history: list[RenderableType] = field(default_factory=list) log_watchers: list[Widget] = field(default_factory=list) - chat_history: list[MessageEvent] = field(default_factory=list) + # 多用户和频道支持 + users: list[User] = field(default_factory=list) + channels: list[Channel] = field(default_factory=list) + + # 按频道分组的聊天历史记录 + chat_history_by_channel: dict[str, list[MessageEvent]] = field(default_factory=dict) + chat_history_by_user: dict[str, list[MessageEvent]] = field(default_factory=dict) chat_watchers: list[Widget] = field(default_factory=list) + def __post_init__(self): + self.channels.append(DIRECT) + if self.current_channel not in self.channels: + self.channels.append(self.current_channel) + + # 添加当前用户到用户列表 + if self.current_user not in self.users: + self.users.append(self.current_user) + + @property + def is_direct(self) -> bool: + return self.current_channel == DIRECT + + @property + def chat_history(self) -> list[MessageEvent]: + """获取当前频道的聊天历史""" + if self.current_channel == DIRECT: + return self.chat_history_by_user.get(self.current_user.id, []) + return self.chat_history_by_channel.get(self.current_channel.id, []) + def set_user(self, user: User): + """切换当前用户""" self.current_user = user + if user not in self.users: + self.users.append(user) + + def set_channel(self, channel: Channel): + """切换当前频道""" + self.current_channel = channel + if channel not in self.channels: + self.channels.append(channel) + + def add_user(self, user: User): + """添加新用户""" + if user not in self.users: + self.users.append(user) + + def add_channel(self, channel: Channel): + """添加新频道""" + if channel not in self.channels: + self.channels.append(channel) def write_log(self, *logs: RenderableType) -> None: self.log_history.extend(logs) @@ -50,11 +99,36 @@ def emit_log_watcher(self, *logs: RenderableType) -> None: watcher.post_message(StateChange(logs)) def write_chat(self, *messages: "MessageEvent") -> None: - self.chat_history.extend(messages) - if len(self.chat_history) > MAX_MSG_RECORDS: - self.chat_history = self.chat_history[-MAX_MSG_RECORDS:] + + if self.current_channel == DIRECT: + if self.current_user.id not in self.chat_history_by_user: + self.chat_history_by_user[self.current_user.id] = [] + # 添加消息到当前用户的私聊历史 + current_history = self.chat_history_by_user[self.current_user.id] + current_history.extend(messages) + + if len(current_history) > MAX_MSG_RECORDS: + self.chat_history_by_user[self.current_user.id] = current_history[-MAX_MSG_RECORDS:] + else: + if self.current_channel.id not in self.chat_history_by_channel: + self.chat_history_by_channel[self.current_channel.id] = [] + # 添加消息到当前频道 + current_history = self.chat_history_by_channel[self.current_channel.id] + current_history.extend(messages) + # 限制历史记录数量 + if len(current_history) > MAX_MSG_RECORDS: + self.chat_history_by_channel[self.current_channel.id] = current_history[-MAX_MSG_RECORDS:] + self.emit_chat_watcher(*messages) + def clear_chat_history(self): + """清空当前频道的聊天历史""" + if self.current_channel == DIRECT: + self.chat_history_by_user[self.current_user.id] = [] + else: + self.chat_history_by_channel[self.current_channel.id] = [] + self.emit_chat_watcher() + def add_chat_watcher(self, watcher: Widget) -> None: self.chat_watchers.append(watcher) diff --git a/nonechat/views/horizontal.py b/nonechat/views/horizontal.py index 7fc6d10..c2dcdfc 100644 --- a/nonechat/views/horizontal.py +++ b/nonechat/views/horizontal.py @@ -6,6 +6,7 @@ from ..components.log import LogPanel from ..components.chatroom import ChatRoom +from ..components.sidebar import Sidebar, SidebarUserChanged, SidebarChannelChanged if TYPE_CHECKING: from ..app import Frontend @@ -21,41 +22,95 @@ class HorizontalView(Widget): width: 100%; } - HorizontalView > * { + HorizontalView > Sidebar { + width: 25%; height: 100%; - width: 100%; + min-width: 20; } - HorizontalView > .-w-50 { - width: 50% !important; + + HorizontalView > ChatRoom { + width: 75%; + height: 100%; } HorizontalView > LogPanel { + width: 25%; + height: 100%; border-left: solid rgba(204, 204, 204, 0.7); + display: none; + } + + HorizontalView.-show-log > ChatRoom { + width: 50%; + } + + HorizontalView.-show-log > Sidebar { + width: 25%; + } + + HorizontalView.-show-log > LogPanel { + width: 25%; + display: block; + } + + HorizontalView.-hide-sidebar > Sidebar { + display: none; + width: 0; + } + + HorizontalView.-hide-sidebar > ChatRoom { + width: 100%; + } + + HorizontalView.-hide-sidebar.-show-log > ChatRoom { + width: 75%; } """ can_show_log: Reactive[bool] = Reactive(False) show_log: Reactive[bool] = Reactive(True) + show_sidebar: Reactive[bool] = Reactive(True) def __init__(self): super().__init__() - setting = self.app.setting + # setting = self.app.setting + self.sidebar = Sidebar() self.chatroom = ChatRoom() - self.log_panel = LogPanel(setting) - if setting.bg_color: - self.styles.background = setting.bg_color + self.log_panel = LogPanel() + # if setting.bg_color: + # self.styles.background = setting.bg_color @property def app(self) -> "Frontend": return cast("Frontend", super().app) def compose(self): + yield self.sidebar yield self.chatroom yield self.log_panel def on_resize(self, event: Resize): self.responsive(event.size.width) + async def on_sidebar_user_changed(self, event: SidebarUserChanged): + """处理用户切换事件""" + # 刷新聊天室显示 + await self.chatroom.history.refresh_history() + + if self.app.storage.is_direct: + self.chatroom.toolbar.center_title.update(self.app.storage.current_user.nickname) + + async def on_sidebar_channel_changed(self, event: SidebarChannelChanged): + """处理频道切换事件""" + # 刷新聊天室显示 + await self.chatroom.history.refresh_history() + + # 更新工具栏标题 + if event.direct: + self.chatroom.toolbar.center_title.update(self.app.storage.current_user.nickname) + else: + self.chatroom.toolbar.center_title.update(event.channel.name) + def watch_can_show_log(self, can_show_log: bool): self._toggle_log_panel() @@ -71,5 +126,19 @@ def action_toggle_log_panel(self): def _toggle_log_panel(self): show = self.can_show_log and self.show_log self.log_panel.display = show - self.chatroom.set_class(show, "-w-50") - self.log_panel.set_class(show, "-w-50") + self.set_class(show, "-show-log") + + def watch_show_sidebar(self, show_sidebar: bool): + """监控侧边栏显示状态变化""" + self._toggle_sidebar() + + def _toggle_sidebar(self): + """切换侧边栏显示状态""" + if self.show_sidebar: + self.remove_class("-hide-sidebar") + else: + self.add_class("-hide-sidebar") + + def action_toggle_sidebar(self): + """触发侧边栏显示/隐藏的动作""" + self.show_sidebar = not self.show_sidebar diff --git a/nonechat/views/log_view.py b/nonechat/views/log_view.py index 4441e9e..ea4e433 100644 --- a/nonechat/views/log_view.py +++ b/nonechat/views/log_view.py @@ -20,13 +20,13 @@ class LogView(Widget): def __init__(self): super().__init__() - setting = self.app.setting - if setting.bg_color: - self.styles.background = setting.bg_color + # setting = self.app.setting + # if setting.bg_color: + # self.styles.background = setting.bg_color def compose(self): yield Toolbar(self.app.setting) - yield LogPanel(self.app.setting) + yield LogPanel() @property def app(self) -> "Frontend": diff --git a/pdm.lock b/pdm.lock index 5fa1f42..80e43f5 100644 --- a/pdm.lock +++ b/pdm.lock @@ -4,13 +4,16 @@ [metadata] groups = ["default", "dev"] strategy = ["cross_platform"] -lock_version = "4.4.1" -content_hash = "sha256:613eaff0a4147a66ae732046d3a00ea92627eff0c5bd0e5520f308705a3334e1" +lock_version = "4.5.0" +content_hash = "sha256:8d9ce48aade457593a220ca61cd659d33e92a63ecb6d1ef83159f15694183333" + +[[metadata.targets]] +requires_python = "~=3.9" [[package]] name = "black" -version = "24.8.0" -requires_python = ">=3.8" +version = "25.1.0" +requires_python = ">=3.9" summary = "The uncompromising code formatter." dependencies = [ "click>=8.0.0", @@ -22,24 +25,28 @@ dependencies = [ "typing-extensions>=4.0.1; python_version < \"3.11\"", ] files = [ - {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, - {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, - {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, - {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, - {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, - {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, - {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, - {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, - {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, - {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, - {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, - {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, - {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, - {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, - {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, - {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, - {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, - {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, + {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, + {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, + {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, + {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, + {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, + {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, + {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, + {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, + {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, + {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, + {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, + {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, + {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, + {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, + {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, + {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, + {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, + {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, + {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, + {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, + {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, + {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, ] [[package]] @@ -59,6 +66,7 @@ requires_python = ">=3.7" summary = "Composable command line interface toolkit" dependencies = [ "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", ] files = [ {file = "click-8.1.5-py3-none-any.whl", hash = "sha256:e576aa487d679441d7d30abb87e1b43d24fc53bffb8758443b1a9e1cee504548"}, @@ -129,16 +137,17 @@ files = [ [[package]] name = "loguru" -version = "0.7.2" -requires_python = ">=3.5" +version = "0.7.3" +requires_python = "<4.0,>=3.5" summary = "Python logging made (stupidly) simple" dependencies = [ + "aiocontextvars>=0.2.0; python_version < \"3.7\"", "colorama>=0.3.4; sys_platform == \"win32\"", "win32-setctime>=1.0.0; sys_platform == \"win32\"", ] files = [ - {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, - {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, + {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, + {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, ] [[package]] @@ -267,6 +276,9 @@ name = "platformdirs" version = "3.8.1" requires_python = ">=3.7" summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +dependencies = [ + "typing-extensions>=4.6.3; python_version < \"3.8\"", +] files = [ {file = "platformdirs-3.8.1-py3-none-any.whl", hash = "sha256:cec7b889196b9144d088e4c57d9ceef7374f6c39694ad1577a0aab50d27ea28c"}, {file = "platformdirs-3.8.1.tar.gz", hash = "sha256:f87ca4fcff7d2b0f81c6a748a77973d7af0f4d526f98f308477c3c436c74d528"}, @@ -274,7 +286,7 @@ files = [ [[package]] name = "pre-commit" -version = "3.8.0" +version = "4.2.0" requires_python = ">=3.9" summary = "A framework for managing and maintaining multi-language pre-commit hooks." dependencies = [ @@ -285,8 +297,8 @@ dependencies = [ "virtualenv>=20.10.0", ] files = [ - {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, - {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, + {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, + {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, ] [[package]] @@ -350,6 +362,7 @@ summary = "Render rich text, tables, progress bars, syntax highlighting, markdow dependencies = [ "markdown-it-py>=2.2.0", "pygments<3.0.0,>=2.13.0", + "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"", ] files = [ {file = "rich-13.4.2-py3-none-any.whl", hash = "sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec"}, @@ -358,28 +371,28 @@ files = [ [[package]] name = "ruff" -version = "0.5.7" +version = "0.12.2" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." files = [ - {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, - {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, - {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, - {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, - {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, - {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, - {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, + {file = "ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be"}, + {file = "ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e"}, + {file = "ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da"}, + {file = "ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce"}, + {file = "ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d"}, + {file = "ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04"}, + {file = "ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342"}, + {file = "ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a"}, + {file = "ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639"}, + {file = "ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12"}, + {file = "ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e"}, ] [[package]] @@ -394,17 +407,18 @@ files = [ [[package]] name = "textual" -version = "0.76.0" +version = "3.6.0" requires_python = "<4.0.0,>=3.8.1" summary = "Modern Text User Interface framework" dependencies = [ "markdown-it-py[linkify,plugins]>=2.1.0", + "platformdirs<5,>=3.6.0", "rich>=13.3.3", "typing-extensions<5.0.0,>=4.4.0", ] files = [ - {file = "textual-0.76.0-py3-none-any.whl", hash = "sha256:e2035609c889dba507d34a5d7b333f1c8c53a29fb170962cb92101507663517a"}, - {file = "textual-0.76.0.tar.gz", hash = "sha256:b12e8879d591090c0901b5cb8121d086e28e677353b368292d3865ec99b83b70"}, + {file = "textual-3.6.0-py3-none-any.whl", hash = "sha256:8b2fafe17b1805fd608092ab2f99b6ddeaff1809f1d5de6820bf5db5b6c51c28"}, + {file = "textual-3.6.0.tar.gz", hash = "sha256:d86b666e34758392a4a1aefc4b781b17d0d001419d24e4d47c277f36c20b1222"}, ] [[package]] @@ -445,6 +459,7 @@ summary = "Virtual Python Environment builder" dependencies = [ "distlib<1,>=0.3.6", "filelock<4,>=3.12", + "importlib-metadata>=6.6; python_version < \"3.8\"", "platformdirs<4,>=3.5.1", ] files = [ @@ -456,6 +471,9 @@ files = [ name = "wcwidth" version = "0.2.6" summary = "Measures the displayed width of unicode strings in a terminal" +dependencies = [ + "backports-functools-lru-cache>=1.2.1; python_version < \"3.2\"", +] files = [ {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, diff --git a/pyproject.toml b/pyproject.toml index 97d2acc..e486dc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,12 +18,12 @@ repository = "https://github.com/nonebot/nonechat" [tool.pdm.dev-dependencies] dev = [ - "isort>=5.13.2", + "isort==5.13.2", "black>=24.4.2", "loguru>=0.7.0", "ruff>=0.5.0", "nonemoji>=0.1", - "pre-commit>=3.7.0" + "pre-commit>=3.7.0", ] [tool.pdm.build] diff --git a/view.png b/view.png new file mode 100644 index 0000000..a80b679 Binary files /dev/null and b/view.png differ