Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ class ExampleBackend(Backend):
app = Frontend(ExampleBackend)
app.run()
```

## 预览

![](./view.png)
66 changes: 54 additions & 12 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
from inspect import cleandoc
from asyncio import gather, create_task

from loguru import logger
Expand All @@ -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):
Expand Down Expand Up @@ -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",
),
)

Expand All @@ -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()
68 changes: 51 additions & 17 deletions nonechat/app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
import contextlib
from datetime import datetime
from typing import Any, TextIO, Generic, TypeVar, Optional, cast
Expand All @@ -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)
Expand All @@ -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):
Expand All @@ -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]):
Expand All @@ -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":
Expand All @@ -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

# 如果有其他需要设置背景色的组件,可以在这里添加
2 changes: 1 addition & 1 deletion nonechat/backend.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
129 changes: 129 additions & 0 deletions nonechat/components/channel_selector.py
Original file line number Diff line number Diff line change
@@ -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()
5 changes: 3 additions & 2 deletions nonechat/components/chatroom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from .history import ChatHistory

if TYPE_CHECKING:
from ...app import Frontend
from nonechat.app import Frontend


class ChatRoom(Widget):
Expand All @@ -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()

Expand Down
Loading