右侧栏七个工具的文案接入多语言
命令片段、历史命令、Docker、系统服务、进程管理、网络连接、系统监控和工作区里它们的确认、 通知改用 t! / tn!,文案在 locales/<工具>.yml 与 locales/tools.yml。容器操作的对象改成 ContainerSubject,每种操作一整句;详情里的字段存键、按键找行,切换语言时跟着变; 搜索框的占位随界面语言重设。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
70b329066f
commit
5514612524
37 files changed
+2605
-810
No files matched your search
@@ -0,0 +1,253 @@
|
||||
_version: 2
|
||||
|
||||
# Docker: the right sidebar's tool for containers, volumes, images and
|
||||
# networks. Docker's own terms stay as Docker says them.
|
||||
|
||||
docker.tab.containers:
|
||||
zh-CN: 容器
|
||||
en: Containers
|
||||
docker.tab.volumes:
|
||||
zh-CN: 卷
|
||||
en: Volumes
|
||||
docker.tab.images:
|
||||
zh-CN: 镜像
|
||||
en: Images
|
||||
docker.tab.networks:
|
||||
zh-CN: 网络
|
||||
en: Networks
|
||||
|
||||
docker.panel.no_containers:
|
||||
zh-CN: 没有容器
|
||||
en: No containers
|
||||
docker.panel.no_volumes:
|
||||
zh-CN: 没有卷
|
||||
en: No volumes
|
||||
docker.panel.no_images:
|
||||
zh-CN: 没有镜像
|
||||
en: No images
|
||||
docker.panel.no_networks:
|
||||
zh-CN: 没有网络
|
||||
en: No networks
|
||||
docker.panel.standalone:
|
||||
zh-CN: 独立容器
|
||||
en: Standalone containers
|
||||
docker.panel.not_connected:
|
||||
zh-CN: 终端没有连接。连接后这里显示主机上的容器。
|
||||
en: The terminal isn’t connected. Once it is, the host’s containers show here.
|
||||
docker.panel.missing:
|
||||
zh-CN: 这台主机没有安装 Docker。
|
||||
en: Docker isn’t installed on this host.
|
||||
docker.panel.unsupported:
|
||||
zh-CN: 暂不支持这台主机上的 Docker。
|
||||
en: Docker on this host isn’t supported yet.
|
||||
docker.panel.unreachable:
|
||||
zh-CN: 无法读取 Docker:%{error}
|
||||
en: "Couldn’t read Docker: %{error}"
|
||||
docker.project.running:
|
||||
zh-CN: "%{running}/%{total} 运行中"
|
||||
en: "%{running}/%{total} running"
|
||||
|
||||
# What a container is doing, as `docker ps` says.
|
||||
docker.state.running:
|
||||
zh-CN: 运行中
|
||||
en: Running
|
||||
docker.state.paused:
|
||||
zh-CN: 已暂停
|
||||
en: Paused
|
||||
docker.state.restarting:
|
||||
zh-CN: 正在重启
|
||||
en: Restarting
|
||||
docker.state.created:
|
||||
zh-CN: 已创建
|
||||
en: Created
|
||||
docker.state.exited:
|
||||
zh-CN: 已停止
|
||||
en: Exited
|
||||
docker.state.dead:
|
||||
zh-CN: 异常
|
||||
en: Dead
|
||||
docker.state.removing:
|
||||
zh-CN: 正在删除
|
||||
en: Removing
|
||||
|
||||
docker.command.start:
|
||||
zh-CN: 启动
|
||||
en: Start
|
||||
docker.command.stop:
|
||||
zh-CN: 停止
|
||||
en: Stop
|
||||
docker.command.restart:
|
||||
zh-CN: 重启
|
||||
en: Restart
|
||||
|
||||
docker.object.container:
|
||||
zh-CN: 容器
|
||||
en: Container
|
||||
docker.object.image:
|
||||
zh-CN: 镜像
|
||||
en: Image
|
||||
docker.object.volume:
|
||||
zh-CN: 卷
|
||||
en: Volume
|
||||
docker.object.network:
|
||||
zh-CN: 网络
|
||||
en: Network
|
||||
docker.object.in_use:
|
||||
zh-CN: 使用中
|
||||
en: In use
|
||||
docker.object.unused:
|
||||
zh-CN: 未使用
|
||||
en: Unused
|
||||
docker.object.in_use_by_containers:
|
||||
zh-CN: 有容器在用,不能删除
|
||||
en: A container uses it, so it can’t be deleted
|
||||
docker.image.unnamed:
|
||||
zh-CN: 未命名镜像
|
||||
en: Untagged image
|
||||
docker.network.builtin:
|
||||
zh-CN: 内置
|
||||
en: Built-in
|
||||
docker.network.builtin_kept:
|
||||
zh-CN: Docker 自带的网络,不能删除
|
||||
en: Docker’s own network can’t be deleted
|
||||
docker.network.containers.one:
|
||||
en: "%{count} container"
|
||||
docker.network.containers.other:
|
||||
zh-CN: "%{count} 个容器"
|
||||
en: "%{count} containers"
|
||||
docker.network.in_use:
|
||||
zh-CN: 有容器连着,不能删除
|
||||
en: Containers are attached, so it can’t be deleted
|
||||
|
||||
# Docker's complaints, where the user can do something about them.
|
||||
docker.error.permission:
|
||||
zh-CN: 没有权限:以 root 登录、把这个用户加入 docker 组,或给它配置免密码的 sudo
|
||||
en: "Permission denied: log in as root, add this user to the docker group, or give it passwordless sudo"
|
||||
docker.error.not_running:
|
||||
zh-CN: Docker 没有运行
|
||||
en: Docker isn’t running
|
||||
docker.error.status:
|
||||
zh-CN: docker 返回 %{status}
|
||||
en: docker exited with status %{status}
|
||||
|
||||
# The details dialogs.
|
||||
docker.details.tab_details:
|
||||
zh-CN: 详情
|
||||
en: Details
|
||||
docker.details.tab_logs:
|
||||
zh-CN: 日志
|
||||
en: Logs
|
||||
docker.details.no_output:
|
||||
zh-CN: 没有输出
|
||||
en: No output
|
||||
docker.details.container_gone:
|
||||
zh-CN: 读不到这个容器:它可能已经被删除
|
||||
en: Couldn’t read this container. It may have been deleted.
|
||||
docker.details.image_gone:
|
||||
zh-CN: 读不到这个镜像:它可能已经被删除
|
||||
en: Couldn’t read this image. It may have been deleted.
|
||||
docker.details.volume_gone:
|
||||
zh-CN: 读不到这个卷:它可能已经被删除
|
||||
en: Couldn’t read this volume. It may have been deleted.
|
||||
docker.details.network_gone:
|
||||
zh-CN: 读不到这个网络:它可能已经被删除
|
||||
en: Couldn’t read this network. It may have been deleted.
|
||||
|
||||
docker.section.basics:
|
||||
zh-CN: 基础信息
|
||||
en: Overview
|
||||
docker.section.ports:
|
||||
zh-CN: 端口映射
|
||||
en: Ports
|
||||
docker.section.mounts:
|
||||
zh-CN: 挂载
|
||||
en: Mounts
|
||||
docker.section.environment:
|
||||
zh-CN: 环境变量
|
||||
en: Environment
|
||||
docker.section.labels:
|
||||
zh-CN: 标签
|
||||
en: Labels
|
||||
docker.section.users:
|
||||
zh-CN: 使用它的容器
|
||||
en: Used by
|
||||
docker.section.options:
|
||||
zh-CN: 选项
|
||||
en: Options
|
||||
docker.section.config:
|
||||
zh-CN: 运行配置
|
||||
en: Configuration
|
||||
docker.section.subnets:
|
||||
zh-CN: 子网
|
||||
en: Subnets
|
||||
docker.section.containers:
|
||||
zh-CN: 容器
|
||||
en: Containers
|
||||
|
||||
docker.field.name:
|
||||
zh-CN: 名称
|
||||
en: Name
|
||||
docker.field.image:
|
||||
zh-CN: 镜像
|
||||
en: Image
|
||||
docker.field.created:
|
||||
zh-CN: 创建时间
|
||||
en: Created
|
||||
docker.field.entrypoint:
|
||||
zh-CN: 入口
|
||||
en: Entrypoint
|
||||
docker.field.command:
|
||||
zh-CN: 命令
|
||||
en: Command
|
||||
docker.field.driver:
|
||||
zh-CN: 驱动
|
||||
en: Driver
|
||||
docker.field.mountpoint:
|
||||
zh-CN: 挂载点
|
||||
en: Mountpoint
|
||||
docker.field.scope:
|
||||
zh-CN: 范围
|
||||
en: Scope
|
||||
docker.field.tags:
|
||||
zh-CN: 标签
|
||||
en: Tags
|
||||
docker.field.digests:
|
||||
zh-CN: 摘要
|
||||
en: Digests
|
||||
docker.field.size:
|
||||
zh-CN: 大小
|
||||
en: Size
|
||||
docker.field.platform:
|
||||
zh-CN: 平台
|
||||
en: Platform
|
||||
docker.field.layers:
|
||||
zh-CN: 层数
|
||||
en: Layers
|
||||
docker.field.author:
|
||||
zh-CN: 作者
|
||||
en: Author
|
||||
docker.field.working_dir:
|
||||
zh-CN: 工作目录
|
||||
en: Working directory
|
||||
docker.field.user:
|
||||
zh-CN: 用户
|
||||
en: User
|
||||
docker.field.exposed_ports:
|
||||
zh-CN: 暴露端口
|
||||
en: Exposed ports
|
||||
docker.field.internal:
|
||||
zh-CN: 内部网络
|
||||
en: Internal
|
||||
|
||||
docker.value.yes:
|
||||
zh-CN: 是
|
||||
en: "Yes"
|
||||
docker.value.no:
|
||||
zh-CN: 否
|
||||
en: "No"
|
||||
docker.mount.read_only:
|
||||
zh-CN: "%{path}(只读)"
|
||||
en: "%{path} (read-only)"
|
||||
docker.network.gateway:
|
||||
zh-CN: 网关 %{address}
|
||||
en: Gateway %{address}
|
||||
@@ -0,0 +1,61 @@
|
||||
_version: 2
|
||||
|
||||
# 历史命令: the commands bash kept in the host's ~/.bash_history.
|
||||
|
||||
history.panel.search:
|
||||
zh-CN: 搜索命令
|
||||
en: Search commands
|
||||
history.panel.count.one:
|
||||
en: "%{count} command"
|
||||
history.panel.count.other:
|
||||
zh-CN: 共 %{count} 条
|
||||
en: "%{count} commands"
|
||||
history.panel.truncated:
|
||||
zh-CN: 历史文件太长,只读了最后 %{size} KB。
|
||||
en: The history file is long, so only its last %{size} KB were read.
|
||||
history.panel.note:
|
||||
zh-CN: 这个终端里执行的命令,bash 退出后才会出现在这里。
|
||||
en: Commands run in this terminal show up here once bash exits.
|
||||
history.panel.empty:
|
||||
zh-CN: 没有历史命令
|
||||
en: No history
|
||||
history.panel.no_match:
|
||||
zh-CN: 没有符合条件的命令
|
||||
en: No matching commands
|
||||
history.panel.not_connected:
|
||||
zh-CN: 终端没有连接。连接后这里显示主机上 bash 的历史命令。
|
||||
en: The terminal isn’t connected. Once it is, the host’s bash history shows here.
|
||||
history.panel.missing:
|
||||
zh-CN: 这台主机上还没有 bash 的历史命令(~/.bash_history)。bash 退出时才会写入它。
|
||||
en: This host has no bash history yet (~/.bash_history). bash writes it when it exits.
|
||||
history.panel.unsupported:
|
||||
zh-CN: 暂不支持读取这台主机的历史命令,只支持 bash 的 ~/.bash_history。
|
||||
en: This host’s history can’t be read yet. Only bash’s ~/.bash_history is supported.
|
||||
history.menu.copy:
|
||||
zh-CN: 复制
|
||||
en: Copy
|
||||
|
||||
# Under each command: when it last ran, and how often.
|
||||
history.entry.runs.one:
|
||||
en: Ran once
|
||||
history.entry.runs.other:
|
||||
zh-CN: 执行 %{count} 次
|
||||
en: Ran %{count} times
|
||||
history.when.just_now:
|
||||
zh-CN: 刚刚
|
||||
en: Just now
|
||||
history.when.minutes.one:
|
||||
en: "%{count} minute ago"
|
||||
history.when.minutes.other:
|
||||
zh-CN: "%{count} 分钟前"
|
||||
en: "%{count} minutes ago"
|
||||
history.when.hours.one:
|
||||
en: "%{count} hour ago"
|
||||
history.when.hours.other:
|
||||
zh-CN: "%{count} 小时前"
|
||||
en: "%{count} hours ago"
|
||||
history.when.days.one:
|
||||
en: "%{count} day ago"
|
||||
history.when.days.other:
|
||||
zh-CN: "%{count} 天前"
|
||||
en: "%{count} days ago"
|
||||
@@ -0,0 +1,92 @@
|
||||
_version: 2
|
||||
|
||||
# 系统监控: a Linux host's CPU, memory, network and disks.
|
||||
|
||||
monitor.panel.not_connected:
|
||||
zh-CN: 终端没有连接。连接后这里显示主机的 CPU、内存、网络和磁盘。
|
||||
en: The terminal isn’t connected. Once it is, the host’s CPU, memory, network and disks show here.
|
||||
monitor.panel.unsupported_system:
|
||||
zh-CN: 暂不支持 %{system} 的监控,目前只支持 Linux 主机。
|
||||
en: Monitoring %{system} isn’t supported yet. Only Linux hosts are supported.
|
||||
monitor.panel.unsupported:
|
||||
zh-CN: 暂不支持这台主机的监控,目前只支持 Linux 主机。
|
||||
en: Monitoring this host isn’t supported yet. Only Linux hosts are supported.
|
||||
|
||||
monitor.system.title:
|
||||
zh-CN: 系统
|
||||
en: System
|
||||
monitor.system.host:
|
||||
zh-CN: 主机
|
||||
en: Host
|
||||
monitor.system.arch:
|
||||
zh-CN: 架构
|
||||
en: Architecture
|
||||
monitor.system.os:
|
||||
zh-CN: 系统
|
||||
en: OS
|
||||
monitor.system.uptime:
|
||||
zh-CN: 运行时长
|
||||
en: Uptime
|
||||
|
||||
monitor.cpu.cores.one:
|
||||
en: "%{count} core"
|
||||
monitor.cpu.cores.other:
|
||||
zh-CN: "%{count} 核"
|
||||
en: "%{count} cores"
|
||||
monitor.cpu.unfold_cores:
|
||||
zh-CN: 显示更多核的占用
|
||||
en: Show every core
|
||||
monitor.cpu.fold_cores:
|
||||
zh-CN: 收起更多核的占用
|
||||
en: Show fewer cores
|
||||
monitor.cpu.average:
|
||||
zh-CN: 平均使用率
|
||||
en: Average
|
||||
# 「核 0:100.0%,核 1:33.3%」: each core, and the list they make.
|
||||
monitor.cpu.core:
|
||||
zh-CN: 核 %{index}:%{load}
|
||||
en: "Core %{index}: %{load}"
|
||||
monitor.cpu.core_list:
|
||||
zh-CN: "%{list},%{core}"
|
||||
en: "%{list}, %{core}"
|
||||
|
||||
monitor.memory.title:
|
||||
zh-CN: 内存
|
||||
en: Memory
|
||||
monitor.memory.physical:
|
||||
zh-CN: 物理内存
|
||||
en: Physical memory
|
||||
monitor.memory.swap:
|
||||
zh-CN: 交换空间
|
||||
en: Swap
|
||||
|
||||
monitor.network.title:
|
||||
zh-CN: 网络
|
||||
en: Network
|
||||
monitor.network.interfaces.one:
|
||||
en: "%{count} interface"
|
||||
monitor.network.interfaces.other:
|
||||
zh-CN: "%{count} 个网卡"
|
||||
en: "%{count} interfaces"
|
||||
monitor.network.unfold:
|
||||
zh-CN: 显示全部网卡
|
||||
en: Show all interfaces
|
||||
monitor.network.fold:
|
||||
zh-CN: 只显示主网卡
|
||||
en: Show only the main interface
|
||||
monitor.network.none:
|
||||
zh-CN: 没有在用的网卡
|
||||
en: No interfaces in use
|
||||
monitor.network.label:
|
||||
zh-CN: 上传 %{up},下载 %{down}
|
||||
en: Up %{up}, down %{down}
|
||||
|
||||
monitor.disks.title:
|
||||
zh-CN: 磁盘
|
||||
en: Disks
|
||||
monitor.disks.none:
|
||||
zh-CN: 没有读到磁盘
|
||||
en: No disks found
|
||||
monitor.disks.label:
|
||||
zh-CN: "%{percent},%{used} / %{total}"
|
||||
en: "%{percent}, %{used} / %{total}"
|
||||
@@ -0,0 +1,100 @@
|
||||
_version: 2
|
||||
|
||||
# 网络连接: a Linux host's sockets, as `ss` lists them.
|
||||
|
||||
netstat.panel.search:
|
||||
zh-CN: 搜索地址、端口、状态、PID 或进程
|
||||
en: Search addresses, ports, states, PIDs and processes
|
||||
netstat.panel.truncated:
|
||||
zh-CN: 连接太多,只列出了前 %{limit} 条。
|
||||
en: There are too many sockets, so only the first %{limit} are listed.
|
||||
netstat.panel.own_processes:
|
||||
zh-CN: 当前用户不是 root,只能看到自己的进程。
|
||||
en: You’re not logged in as root, so only your own processes show.
|
||||
netstat.panel.no_ss:
|
||||
zh-CN: 这台主机没有 ss 命令,看不到占用连接的进程。
|
||||
en: This host has no ss command, so the processes behind the sockets don’t show.
|
||||
netstat.panel.protocol:
|
||||
zh-CN: 协议
|
||||
en: Protocol
|
||||
netstat.panel.state:
|
||||
zh-CN: 状态
|
||||
en: State
|
||||
netstat.panel.matches.one:
|
||||
en: "%{count} match"
|
||||
netstat.panel.matches.other:
|
||||
zh-CN: 筛选出 %{count} 条
|
||||
en: "%{count} matches"
|
||||
netstat.panel.empty:
|
||||
zh-CN: 没有网络连接
|
||||
en: No sockets
|
||||
netstat.panel.no_match:
|
||||
zh-CN: 没有符合条件的连接
|
||||
en: No matching sockets
|
||||
netstat.panel.not_connected:
|
||||
zh-CN: 终端没有连接。连接后这里显示主机的网络连接。
|
||||
en: The terminal isn’t connected. Once it is, the host’s sockets show here.
|
||||
netstat.panel.unsupported_system:
|
||||
zh-CN: 暂不支持查看 %{system} 的网络连接,目前只支持 Linux 主机。
|
||||
en: Sockets on %{system} can’t be listed yet. Only Linux hosts are supported.
|
||||
netstat.panel.unsupported:
|
||||
zh-CN: 暂不支持查看这台主机的网络连接,目前只支持 Linux 主机。
|
||||
en: This host’s sockets can’t be listed yet. Only Linux hosts are supported.
|
||||
|
||||
# 「共 96 条 · 15 个监听端口 · 21 条已连接」.
|
||||
netstat.summary.total.one:
|
||||
en: "%{count} socket"
|
||||
netstat.summary.total.other:
|
||||
zh-CN: 共 %{count} 条
|
||||
en: "%{count} sockets"
|
||||
netstat.summary.listening.one:
|
||||
en: "%{count} listening port"
|
||||
netstat.summary.listening.other:
|
||||
zh-CN: "%{count} 个监听端口"
|
||||
en: "%{count} listening ports"
|
||||
netstat.summary.connected.other:
|
||||
zh-CN: "%{count} 条已连接"
|
||||
en: "%{count} established"
|
||||
|
||||
netstat.filter.all_protocols:
|
||||
zh-CN: 全部协议
|
||||
en: All protocols
|
||||
netstat.filter.all_states:
|
||||
zh-CN: 全部状态
|
||||
en: All states
|
||||
netstat.filter.other_states:
|
||||
zh-CN: 其他状态
|
||||
en: Other states
|
||||
|
||||
# The two states said in words; the rest keep their TCP names.
|
||||
netstat.state.listen:
|
||||
zh-CN: 监听
|
||||
en: Listening
|
||||
netstat.state.established:
|
||||
zh-CN: 已连接
|
||||
en: Established
|
||||
netstat.role.listening:
|
||||
zh-CN: 监听端口
|
||||
en: Listening port
|
||||
netstat.role.inbound:
|
||||
zh-CN: 入站连接
|
||||
en: Inbound
|
||||
netstat.role.outbound:
|
||||
zh-CN: 出站连接
|
||||
en: Outbound
|
||||
|
||||
netstat.card.label:
|
||||
zh-CN: "%{protocol} %{state} · 本地 %{local} · 远端 %{peer} · %{process} · %{user}"
|
||||
en: "%{protocol} %{state} · Local %{local} · Remote %{peer} · %{process} · %{user}"
|
||||
netstat.card.local:
|
||||
zh-CN: 本地
|
||||
en: Local
|
||||
netstat.card.peer:
|
||||
zh-CN: 远端
|
||||
en: Remote
|
||||
netstat.card.process:
|
||||
zh-CN: 进程
|
||||
en: Process
|
||||
netstat.process.several:
|
||||
zh-CN: "%{name} (PID %{pid} 等 %{count} 个)"
|
||||
en: "%{name} (PID %{pid} and others, %{count} in all)"
|
||||
@@ -0,0 +1,166 @@
|
||||
_version: 2
|
||||
|
||||
# 进程管理: a Linux host's processes.
|
||||
|
||||
processes.panel.search:
|
||||
zh-CN: 搜索进程名称、PID 或用户
|
||||
en: Search names, PIDs and users
|
||||
processes.panel.summary.one:
|
||||
en: "%{count} process · refreshed every %{seconds} s"
|
||||
processes.panel.summary.other:
|
||||
zh-CN: 共 %{count} 个进程 · 每 %{seconds} 秒刷新
|
||||
en: "%{count} processes · refreshed every %{seconds} s"
|
||||
processes.panel.truncated:
|
||||
zh-CN: 进程太多,只列出了前 %{limit} 个。
|
||||
en: There are too many processes, so only the first %{limit} are listed.
|
||||
processes.panel.empty:
|
||||
zh-CN: 没有进程
|
||||
en: No processes
|
||||
processes.panel.no_match:
|
||||
zh-CN: 没有符合条件的进程
|
||||
en: No matching processes
|
||||
processes.panel.not_connected:
|
||||
zh-CN: 终端没有连接。连接后这里显示主机的进程。
|
||||
en: The terminal isn’t connected. Once it is, the host’s processes show here.
|
||||
processes.panel.unsupported_system:
|
||||
zh-CN: 暂不支持管理 %{system} 的进程,目前只支持 Linux 主机。
|
||||
en: Processes on %{system} can’t be managed yet. Only Linux hosts are supported.
|
||||
processes.panel.unsupported:
|
||||
zh-CN: 暂不支持管理这台主机的进程,目前只支持 Linux 主机。
|
||||
en: This host’s processes can’t be managed yet. Only Linux hosts are supported.
|
||||
processes.memory:
|
||||
zh-CN: 内存
|
||||
en: Memory
|
||||
processes.card.label:
|
||||
zh-CN: "%{name} · PID %{pid} · %{state} · %{user} · 内存 %{memory} · CPU %{cpu}"
|
||||
en: "%{name} · PID %{pid} · %{state} · %{user} · Memory %{memory} · CPU %{cpu}"
|
||||
|
||||
processes.sort.memory_descending:
|
||||
zh-CN: 按内存从多到少排列
|
||||
en: Sort by memory, most first
|
||||
processes.sort.memory_ascending:
|
||||
zh-CN: 改为按内存从少到多排列
|
||||
en: Sort by memory, least first
|
||||
processes.sort.cpu_descending:
|
||||
zh-CN: 按CPU从多到少排列
|
||||
en: Sort by CPU, most first
|
||||
processes.sort.cpu_ascending:
|
||||
zh-CN: 改为按CPU从少到多排列
|
||||
en: Sort by CPU, least first
|
||||
|
||||
processes.menu.end:
|
||||
zh-CN: 结束进程…
|
||||
en: End process…
|
||||
processes.menu.kill:
|
||||
zh-CN: 强制结束进程…
|
||||
en: Kill process…
|
||||
|
||||
# What a process is doing: the letter in /proc/<pid>/stat.
|
||||
processes.state.running:
|
||||
zh-CN: 运行中
|
||||
en: Running
|
||||
processes.state.sleeping:
|
||||
zh-CN: 休眠
|
||||
en: Sleeping
|
||||
processes.state.uninterruptible:
|
||||
zh-CN: 等待 I/O
|
||||
en: Waiting for I/O
|
||||
processes.state.stopped:
|
||||
zh-CN: 已暂停
|
||||
en: Stopped
|
||||
processes.state.zombie:
|
||||
zh-CN: 僵尸进程
|
||||
en: Zombie
|
||||
processes.state.idle:
|
||||
zh-CN: 空闲
|
||||
en: Idle
|
||||
processes.state.misc:
|
||||
zh-CN: 其他
|
||||
en: Other
|
||||
|
||||
processes.error.not_permitted:
|
||||
zh-CN: 没有权限:只能结束自己的进程,root 才能结束所有进程
|
||||
en: "Permission denied: only root can end another user’s process"
|
||||
processes.error.gone:
|
||||
zh-CN: 进程已经退出
|
||||
en: The process has already exited
|
||||
processes.error.status:
|
||||
zh-CN: kill 返回 %{status}
|
||||
en: kill exited with status %{status}
|
||||
|
||||
# A process's details.
|
||||
processes.details.info:
|
||||
zh-CN: 进程信息
|
||||
en: Overview
|
||||
processes.details.command:
|
||||
zh-CN: 完整命令
|
||||
en: Command line
|
||||
processes.details.copy_pid:
|
||||
zh-CN: 复制 PID
|
||||
en: Copy PID
|
||||
processes.details.copy_command:
|
||||
zh-CN: 复制命令
|
||||
en: Copy command
|
||||
processes.details.copied:
|
||||
zh-CN: 已复制
|
||||
en: Copied
|
||||
processes.details.end:
|
||||
zh-CN: 结束进程
|
||||
en: End process
|
||||
processes.details.kill:
|
||||
zh-CN: 强制结束
|
||||
en: Kill
|
||||
processes.details.none:
|
||||
zh-CN: 无
|
||||
en: None
|
||||
processes.details.no_command:
|
||||
zh-CN: 没有命令行:进程已经退出,或是僵尸进程
|
||||
en: No command line. The process has exited, or it’s a zombie.
|
||||
processes.field.pid:
|
||||
zh-CN: PID
|
||||
en: PID
|
||||
processes.field.parent:
|
||||
zh-CN: 父进程
|
||||
en: Parent
|
||||
processes.field.user:
|
||||
zh-CN: 用户
|
||||
en: User
|
||||
processes.field.state:
|
||||
zh-CN: 状态
|
||||
en: State
|
||||
processes.field.started:
|
||||
zh-CN: 启动时间
|
||||
en: Started
|
||||
processes.field.running:
|
||||
zh-CN: 运行时长
|
||||
en: Elapsed
|
||||
processes.field.terminal:
|
||||
zh-CN: 终端
|
||||
en: TTY
|
||||
processes.field.priority:
|
||||
zh-CN: 优先级
|
||||
en: Priority
|
||||
processes.field.nice:
|
||||
zh-CN: Nice
|
||||
en: Nice
|
||||
processes.field.cpu:
|
||||
zh-CN: CPU 使用率
|
||||
en: CPU usage
|
||||
processes.field.cpu_time:
|
||||
zh-CN: 累计 CPU 时间
|
||||
en: CPU time
|
||||
processes.field.memory:
|
||||
zh-CN: 常驻内存
|
||||
en: Memory (RSS)
|
||||
processes.field.virtual_memory:
|
||||
zh-CN: 虚拟内存
|
||||
en: Virtual memory
|
||||
processes.field.threads:
|
||||
zh-CN: 线程
|
||||
en: Threads
|
||||
processes.field.children:
|
||||
zh-CN: 直接子进程
|
||||
en: Children
|
||||
processes.field.descendants:
|
||||
zh-CN: 全部后代进程
|
||||
en: Descendants
|
||||
@@ -0,0 +1,251 @@
|
||||
_version: 2
|
||||
|
||||
# 系统服务: a Linux host's systemd services. systemd's states keep the
|
||||
# words systemd uses.
|
||||
|
||||
services.panel.search:
|
||||
zh-CN: 搜索服务名称或描述
|
||||
en: Search names and descriptions
|
||||
services.panel.empty:
|
||||
zh-CN: 没有服务
|
||||
en: No services
|
||||
services.panel.no_match:
|
||||
zh-CN: 没有符合条件的服务
|
||||
en: No matching services
|
||||
services.panel.no_description:
|
||||
zh-CN: 无描述
|
||||
en: No description
|
||||
services.panel.not_connected:
|
||||
zh-CN: 终端没有连接。连接后这里显示主机的系统服务。
|
||||
en: The terminal isn’t connected. Once it is, the host’s services show here.
|
||||
services.panel.no_systemd:
|
||||
zh-CN: 这台主机没有使用 systemd,暂不支持管理它的服务。
|
||||
en: This host doesn’t use systemd, so its services can’t be managed yet.
|
||||
services.panel.unsupported_system:
|
||||
zh-CN: 暂不支持管理 %{system} 的服务,目前只支持使用 systemd 的 Linux 主机。
|
||||
en: Services on %{system} can’t be managed yet. Only Linux hosts with systemd are supported.
|
||||
services.panel.unsupported:
|
||||
zh-CN: 暂不支持管理这台主机的服务,目前只支持使用 systemd 的 Linux 主机。
|
||||
en: This host’s services can’t be managed yet. Only Linux hosts with systemd are supported.
|
||||
services.group.custom:
|
||||
zh-CN: 自定义服务
|
||||
en: Custom services
|
||||
services.group.system:
|
||||
zh-CN: 系统服务
|
||||
en: System services
|
||||
|
||||
services.filter.all:
|
||||
zh-CN: 全部
|
||||
en: All
|
||||
services.filter.running:
|
||||
zh-CN: 运行中
|
||||
en: Active
|
||||
services.filter.stopped:
|
||||
zh-CN: 已停止
|
||||
en: Inactive
|
||||
services.filter.failed:
|
||||
zh-CN: 失败
|
||||
en: Failed
|
||||
|
||||
services.command.start:
|
||||
zh-CN: 启动
|
||||
en: Start
|
||||
services.command.stop:
|
||||
zh-CN: 停止
|
||||
en: Stop
|
||||
services.command.restart:
|
||||
zh-CN: 重启
|
||||
en: Restart
|
||||
services.command.enable:
|
||||
zh-CN: 启用开机启动
|
||||
en: Enable at boot
|
||||
services.command.disable:
|
||||
zh-CN: 禁用开机启动
|
||||
en: Disable at boot
|
||||
|
||||
# systemd's ActiveState.
|
||||
services.active.active:
|
||||
zh-CN: 运行中
|
||||
en: Active
|
||||
services.active.reloading:
|
||||
zh-CN: 正在重载
|
||||
en: Reloading
|
||||
services.active.inactive:
|
||||
zh-CN: 已停止
|
||||
en: Inactive
|
||||
services.active.failed:
|
||||
zh-CN: 失败
|
||||
en: Failed
|
||||
services.active.activating:
|
||||
zh-CN: 正在启动
|
||||
en: Activating
|
||||
services.active.deactivating:
|
||||
zh-CN: 正在停止
|
||||
en: Deactivating
|
||||
|
||||
# systemd's SubState.
|
||||
services.sub.running:
|
||||
zh-CN: 运行中
|
||||
en: Running
|
||||
services.sub.exited:
|
||||
zh-CN: 已退出
|
||||
en: Exited
|
||||
services.sub.dead:
|
||||
zh-CN: 未运行
|
||||
en: Dead
|
||||
services.sub.failed:
|
||||
zh-CN: 失败
|
||||
en: Failed
|
||||
services.sub.waiting:
|
||||
zh-CN: 等待中
|
||||
en: Waiting
|
||||
services.sub.listening:
|
||||
zh-CN: 监听中
|
||||
en: Listening
|
||||
services.sub.reload:
|
||||
zh-CN: 正在重载
|
||||
en: Reloading
|
||||
services.sub.auto_restart:
|
||||
zh-CN: 等待自动重启
|
||||
en: Waiting to restart
|
||||
services.sub.condition:
|
||||
zh-CN: 条件不满足
|
||||
en: Condition not met
|
||||
services.sub.starting:
|
||||
zh-CN: 正在启动
|
||||
en: Starting
|
||||
services.sub.stopping:
|
||||
zh-CN: 正在停止
|
||||
en: Stopping
|
||||
|
||||
# systemd's UnitFileState: whether it starts at boot.
|
||||
services.file.enabled:
|
||||
zh-CN: 已启用
|
||||
en: Enabled
|
||||
services.file.enabled_runtime:
|
||||
zh-CN: 已启用(本次运行)
|
||||
en: Enabled until reboot
|
||||
services.file.disabled:
|
||||
zh-CN: 已禁用
|
||||
en: Disabled
|
||||
services.file.static:
|
||||
zh-CN: 静态
|
||||
en: Static
|
||||
services.file.masked:
|
||||
zh-CN: 已屏蔽
|
||||
en: Masked
|
||||
services.file.generated:
|
||||
zh-CN: 自动生成
|
||||
en: Generated
|
||||
services.file.indirect:
|
||||
zh-CN: 间接启用
|
||||
en: Indirect
|
||||
services.file.linked:
|
||||
zh-CN: 已链接
|
||||
en: Linked
|
||||
services.file.transient:
|
||||
zh-CN: 临时
|
||||
en: Transient
|
||||
services.file.alias:
|
||||
zh-CN: 别名
|
||||
en: Alias
|
||||
services.file.bad:
|
||||
zh-CN: 出错
|
||||
en: Bad
|
||||
|
||||
# systemd's LoadState.
|
||||
services.load.loaded:
|
||||
zh-CN: 已加载
|
||||
en: Loaded
|
||||
services.load.not_found:
|
||||
zh-CN: 未找到
|
||||
en: Not found
|
||||
services.load.masked:
|
||||
zh-CN: 已屏蔽
|
||||
en: Masked
|
||||
services.load.error:
|
||||
zh-CN: 出错
|
||||
en: Error
|
||||
services.load.bad_setting:
|
||||
zh-CN: 配置有误
|
||||
en: Bad setting
|
||||
|
||||
# `systemctl is-system-running`.
|
||||
services.system.running:
|
||||
zh-CN: 运行正常
|
||||
en: Running
|
||||
services.system.degraded:
|
||||
zh-CN: 降级运行
|
||||
en: Degraded
|
||||
services.system.starting:
|
||||
zh-CN: 正在启动
|
||||
en: Starting
|
||||
services.system.initializing:
|
||||
zh-CN: 正在初始化
|
||||
en: Initializing
|
||||
services.system.maintenance:
|
||||
zh-CN: 维护模式
|
||||
en: Maintenance
|
||||
services.system.stopping:
|
||||
zh-CN: 正在关机
|
||||
en: Shutting down
|
||||
services.system.offline:
|
||||
zh-CN: 离线
|
||||
en: Offline
|
||||
|
||||
services.error.needs_root:
|
||||
zh-CN: 需要 root 权限:以 root 登录,或给这个用户配置免密码的 sudo
|
||||
en: "Needs root: log in as root, or give this user passwordless sudo"
|
||||
services.error.needs_root_no_sudo:
|
||||
zh-CN: 需要 root 权限:这台主机没有 sudo,请以 root 登录
|
||||
en: "Needs root: this host has no sudo, so log in as root"
|
||||
services.error.status:
|
||||
zh-CN: systemctl 返回 %{status}
|
||||
en: systemctl exited with status %{status}
|
||||
|
||||
# A service's details.
|
||||
services.details.tab_status:
|
||||
zh-CN: 状态
|
||||
en: Status
|
||||
services.details.tab_journal:
|
||||
zh-CN: 日志
|
||||
en: Journal
|
||||
services.details.unreadable:
|
||||
zh-CN: 读不到这个服务的状态
|
||||
en: Couldn’t read this service’s state
|
||||
services.details.no_journal:
|
||||
zh-CN: 没有日志
|
||||
en: No journal entries
|
||||
services.field.load:
|
||||
zh-CN: 加载状态
|
||||
en: Load state
|
||||
services.field.active:
|
||||
zh-CN: 运行状态
|
||||
en: Active state
|
||||
services.field.boot:
|
||||
zh-CN: 开机启动
|
||||
en: At boot
|
||||
services.field.main_pid:
|
||||
zh-CN: 主进程 PID
|
||||
en: Main PID
|
||||
services.field.memory:
|
||||
zh-CN: 内存
|
||||
en: Memory
|
||||
services.field.tasks:
|
||||
zh-CN: 任务数
|
||||
en: Tasks
|
||||
services.field.restarts:
|
||||
zh-CN: 重启次数
|
||||
en: Restarts
|
||||
services.field.exit_status:
|
||||
zh-CN: 退出状态
|
||||
en: Exit status
|
||||
services.field.started:
|
||||
zh-CN: 启动时间
|
||||
en: Started
|
||||
services.field.stopped:
|
||||
zh-CN: 停止时间
|
||||
en: Stopped
|
||||
services.field.unit_file:
|
||||
zh-CN: Unit 文件
|
||||
en: Unit file
|
||||
@@ -0,0 +1,114 @@
|
||||
_version: 2
|
||||
|
||||
# 命令片段: the commands kept for every host, in categories.
|
||||
|
||||
snippets.uncategorized:
|
||||
zh-CN: 未分类
|
||||
en: Uncategorized
|
||||
|
||||
snippets.panel.search:
|
||||
zh-CN: 搜索名称或命令
|
||||
en: Search names and commands
|
||||
snippets.panel.count.one:
|
||||
en: "%{count} snippet"
|
||||
snippets.panel.count.other:
|
||||
zh-CN: 共 %{count} 个片段
|
||||
en: "%{count} snippets"
|
||||
snippets.panel.categories.one:
|
||||
en: "%{count} category"
|
||||
snippets.panel.categories.other:
|
||||
zh-CN: "%{count} 个分类"
|
||||
en: "%{count} categories"
|
||||
snippets.panel.new_snippet:
|
||||
zh-CN: 新建片段
|
||||
en: New snippet
|
||||
snippets.panel.new_category:
|
||||
zh-CN: 新建分类
|
||||
en: New category
|
||||
snippets.panel.empty:
|
||||
zh-CN: 还没有命令片段
|
||||
en: No snippets yet
|
||||
snippets.panel.empty_description:
|
||||
zh-CN: 把常用的命令存成片段,点一下就输入到当前终端。
|
||||
en: Save the commands you use often as snippets, then type one into the terminal with a click.
|
||||
snippets.panel.no_match:
|
||||
zh-CN: 没有符合条件的片段
|
||||
en: No matching snippets
|
||||
snippets.card.runs_on_click:
|
||||
zh-CN: 点击时自动执行
|
||||
en: Runs on click
|
||||
|
||||
snippets.menu.new_snippet:
|
||||
zh-CN: 新建片段…
|
||||
en: New snippet…
|
||||
snippets.menu.new_category:
|
||||
zh-CN: 新建分类…
|
||||
en: New category…
|
||||
snippets.menu.rename_category:
|
||||
zh-CN: 重命名分类…
|
||||
en: Rename category…
|
||||
snippets.menu.delete_category:
|
||||
zh-CN: 删除分类…
|
||||
en: Delete category…
|
||||
snippets.menu.copy:
|
||||
zh-CN: 复制命令
|
||||
en: Copy command
|
||||
snippets.menu.edit:
|
||||
zh-CN: 编辑…
|
||||
en: Edit…
|
||||
snippets.menu.delete:
|
||||
zh-CN: 删除…
|
||||
en: Delete…
|
||||
|
||||
snippets.dialog.new_title:
|
||||
zh-CN: 新建命令片段
|
||||
en: New snippet
|
||||
snippets.dialog.edit_title:
|
||||
zh-CN: 编辑命令片段
|
||||
en: Edit snippet
|
||||
snippets.dialog.create:
|
||||
zh-CN: 创建
|
||||
en: Create
|
||||
snippets.dialog.name:
|
||||
zh-CN: 名称
|
||||
en: Name
|
||||
snippets.dialog.name_placeholder:
|
||||
zh-CN: 例如 查看容器
|
||||
en: e.g. List containers
|
||||
snippets.dialog.category:
|
||||
zh-CN: 分类
|
||||
en: Category
|
||||
snippets.dialog.command:
|
||||
zh-CN: 命令
|
||||
en: Command
|
||||
snippets.dialog.command_placeholder:
|
||||
zh-CN: 例如 docker ps -a
|
||||
en: e.g. docker ps -a
|
||||
snippets.dialog.command_description:
|
||||
zh-CN: 可以有多行。
|
||||
en: It can span several lines.
|
||||
snippets.dialog.run_on_click:
|
||||
zh-CN: 点击时自动执行
|
||||
en: Run on click
|
||||
snippets.dialog.run_on_click_description:
|
||||
zh-CN: 启用后将自动执行命令,否则只将内容填入终端
|
||||
en: A click runs the command; otherwise it’s only typed into the terminal
|
||||
|
||||
snippets.category.new_title:
|
||||
zh-CN: 新建分类
|
||||
en: New category
|
||||
snippets.category.rename_title:
|
||||
zh-CN: 重命名分类
|
||||
en: Rename category
|
||||
snippets.category.name:
|
||||
zh-CN: 名称
|
||||
en: Name
|
||||
snippets.category.name_placeholder:
|
||||
zh-CN: 例如 Docker
|
||||
en: e.g. Docker
|
||||
snippets.category.name_missing:
|
||||
zh-CN: 请输入分类名称
|
||||
en: Enter a category name
|
||||
snippets.category.name_taken:
|
||||
zh-CN: 已有同名分类
|
||||
en: A category with this name already exists
|
||||
@@ -0,0 +1,251 @@
|
||||
_version: 2
|
||||
|
||||
# The right sidebar: its seven tools, the words they share, and the
|
||||
# workspace's confirmations and notifications for what they run.
|
||||
|
||||
tools.kind.snippets:
|
||||
zh-CN: 命令片段
|
||||
en: Snippets
|
||||
tools.kind.history:
|
||||
zh-CN: 历史命令
|
||||
en: History
|
||||
tools.kind.services:
|
||||
zh-CN: 系统服务
|
||||
en: Services
|
||||
tools.kind.processes:
|
||||
zh-CN: 进程管理
|
||||
en: Processes
|
||||
tools.kind.connections:
|
||||
zh-CN: 网络连接
|
||||
en: Network
|
||||
tools.kind.monitor:
|
||||
zh-CN: 系统监控
|
||||
en: Monitor
|
||||
|
||||
# Shared by the tools.
|
||||
tools.refresh:
|
||||
zh-CN: 刷新
|
||||
en: Refresh
|
||||
tools.reading:
|
||||
zh-CN: 正在读取…
|
||||
en: Loading…
|
||||
tools.read_failed:
|
||||
zh-CN: 读取失败:%{error}
|
||||
en: "Couldn’t load: %{error}"
|
||||
tools.not_connected:
|
||||
zh-CN: 终端没有连接
|
||||
en: The terminal isn’t connected
|
||||
tools.no_answer:
|
||||
zh-CN: 主机没有回答
|
||||
en: The host didn’t answer
|
||||
tools.details_and_logs:
|
||||
zh-CN: 详情和日志
|
||||
en: Details and logs
|
||||
tools.run:
|
||||
zh-CN: 执行
|
||||
en: Run
|
||||
tools.menu.insert:
|
||||
zh-CN: 输入到终端
|
||||
en: Insert into terminal
|
||||
tools.menu.details:
|
||||
zh-CN: 查看详情
|
||||
en: View details
|
||||
|
||||
# A command of 命令片段 or 历史命令 onto the terminal's input line.
|
||||
tools.command.run_failed:
|
||||
zh-CN: 无法执行命令:%{error}
|
||||
en: "Couldn’t run the command: %{error}"
|
||||
tools.command.enter_failed:
|
||||
zh-CN: 无法输入命令:%{error}
|
||||
en: "Couldn’t insert the command: %{error}"
|
||||
tools.command.copied:
|
||||
zh-CN: 已复制命令
|
||||
en: Command copied
|
||||
|
||||
# Docker: a container, or a compose project's containers.
|
||||
tools.container.start.done:
|
||||
zh-CN: 容器“%{name}”已启动
|
||||
en: Container “%{name}” started
|
||||
tools.container.start.failed:
|
||||
zh-CN: 无法启动容器“%{name}”:%{error}
|
||||
en: "Couldn’t start container “%{name}”: %{error}"
|
||||
tools.container.stop.title:
|
||||
zh-CN: 停止容器“%{name}”?
|
||||
en: Stop container “%{name}”?
|
||||
tools.container.stop.description:
|
||||
zh-CN: 停止后它提供的服务就不可用了,直到再次启动。
|
||||
en: What it serves is unavailable until it’s started again.
|
||||
tools.container.stop.done:
|
||||
zh-CN: 容器“%{name}”已停止
|
||||
en: Container “%{name}” stopped
|
||||
tools.container.stop.failed:
|
||||
zh-CN: 无法停止容器“%{name}”:%{error}
|
||||
en: "Couldn’t stop container “%{name}”: %{error}"
|
||||
tools.container.restart.title:
|
||||
zh-CN: 重启容器“%{name}”?
|
||||
en: Restart container “%{name}”?
|
||||
tools.container.restart.description:
|
||||
zh-CN: 会先停止再启动,中间短暂不可用。
|
||||
en: It stops and starts again, so it’s briefly unavailable.
|
||||
tools.container.restart.done:
|
||||
zh-CN: 容器“%{name}”已重启
|
||||
en: Container “%{name}” restarted
|
||||
tools.container.restart.failed:
|
||||
zh-CN: 无法重启容器“%{name}”:%{error}
|
||||
en: "Couldn’t restart container “%{name}”: %{error}"
|
||||
|
||||
tools.project.start.done:
|
||||
zh-CN: 项目“%{name}”已启动
|
||||
en: Project “%{name}” started
|
||||
tools.project.start.failed:
|
||||
zh-CN: 无法启动项目“%{name}”:%{error}
|
||||
en: "Couldn’t start project “%{name}”: %{error}"
|
||||
tools.project.stop.title:
|
||||
zh-CN: 停止项目“%{name}”?
|
||||
en: Stop project “%{name}”?
|
||||
tools.project.stop.description:
|
||||
zh-CN: 停止后它提供的服务就不可用了,直到再次启动。
|
||||
en: What its containers serve is unavailable until they’re started again.
|
||||
tools.project.stop.done:
|
||||
zh-CN: 项目“%{name}”已停止
|
||||
en: Project “%{name}” stopped
|
||||
tools.project.stop.failed:
|
||||
zh-CN: 无法停止项目“%{name}”:%{error}
|
||||
en: "Couldn’t stop project “%{name}”: %{error}"
|
||||
tools.project.restart.title:
|
||||
zh-CN: 重启项目“%{name}”?
|
||||
en: Restart project “%{name}”?
|
||||
tools.project.restart.description:
|
||||
zh-CN: 会先停止再启动,中间短暂不可用。
|
||||
en: Its containers stop and start again, so they’re briefly unavailable.
|
||||
tools.project.restart.done:
|
||||
zh-CN: 项目“%{name}”已重启
|
||||
en: Project “%{name}” restarted
|
||||
tools.project.restart.failed:
|
||||
zh-CN: 无法重启项目“%{name}”:%{error}
|
||||
en: "Couldn’t restart project “%{name}”: %{error}"
|
||||
|
||||
tools.remove.container.title:
|
||||
zh-CN: 删除容器“%{name}”?
|
||||
en: Delete container “%{name}”?
|
||||
tools.remove.container.description:
|
||||
zh-CN: 容器和它里面没有存进卷的数据会一起删除,不能恢复。
|
||||
en: The container is deleted with any data not kept in a volume. This can’t be undone.
|
||||
tools.remove.container.done:
|
||||
zh-CN: 容器“%{name}”已删除
|
||||
en: Container “%{name}” deleted
|
||||
tools.remove.container.failed:
|
||||
zh-CN: 无法删除容器“%{name}”:%{error}
|
||||
en: "Couldn’t delete container “%{name}”: %{error}"
|
||||
tools.remove.image.title:
|
||||
zh-CN: 删除镜像“%{name}”?
|
||||
en: Delete image “%{name}”?
|
||||
tools.remove.image.description:
|
||||
zh-CN: 删除后再要用它,需要重新拉取或构建。
|
||||
en: To use it again, you’ll need to pull or build it again.
|
||||
tools.remove.image.done:
|
||||
zh-CN: 镜像“%{name}”已删除
|
||||
en: Image “%{name}” deleted
|
||||
tools.remove.image.failed:
|
||||
zh-CN: 无法删除镜像“%{name}”:%{error}
|
||||
en: "Couldn’t delete image “%{name}”: %{error}"
|
||||
tools.remove.volume.title:
|
||||
zh-CN: 删除卷“%{name}”?
|
||||
en: Delete volume “%{name}”?
|
||||
tools.remove.volume.description:
|
||||
zh-CN: 卷里的数据会一起删除,不能恢复。
|
||||
en: The data in it is deleted too. This can’t be undone.
|
||||
tools.remove.volume.done:
|
||||
zh-CN: 卷“%{name}”已删除
|
||||
en: Volume “%{name}” deleted
|
||||
tools.remove.volume.failed:
|
||||
zh-CN: 无法删除卷“%{name}”:%{error}
|
||||
en: "Couldn’t delete volume “%{name}”: %{error}"
|
||||
tools.remove.network.title:
|
||||
zh-CN: 删除网络“%{name}”?
|
||||
en: Delete network “%{name}”?
|
||||
tools.remove.network.description:
|
||||
zh-CN: 删除后再要用它,需要重新创建。
|
||||
en: To use it again, you’ll need to create it again.
|
||||
tools.remove.network.done:
|
||||
zh-CN: 网络“%{name}”已删除
|
||||
en: Network “%{name}” deleted
|
||||
tools.remove.network.failed:
|
||||
zh-CN: 无法删除网络“%{name}”:%{error}
|
||||
en: "Couldn’t delete network “%{name}”: %{error}"
|
||||
|
||||
# 系统服务: a systemd service.
|
||||
tools.service.start.done:
|
||||
zh-CN: "%{name} 已启动"
|
||||
en: "%{name} started"
|
||||
tools.service.start.failed:
|
||||
zh-CN: 无法启动 %{name}:%{error}
|
||||
en: "Couldn’t start %{name}: %{error}"
|
||||
tools.service.stop.title:
|
||||
zh-CN: 停止服务“%{name}”?
|
||||
en: Stop service “%{name}”?
|
||||
tools.service.stop.description:
|
||||
zh-CN: 停止后它提供的功能就不可用了,直到再次启动。
|
||||
en: What it provides is unavailable until it’s started again.
|
||||
tools.service.stop.description_ssh:
|
||||
zh-CN: 这是 SSH 服务:停止后新的连接都连不上这台主机,直到它再次启动。
|
||||
en: This is the SSH service. Until it’s started again, no new connection can reach this host.
|
||||
tools.service.stop.done:
|
||||
zh-CN: "%{name} 已停止"
|
||||
en: "%{name} stopped"
|
||||
tools.service.stop.failed:
|
||||
zh-CN: 无法停止 %{name}:%{error}
|
||||
en: "Couldn’t stop %{name}: %{error}"
|
||||
tools.service.restart.title:
|
||||
zh-CN: 重启服务“%{name}”?
|
||||
en: Restart service “%{name}”?
|
||||
tools.service.restart.description:
|
||||
zh-CN: 服务会先停止再启动,中间短暂不可用。
|
||||
en: The service stops and starts again, so it’s briefly unavailable.
|
||||
tools.service.restart.done:
|
||||
zh-CN: "%{name} 已重启"
|
||||
en: "%{name} restarted"
|
||||
tools.service.restart.failed:
|
||||
zh-CN: 无法重启 %{name}:%{error}
|
||||
en: "Couldn’t restart %{name}: %{error}"
|
||||
tools.service.enable.done:
|
||||
zh-CN: "%{name} 已设为开机启动"
|
||||
en: "%{name} now starts at boot"
|
||||
tools.service.enable.failed:
|
||||
zh-CN: 无法启用开机启动 %{name}:%{error}
|
||||
en: "Couldn’t enable %{name} at boot: %{error}"
|
||||
tools.service.disable.done:
|
||||
zh-CN: "%{name} 已取消开机启动"
|
||||
en: "%{name} no longer starts at boot"
|
||||
tools.service.disable.failed:
|
||||
zh-CN: 无法禁用开机启动 %{name}:%{error}
|
||||
en: "Couldn’t disable %{name} at boot: %{error}"
|
||||
|
||||
# 进程管理: ending a process.
|
||||
tools.process.end.title:
|
||||
zh-CN: 结束进程“%{name}”?
|
||||
en: End process “%{name}”?
|
||||
tools.process.end.description:
|
||||
zh-CN: PID %{pid},用户 %{user}。进程会收到结束信号(SIGTERM),可以先做完收尾再退出。
|
||||
en: PID %{pid}, user %{user}. The process gets SIGTERM, so it can clean up before it exits.
|
||||
tools.process.end.confirm:
|
||||
zh-CN: 结束进程
|
||||
en: End process
|
||||
tools.process.kill.title:
|
||||
zh-CN: 强制结束进程“%{name}”?
|
||||
en: Kill process “%{name}”?
|
||||
tools.process.kill.description:
|
||||
zh-CN: PID %{pid},用户 %{user}。进程会立即被终止(SIGKILL),来不及保存数据;只在「结束进程」不起作用时使用。
|
||||
en: PID %{pid}, user %{user}. The process is stopped at once (SIGKILL), with no chance to save its data. Use this only when “End process” doesn’t work.
|
||||
tools.process.kill.confirm:
|
||||
zh-CN: 强制结束
|
||||
en: Kill
|
||||
tools.process.ended:
|
||||
zh-CN: 已向 %{name}(PID %{pid})发送结束信号
|
||||
en: Sent SIGTERM to %{name} (PID %{pid})
|
||||
tools.process.killed:
|
||||
zh-CN: 已强制结束 %{name}(PID %{pid})
|
||||
en: Killed %{name} (PID %{pid})
|
||||
tools.process.end_failed:
|
||||
zh-CN: 无法结束 %{name}(PID %{pid}):%{error}
|
||||
en: "Couldn’t end %{name} (PID %{pid}): %{error}"
|
||||
+2
-2
@@ -5,7 +5,7 @@ use gpui_kit::*;
|
||||
|
||||
use crate::{
|
||||
cli::AgentKind,
|
||||
docker::{ContainerCommand, DockerObject},
|
||||
docker::{ContainerCommand, ContainerSubject, DockerObject},
|
||||
editor::EditorId,
|
||||
explorer::{ExplorerId, FileSizeFormat, PaneSide},
|
||||
host::{
|
||||
@@ -327,7 +327,7 @@ pub struct ControlService {
|
||||
pub struct ControlContainers {
|
||||
/// What they are, for the question and the notification: 「容器“web”」,
|
||||
/// 「项目“php-56”」.
|
||||
pub subject: String,
|
||||
pub subject: ContainerSubject,
|
||||
pub ids: Vec<String>,
|
||||
pub command: ContainerCommand,
|
||||
}
|
||||
|
||||
@@ -15,8 +15,9 @@ use gpui_kit::*;
|
||||
use super::details::{Read, message, reading, render_sections, run};
|
||||
use super::docker_panel::state_color;
|
||||
use super::linux::{inspect_command, logs_command, parse_details};
|
||||
use super::model::{Container, ContainerCommand, ContainerDetails, DockerObject};
|
||||
use super::model::{Container, ContainerCommand, ContainerDetails, ContainerSubject, DockerObject};
|
||||
use crate::app::{CatalogIcon, ControlContainers, RemoveDockerObject};
|
||||
use crate::i18n::t;
|
||||
use crate::shared::soft_tag;
|
||||
use crate::terminal::TerminalView;
|
||||
|
||||
@@ -52,7 +53,7 @@ pub fn open_container_dialog(
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(container.image.clone()),
|
||||
);
|
||||
let subject = format!("容器“{}”", container.name);
|
||||
let subject = ContainerSubject::Container(container.name.clone());
|
||||
let control = |command: ContainerCommand| {
|
||||
let (dispatch, subject, id) = (dispatch.clone(), subject.clone(), container.id.clone());
|
||||
let button = Button::new(SharedString::from(format!(
|
||||
@@ -94,7 +95,7 @@ pub fn open_container_dialog(
|
||||
Button::new("container-dialog-remove")
|
||||
.outline()
|
||||
.icon(Icon::new(CatalogIcon::Trash))
|
||||
.label("删除")
|
||||
.label(t!("common.delete"))
|
||||
.on_click(move |_, window, cx| {
|
||||
window.close_dialog(cx);
|
||||
dispatch.dispatch_action(&action, window, cx);
|
||||
@@ -152,7 +153,7 @@ impl ContainerDetailsView {
|
||||
this.details = match answer {
|
||||
Ok(output) => match parse_details(&output) {
|
||||
Some(details) => Read::Known(details),
|
||||
None => Read::Unknown("读不到这个容器:它可能已经被删除".into()),
|
||||
None => Read::Unknown(t!("docker.details.container_gone").into()),
|
||||
},
|
||||
Err(why) => Read::Unknown(why),
|
||||
};
|
||||
@@ -180,7 +181,9 @@ impl ContainerDetailsView {
|
||||
cx,
|
||||
|this, answer| {
|
||||
this.output = match answer {
|
||||
Ok(output) if output.trim().is_empty() => Read::Unknown("没有输出".into()),
|
||||
Ok(output) if output.trim().is_empty() => {
|
||||
Read::Unknown(t!("docker.details.no_output").into())
|
||||
}
|
||||
Ok(output) => Read::Known(output.trim_end().to_owned()),
|
||||
Err(why) => Read::Unknown(why),
|
||||
};
|
||||
@@ -238,11 +241,15 @@ impl Render for ContainerDetailsView {
|
||||
.small()
|
||||
.selected_index(self.tab)
|
||||
.on_click(cx.listener(|this, index: &usize, _, cx| this.select(*index, cx)))
|
||||
.child(Tab::new().icon(Icon::new(IconName::Info)).label("详情"))
|
||||
.child(
|
||||
Tab::new()
|
||||
.icon(Icon::new(IconName::Info))
|
||||
.label(t!("docker.details.tab_details")),
|
||||
)
|
||||
.child(
|
||||
Tab::new()
|
||||
.icon(Icon::new(CatalogIcon::FileText))
|
||||
.label("日志"),
|
||||
.label(t!("docker.details.tab_logs")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ use gpui_kit::prelude::FluentBuilder as _;
|
||||
use gpui_kit::*;
|
||||
|
||||
use super::model::{DetailSection, SectionBody};
|
||||
use crate::i18n::t;
|
||||
use crate::terminal::{ExecResult, TerminalView, exec_answer};
|
||||
|
||||
/// Something read off the host for a dialog.
|
||||
@@ -29,10 +30,10 @@ pub(super) fn run<V: 'static>(
|
||||
let reply = command.and_then(|command| terminal.upgrade()?.read(cx).exec(command, cx));
|
||||
cx.spawn(async move |this, cx| {
|
||||
let answer: ExecResult = match reply {
|
||||
None => Err("终端没有连接".into()),
|
||||
None => Err(t!("tools.not_connected").into()),
|
||||
Some(reply) => match exec_answer(reply, cx).await {
|
||||
None => Err("终端没有连接".into()),
|
||||
Some(Err(error)) => Err(format!("读取失败:{error}")),
|
||||
None => Err(t!("tools.not_connected").into()),
|
||||
Some(Err(error)) => Err(t!("tools.read_failed", error = error).into()),
|
||||
Some(Ok(output)) => Ok(output),
|
||||
},
|
||||
};
|
||||
@@ -46,7 +47,7 @@ pub(super) fn run<V: 'static>(
|
||||
|
||||
/// The sections one under another, each under its heading; 「—」 for one
|
||||
/// with nothing in it. Their lines are found as 「{prefix}-{section}:{label}」
|
||||
/// (「container-basics:名称」), a text section as 「{prefix}-{section}」.
|
||||
/// (「container-basics:name」), a text section as 「{prefix}-{section}」.
|
||||
pub(super) fn render_sections(
|
||||
prefix: &'static str,
|
||||
sections: &[DetailSection],
|
||||
@@ -59,7 +60,7 @@ pub(super) fn render_sections(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(section.title),
|
||||
.child(t!(section.title)),
|
||||
)
|
||||
.child(render_body(prefix, section, cx))
|
||||
}))
|
||||
@@ -98,7 +99,7 @@ fn render_body(prefix: &'static str, section: &DetailSection, cx: &App) -> AnyEl
|
||||
rows.iter()
|
||||
.map(|(label, value)| {
|
||||
h_flex()
|
||||
.id(line_id(label))
|
||||
.id(line_id(label.id()))
|
||||
.test_support()
|
||||
.aria_label(value.clone())
|
||||
.items_start()
|
||||
@@ -111,7 +112,7 @@ fn render_body(prefix: &'static str, section: &DetailSection, cx: &App) -> AnyEl
|
||||
.w(rems(7.))
|
||||
.flex_shrink_0()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(label.clone()),
|
||||
.child(label.text()),
|
||||
)
|
||||
// Long IDs and names wrap rather than go off the edge.
|
||||
.child(div().flex_1().min_w_0().child(value.clone()))
|
||||
@@ -164,7 +165,7 @@ pub(super) fn reading(id: &'static str, cx: &App) -> AnyElement {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(Spinner::new().small())
|
||||
.child("正在读取…")
|
||||
.child(t!("tools.reading"))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
|
||||
+39
-30
@@ -16,14 +16,15 @@ use gpui_kit::*;
|
||||
|
||||
use super::linux::{self, Parsed};
|
||||
use super::model::{
|
||||
Container, ContainerCommand, ContainerState, DockerObject, DockerRow, DockerTab, DockerTable,
|
||||
ObjectSummary, Project,
|
||||
Container, ContainerCommand, ContainerState, ContainerSubject, DockerObject, DockerRow,
|
||||
DockerTab, DockerTable, ObjectSummary, Project,
|
||||
};
|
||||
use super::object_details::tone_color;
|
||||
use crate::app::{
|
||||
CatalogIcon, ControlContainers, RefreshDocker, RemoveDockerObject, ShowDockerObject,
|
||||
ToggleDockerProject,
|
||||
};
|
||||
use crate::i18n::t;
|
||||
use crate::shared::{count_tabs, soft_tag, tinted};
|
||||
use crate::terminal::{ExecResult, ExecTarget, RemoteTerminalId, exec_answer};
|
||||
|
||||
@@ -275,8 +276,8 @@ impl DockerPanel {
|
||||
.xsmall()
|
||||
.icon(Icon::new(CatalogIcon::RefreshCw))
|
||||
.loading(self.loading)
|
||||
.tooltip("刷新")
|
||||
.accessibility_label("刷新")
|
||||
.tooltip(t!("tools.refresh"))
|
||||
.accessibility_label(t!("tools.refresh"))
|
||||
.on_click(move |_, window, cx| {
|
||||
refresh.dispatch_action(&RefreshDocker, window, cx)
|
||||
}),
|
||||
@@ -299,10 +300,10 @@ impl DockerPanel {
|
||||
));
|
||||
|
||||
let empty = match self.tab {
|
||||
DockerTab::Containers => "没有容器",
|
||||
DockerTab::Volumes => "没有卷",
|
||||
DockerTab::Images => "没有镜像",
|
||||
DockerTab::Networks => "没有网络",
|
||||
DockerTab::Containers => t!("docker.panel.no_containers"),
|
||||
DockerTab::Volumes => t!("docker.panel.no_volumes"),
|
||||
DockerTab::Images => t!("docker.panel.no_images"),
|
||||
DockerTab::Networks => t!("docker.panel.no_networks"),
|
||||
};
|
||||
// The scrollbar goes on the list's box, which does not scroll: on
|
||||
// the scrolled lines it would scroll away with them.
|
||||
@@ -320,7 +321,7 @@ impl DockerPanel {
|
||||
div()
|
||||
.id("docker-empty")
|
||||
.test_support()
|
||||
.aria_label(empty)
|
||||
.aria_label(empty.clone())
|
||||
.py_8()
|
||||
.text_sm()
|
||||
.text_center()
|
||||
@@ -347,7 +348,7 @@ impl DockerPanel {
|
||||
.pt_2()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("独立容器")
|
||||
.child(t!("docker.panel.standalone"))
|
||||
.into_any_element(),
|
||||
DockerRow::Container(index) => render_container(
|
||||
&table.containers()[*index],
|
||||
@@ -430,7 +431,7 @@ impl Render for DockerPanel {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(Spinner::new().small())
|
||||
.child("正在读取…")
|
||||
.child(t!("tools.reading"))
|
||||
.into_any_element(),
|
||||
};
|
||||
v_flex().id("docker").test_support().size_full().child(body)
|
||||
@@ -440,19 +441,16 @@ impl Render for DockerPanel {
|
||||
fn render_problem(problem: &Problem, cx: &App) -> impl IntoElement {
|
||||
let (text, color) = match problem {
|
||||
Problem::NotConnected => (
|
||||
"终端没有连接。连接后这里显示主机上的容器。".to_string(),
|
||||
t!("docker.panel.not_connected"),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Missing => (
|
||||
"这台主机没有安装 Docker。".to_string(),
|
||||
cx.theme().muted_foreground,
|
||||
Problem::Missing => (t!("docker.panel.missing"), cx.theme().muted_foreground),
|
||||
Problem::Unsupported => (t!("docker.panel.unsupported"), cx.theme().muted_foreground),
|
||||
Problem::Unreachable(why) => (
|
||||
t!("docker.panel.unreachable", error = why),
|
||||
cx.theme().danger,
|
||||
),
|
||||
Problem::Unsupported => (
|
||||
"暂不支持这台主机上的 Docker。".to_string(),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Unreachable(why) => (format!("无法读取 Docker:{why}"), cx.theme().danger),
|
||||
Problem::Failed(error) => (format!("读取失败:{error}"), cx.theme().danger),
|
||||
Problem::Failed(error) => (t!("tools.read_failed", error = error), cx.theme().danger),
|
||||
};
|
||||
div()
|
||||
.id("docker-message")
|
||||
@@ -484,7 +482,7 @@ fn dot(color: Hsla) -> Div {
|
||||
/// stop while something in it is up.
|
||||
fn command_buttons(
|
||||
id: &str,
|
||||
subject: String,
|
||||
subject: ContainerSubject,
|
||||
ids: Vec<String>,
|
||||
up: bool,
|
||||
dispatch: &FocusHandle,
|
||||
@@ -563,8 +561,13 @@ fn render_project(
|
||||
)))
|
||||
.test_support()
|
||||
.aria_label(format!(
|
||||
"{} · {}/{total} 运行中",
|
||||
project.name, project.running
|
||||
"{} · {}",
|
||||
project.name,
|
||||
t!(
|
||||
"docker.project.running",
|
||||
running = project.running,
|
||||
total = total
|
||||
)
|
||||
))
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().tokens.group_box)
|
||||
@@ -628,7 +631,7 @@ fn render_project(
|
||||
)
|
||||
.child(command_buttons(
|
||||
&format!("project:{}", project.name),
|
||||
format!("项目“{}”", project.name),
|
||||
ContainerSubject::Project(project.name.clone()),
|
||||
ids,
|
||||
project.running > 0,
|
||||
dispatch,
|
||||
@@ -720,7 +723,7 @@ fn render_container(
|
||||
)
|
||||
.child(command_buttons(
|
||||
&container.name,
|
||||
format!("容器“{}”", container.name),
|
||||
ContainerSubject::Container(container.name.clone()),
|
||||
vec![container.id.clone()],
|
||||
container.state.is_up(),
|
||||
dispatch,
|
||||
@@ -734,8 +737,8 @@ fn render_container(
|
||||
.ghost()
|
||||
.small()
|
||||
.icon(IconName::Ellipsis)
|
||||
.tooltip("详情和日志")
|
||||
.accessibility_label("详情和日志")
|
||||
.tooltip(t!("tools.details_and_logs"))
|
||||
.accessibility_label(t!("tools.details_and_logs"))
|
||||
.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
more.dispatch_action(
|
||||
@@ -831,8 +834,14 @@ fn render_object(
|
||||
.ghost()
|
||||
.small()
|
||||
.icon(Icon::new(CatalogIcon::Trash))
|
||||
.tooltip(summary.removable.err().unwrap_or("删除"))
|
||||
.accessibility_label("删除")
|
||||
.tooltip(
|
||||
summary
|
||||
.removable
|
||||
.clone()
|
||||
.err()
|
||||
.unwrap_or_else(|| t!("common.delete")),
|
||||
)
|
||||
.accessibility_label(t!("common.delete"))
|
||||
.disabled(summary.removable.is_err())
|
||||
.on_click(move |_, window, cx| {
|
||||
// Not the card's click too, which opens the details.
|
||||
|
||||
+138
-61
@@ -7,8 +7,9 @@ use serde_json::Value;
|
||||
|
||||
use super::model::{
|
||||
Container, ContainerCommand, ContainerDetails, ContainerState, DetailSection, DockerObject,
|
||||
DockerTable, Image, Network, SectionBody, Volume, rows,
|
||||
DockerTable, Image, Network, RowLabel, SectionBody, Volume, rows,
|
||||
};
|
||||
use crate::i18n::t;
|
||||
use crate::shared::format_bytes;
|
||||
|
||||
/// Where `docker` may be besides the login's `PATH` (Docker Desktop on a
|
||||
@@ -107,15 +108,15 @@ pub fn logs_command(id: &str) -> Option<String> {
|
||||
))
|
||||
}
|
||||
|
||||
/// Docker's complaint, in Chinese where it is one a user can do something
|
||||
/// about.
|
||||
/// Docker's complaint, in ShellRS's words where it is one a user can do
|
||||
/// something about.
|
||||
fn complaint(said: &str) -> String {
|
||||
if said.contains("password is required")
|
||||
|| (said.contains("permission denied") && said.contains("docker"))
|
||||
{
|
||||
"没有权限:以 root 登录、把这个用户加入 docker 组,或给它配置免密码的 sudo".into()
|
||||
t!("docker.error.permission").into()
|
||||
} else if said.contains("Cannot connect to the Docker daemon") {
|
||||
"Docker 没有运行".into()
|
||||
t!("docker.error.not_running").into()
|
||||
} else {
|
||||
let line = said.lines().next().unwrap_or(said).trim();
|
||||
line.trim_start_matches("Error response from daemon: ")
|
||||
@@ -128,10 +129,10 @@ fn complaint(said: &str) -> String {
|
||||
pub fn done(output: &str) -> Result<(), String> {
|
||||
let (said, status) = output
|
||||
.rsplit_once("@@status")
|
||||
.ok_or_else(|| "主机没有回答".to_string())?;
|
||||
.ok_or_else(|| t!("tools.no_answer").to_string())?;
|
||||
match status.trim() {
|
||||
"0" => Ok(()),
|
||||
status if said.trim().is_empty() => Err(format!("docker 返回 {status}")),
|
||||
status if said.trim().is_empty() => Err(t!("docker.error.status", status = status).into()),
|
||||
_ => Err(complaint(said.trim())),
|
||||
}
|
||||
}
|
||||
@@ -374,13 +375,14 @@ pub fn parse_details(output: &str) -> Option<ContainerDetails> {
|
||||
_ => text(mount, "Source"),
|
||||
};
|
||||
let read_only = mount.get("RW").and_then(Value::as_bool) == Some(false);
|
||||
let destination = text(mount, "Destination");
|
||||
(
|
||||
source.to_owned(),
|
||||
format!(
|
||||
"{}{}",
|
||||
text(mount, "Destination"),
|
||||
if read_only { "(只读)" } else { "" }
|
||||
),
|
||||
if read_only {
|
||||
t!("docker.mount.read_only", path = destination).into()
|
||||
} else {
|
||||
destination.to_owned()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
@@ -462,8 +464,8 @@ fn pairs_at(value: &Value, keys: &[&str]) -> Vec<(String, String)> {
|
||||
|
||||
fn yes_no(value: &Value, keys: &[&str]) -> String {
|
||||
match at(value, keys).and_then(Value::as_bool) {
|
||||
Some(true) => "是".into(),
|
||||
Some(false) => "否".into(),
|
||||
Some(true) => t!("docker.value.yes").into(),
|
||||
Some(false) => t!("docker.value.no").into(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
@@ -473,7 +475,7 @@ fn yes_no(value: &Value, keys: &[&str]) -> String {
|
||||
fn used_by(used_by: &[String]) -> DetailSection {
|
||||
DetailSection {
|
||||
id: "users",
|
||||
title: "使用它的容器",
|
||||
title: "docker.section.users",
|
||||
body: SectionBody::List(used_by.to_vec()),
|
||||
}
|
||||
}
|
||||
@@ -485,24 +487,39 @@ pub fn volume_details(output: &str, users: &[String]) -> Option<Vec<DetailSectio
|
||||
Some(vec![
|
||||
DetailSection {
|
||||
id: "basics",
|
||||
title: "基础信息",
|
||||
title: "docker.section.basics",
|
||||
body: rows([
|
||||
("名称", string_at(&volume, &["Name"])),
|
||||
("驱动", string_at(&volume, &["Driver"])),
|
||||
("挂载点", string_at(&volume, &["Mountpoint"])),
|
||||
("范围", string_at(&volume, &["Scope"])),
|
||||
("创建时间", string_at(&volume, &["CreatedAt"])),
|
||||
(
|
||||
RowLabel::Field("docker.field.name"),
|
||||
string_at(&volume, &["Name"]),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.driver"),
|
||||
string_at(&volume, &["Driver"]),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.mountpoint"),
|
||||
string_at(&volume, &["Mountpoint"]),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.scope"),
|
||||
string_at(&volume, &["Scope"]),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.created"),
|
||||
string_at(&volume, &["CreatedAt"]),
|
||||
),
|
||||
]),
|
||||
},
|
||||
used_by(users),
|
||||
DetailSection {
|
||||
id: "options",
|
||||
title: "选项",
|
||||
title: "docker.section.options",
|
||||
body: rows(pairs_at(&volume, &["Options"])),
|
||||
},
|
||||
DetailSection {
|
||||
id: "labels",
|
||||
title: "标签",
|
||||
title: "docker.section.labels",
|
||||
body: rows(pairs_at(&volume, &["Labels"])),
|
||||
},
|
||||
])
|
||||
@@ -531,51 +548,75 @@ pub fn image_details(output: &str, users: &[String]) -> Option<Vec<DetailSection
|
||||
Some(vec![
|
||||
DetailSection {
|
||||
id: "basics",
|
||||
title: "基础信息",
|
||||
title: "docker.section.basics",
|
||||
body: rows([
|
||||
(
|
||||
"ID",
|
||||
RowLabel::Raw("ID".into()),
|
||||
string_at(&image, &["Id"])
|
||||
.trim_start_matches("sha256:")
|
||||
.to_owned(),
|
||||
),
|
||||
("标签", strings_at(&image, &["RepoTags"]).join("\n")),
|
||||
("摘要", strings_at(&image, &["RepoDigests"]).join("\n")),
|
||||
("创建时间", string_at(&image, &["Created"])),
|
||||
(
|
||||
"大小",
|
||||
RowLabel::Field("docker.field.tags"),
|
||||
strings_at(&image, &["RepoTags"]).join("\n"),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.digests"),
|
||||
strings_at(&image, &["RepoDigests"]).join("\n"),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.created"),
|
||||
string_at(&image, &["Created"]),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.size"),
|
||||
at(&image, &["Size"])
|
||||
.and_then(Value::as_u64)
|
||||
.map_or(String::new(), format_bytes),
|
||||
),
|
||||
("平台", platform),
|
||||
("层数", layers),
|
||||
("作者", string_at(&image, &["Author"])),
|
||||
(RowLabel::Field("docker.field.platform"), platform),
|
||||
(RowLabel::Field("docker.field.layers"), layers),
|
||||
(
|
||||
RowLabel::Field("docker.field.author"),
|
||||
string_at(&image, &["Author"]),
|
||||
),
|
||||
]),
|
||||
},
|
||||
used_by(users),
|
||||
DetailSection {
|
||||
id: "config",
|
||||
title: "运行配置",
|
||||
title: "docker.section.config",
|
||||
body: rows([
|
||||
(
|
||||
"入口",
|
||||
RowLabel::Field("docker.field.entrypoint"),
|
||||
strings_at(&image, &["Config", "Entrypoint"]).join(" "),
|
||||
),
|
||||
("命令", strings_at(&image, &["Config", "Cmd"]).join(" ")),
|
||||
("工作目录", string_at(&image, &["Config", "WorkingDir"])),
|
||||
("用户", string_at(&image, &["Config", "User"])),
|
||||
("暴露端口", ports.join(", ")),
|
||||
(
|
||||
RowLabel::Field("docker.field.command"),
|
||||
strings_at(&image, &["Config", "Cmd"]).join(" "),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.working_dir"),
|
||||
string_at(&image, &["Config", "WorkingDir"]),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.user"),
|
||||
string_at(&image, &["Config", "User"]),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.exposed_ports"),
|
||||
ports.join(", "),
|
||||
),
|
||||
]),
|
||||
},
|
||||
DetailSection {
|
||||
id: "environment",
|
||||
title: "环境变量",
|
||||
title: "docker.section.environment",
|
||||
body: SectionBody::Text(strings_at(&image, &["Config", "Env"])),
|
||||
},
|
||||
DetailSection {
|
||||
id: "labels",
|
||||
title: "标签",
|
||||
title: "docker.section.labels",
|
||||
body: rows(pairs_at(&image, &["Config", "Labels"])),
|
||||
},
|
||||
])
|
||||
@@ -594,7 +635,7 @@ pub fn network_details(output: &str) -> Option<Vec<DetailSection>> {
|
||||
text(config, "Subnet").to_owned(),
|
||||
match text(config, "Gateway") {
|
||||
"" => String::new(),
|
||||
gateway => format!("网关 {gateway}"),
|
||||
gateway => t!("docker.network.gateway", address = gateway).into(),
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -621,35 +662,53 @@ pub fn network_details(output: &str) -> Option<Vec<DetailSection>> {
|
||||
Some(vec![
|
||||
DetailSection {
|
||||
id: "basics",
|
||||
title: "基础信息",
|
||||
title: "docker.section.basics",
|
||||
body: rows([
|
||||
("名称", string_at(&network, &["Name"])),
|
||||
("ID", string_at(&network, &["Id"])),
|
||||
("驱动", string_at(&network, &["Driver"])),
|
||||
("范围", string_at(&network, &["Scope"])),
|
||||
("创建时间", string_at(&network, &["Created"])),
|
||||
("IPv6", yes_no(&network, &["EnableIPv6"])),
|
||||
("内部网络", yes_no(&network, &["Internal"])),
|
||||
(
|
||||
RowLabel::Field("docker.field.name"),
|
||||
string_at(&network, &["Name"]),
|
||||
),
|
||||
(RowLabel::Raw("ID".into()), string_at(&network, &["Id"])),
|
||||
(
|
||||
RowLabel::Field("docker.field.driver"),
|
||||
string_at(&network, &["Driver"]),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.scope"),
|
||||
string_at(&network, &["Scope"]),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.created"),
|
||||
string_at(&network, &["Created"]),
|
||||
),
|
||||
(
|
||||
RowLabel::Raw("IPv6".into()),
|
||||
yes_no(&network, &["EnableIPv6"]),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.internal"),
|
||||
yes_no(&network, &["Internal"]),
|
||||
),
|
||||
]),
|
||||
},
|
||||
DetailSection {
|
||||
id: "subnets",
|
||||
title: "子网",
|
||||
title: "docker.section.subnets",
|
||||
body: rows(subnets),
|
||||
},
|
||||
DetailSection {
|
||||
id: "containers",
|
||||
title: "容器",
|
||||
title: "docker.section.containers",
|
||||
body: rows(containers),
|
||||
},
|
||||
DetailSection {
|
||||
id: "options",
|
||||
title: "选项",
|
||||
title: "docker.section.options",
|
||||
body: rows(pairs_at(&network, &["Options"])),
|
||||
},
|
||||
DetailSection {
|
||||
id: "labels",
|
||||
title: "标签",
|
||||
title: "docker.section.labels",
|
||||
body: rows(pairs_at(&network, &["Labels"])),
|
||||
},
|
||||
])
|
||||
@@ -846,7 +905,7 @@ v2.25.0
|
||||
panic!("{id} has no rows");
|
||||
};
|
||||
rows.into_iter()
|
||||
.find(|(key, _)| key == label)
|
||||
.find(|(key, _)| key.id() == label)
|
||||
.unwrap_or_else(|| panic!("no {label} in {id}"))
|
||||
.1
|
||||
}
|
||||
@@ -860,7 +919,7 @@ v2.25.0
|
||||
)
|
||||
.expect("volume");
|
||||
assert_eq!(
|
||||
row(&volume, "basics", "挂载点"),
|
||||
row(&volume, "basics", "mountpoint"),
|
||||
"/var/lib/docker/volumes/vw-data/_data"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -882,13 +941,13 @@ v2.25.0
|
||||
)
|
||||
.expect("image");
|
||||
assert_eq!(row(&image, "basics", "ID"), "4a3b5c6d7e8f9a");
|
||||
assert_eq!(row(&image, "basics", "大小"), "9.84 KB");
|
||||
assert_eq!(row(&image, "basics", "平台"), "linux/amd64");
|
||||
assert_eq!(row(&image, "basics", "层数"), "2");
|
||||
assert_eq!(row(&image, "basics", "size"), "9.84 KB");
|
||||
assert_eq!(row(&image, "basics", "platform"), "linux/amd64");
|
||||
assert_eq!(row(&image, "basics", "layers"), "2");
|
||||
// Nothing to say is said with a dash.
|
||||
assert_eq!(row(&image, "basics", "作者"), "—");
|
||||
assert_eq!(row(&image, "config", "命令"), "/hello");
|
||||
assert_eq!(row(&image, "config", "暴露端口"), "80/tcp");
|
||||
assert_eq!(row(&image, "basics", "author"), "—");
|
||||
assert_eq!(row(&image, "config", "command"), "/hello");
|
||||
assert_eq!(row(&image, "config", "exposed_ports"), "80/tcp");
|
||||
assert_eq!(
|
||||
section(&image, "environment"),
|
||||
SectionBody::Text(vec!["PATH=/usr/bin".into()])
|
||||
@@ -907,6 +966,24 @@ v2.25.0
|
||||
assert_eq!(volume_details("Error: no such volume", &[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_heading_and_label_has_its_text() {
|
||||
let mut sections = ContainerDetails::default().sections();
|
||||
sections.extend(volume_details("[{}]", &[]).expect("volume"));
|
||||
sections.extend(image_details("[{}]", &[]).expect("image"));
|
||||
sections.extend(network_details("[{}]").expect("network"));
|
||||
for section in sections {
|
||||
assert!(crate::i18n::has(section.title), "{}", section.title);
|
||||
if let SectionBody::Rows(rows) = section.body {
|
||||
for (label, _) in rows {
|
||||
if let RowLabel::Field(key) = label {
|
||||
assert!(crate::i18n::has(key), "{key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_an_id_goes_into_a_command() {
|
||||
assert!(valid_id("f8664a4a9b8d"));
|
||||
|
||||
+1
-1
@@ -14,5 +14,5 @@ mod object_details;
|
||||
pub use container_details::open_container_dialog;
|
||||
pub use docker_panel::DockerPanel;
|
||||
pub use linux::{control_command, done, remove_command};
|
||||
pub use model::{Container, ContainerCommand, DockerObject, ObjectSummary};
|
||||
pub use model::{Container, ContainerCommand, ContainerSubject, DockerObject, ObjectSummary};
|
||||
pub use object_details::open_object_dialog;
|
||||
+110
-57
@@ -3,6 +3,10 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use gpui_kit::SharedString;
|
||||
|
||||
use crate::i18n::{t, tn};
|
||||
|
||||
/// What a container is doing, as `docker ps` says.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ContainerState {
|
||||
@@ -37,15 +41,15 @@ impl ContainerState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
pub fn label(self) -> SharedString {
|
||||
match self {
|
||||
ContainerState::Running => "运行中",
|
||||
ContainerState::Paused => "已暂停",
|
||||
ContainerState::Restarting => "正在重启",
|
||||
ContainerState::Created => "已创建",
|
||||
ContainerState::Exited => "已停止",
|
||||
ContainerState::Dead => "异常",
|
||||
ContainerState::Removing => "正在删除",
|
||||
ContainerState::Running => t!("docker.state.running"),
|
||||
ContainerState::Paused => t!("docker.state.paused"),
|
||||
ContainerState::Restarting => t!("docker.state.restarting"),
|
||||
ContainerState::Created => t!("docker.state.created"),
|
||||
ContainerState::Exited => t!("docker.state.exited"),
|
||||
ContainerState::Dead => t!("docker.state.dead"),
|
||||
ContainerState::Removing => t!("docker.state.removing"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,22 +163,23 @@ impl ContainerCommand {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
pub fn label(self) -> SharedString {
|
||||
match self {
|
||||
ContainerCommand::Start => "启动",
|
||||
ContainerCommand::Stop => "停止",
|
||||
ContainerCommand::Restart => "重启",
|
||||
ContainerCommand::Start => t!("docker.command.start"),
|
||||
ContainerCommand::Stop => t!("docker.command.stop"),
|
||||
ContainerCommand::Restart => t!("docker.command.restart"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What it did, after the name: 「已停止」.
|
||||
pub fn done(self) -> &'static str {
|
||||
match self {
|
||||
ContainerCommand::Start => "已启动",
|
||||
ContainerCommand::Stop => "已停止",
|
||||
ContainerCommand::Restart => "已重启",
|
||||
}
|
||||
}
|
||||
/// What a container command is run on, for the question and the
|
||||
/// notification: 「容器“web”」, 「项目“php-56”」.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ContainerSubject {
|
||||
/// One container, by name.
|
||||
Container(String),
|
||||
/// A compose project's containers, by the project's name.
|
||||
Project(String),
|
||||
}
|
||||
|
||||
/// What kind of thing a removal removes.
|
||||
@@ -187,12 +192,12 @@ pub enum DockerObject {
|
||||
}
|
||||
|
||||
impl DockerObject {
|
||||
pub fn label(self) -> &'static str {
|
||||
pub fn label(self) -> SharedString {
|
||||
match self {
|
||||
DockerObject::Container => "容器",
|
||||
DockerObject::Image => "镜像",
|
||||
DockerObject::Volume => "卷",
|
||||
DockerObject::Network => "网络",
|
||||
DockerObject::Container => t!("docker.object.container"),
|
||||
DockerObject::Image => t!("docker.object.image"),
|
||||
DockerObject::Volume => t!("docker.object.volume"),
|
||||
DockerObject::Network => t!("docker.object.network"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,12 +220,12 @@ impl DockerTab {
|
||||
DockerTab::Networks,
|
||||
];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
pub fn label(self) -> SharedString {
|
||||
match self {
|
||||
DockerTab::Containers => "容器",
|
||||
DockerTab::Volumes => "卷",
|
||||
DockerTab::Images => "镜像",
|
||||
DockerTab::Networks => "网络",
|
||||
DockerTab::Containers => t!("docker.tab.containers"),
|
||||
DockerTab::Volumes => t!("docker.tab.volumes"),
|
||||
DockerTab::Images => t!("docker.tab.images"),
|
||||
DockerTab::Networks => t!("docker.tab.networks"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -468,7 +473,7 @@ pub struct ObjectSummary {
|
||||
pub detail: String,
|
||||
pub tag: Option<(String, Tone)>,
|
||||
/// Whether it can be removed, and why not.
|
||||
pub removable: Result<(), &'static str>,
|
||||
pub removable: Result<(), SharedString>,
|
||||
/// The containers using it, by name.
|
||||
pub used_by: Vec<String>,
|
||||
}
|
||||
@@ -491,12 +496,12 @@ impl Volume {
|
||||
name: self.name.clone(),
|
||||
detail: joined(&[&self.driver, &self.mountpoint]),
|
||||
tag: Some(if in_use {
|
||||
("使用中".into(), Tone::Good)
|
||||
(t!("docker.object.in_use").into(), Tone::Good)
|
||||
} else {
|
||||
("未使用".into(), Tone::Quiet)
|
||||
(t!("docker.object.unused").into(), Tone::Quiet)
|
||||
}),
|
||||
removable: if in_use {
|
||||
Err("有容器在用,不能删除")
|
||||
Err(t!("docker.object.in_use_by_containers"))
|
||||
} else {
|
||||
Ok(())
|
||||
},
|
||||
@@ -514,11 +519,11 @@ impl Image {
|
||||
name: self
|
||||
.reference
|
||||
.clone()
|
||||
.unwrap_or_else(|| "未命名镜像".into()),
|
||||
.unwrap_or_else(|| t!("docker.image.unnamed").into()),
|
||||
detail: joined(&[&self.id, &self.size, &self.created]),
|
||||
tag: in_use.then(|| ("使用中".into(), Tone::Good)),
|
||||
tag: in_use.then(|| (t!("docker.object.in_use").into(), Tone::Good)),
|
||||
removable: if in_use {
|
||||
Err("有容器在用,不能删除")
|
||||
Err(t!("docker.object.in_use_by_containers"))
|
||||
} else {
|
||||
Ok(())
|
||||
},
|
||||
@@ -531,16 +536,19 @@ impl Network {
|
||||
pub fn summary(&self) -> ObjectSummary {
|
||||
let (tag, removable) = if self.builtin() {
|
||||
(
|
||||
("内置".to_owned(), Tone::Quiet),
|
||||
Err("Docker 自带的网络,不能删除"),
|
||||
(t!("docker.network.builtin").into(), Tone::Quiet),
|
||||
Err(t!("docker.network.builtin_kept")),
|
||||
)
|
||||
} else if !self.used_by.is_empty() {
|
||||
(
|
||||
(format!("{} 个容器", self.used_by.len()), Tone::Good),
|
||||
Err("有容器连着,不能删除"),
|
||||
(
|
||||
tn!("docker.network.containers", self.used_by.len()).into(),
|
||||
Tone::Good,
|
||||
),
|
||||
Err(t!("docker.network.in_use")),
|
||||
)
|
||||
} else {
|
||||
(("未使用".to_owned(), Tone::Quiet), Ok(()))
|
||||
((t!("docker.object.unused").into(), Tone::Quiet), Ok(()))
|
||||
};
|
||||
ObjectSummary {
|
||||
object: DockerObject::Network,
|
||||
@@ -559,14 +567,48 @@ impl Network {
|
||||
pub struct DetailSection {
|
||||
/// For the test ids of its lines: 「basics」.
|
||||
pub id: &'static str,
|
||||
/// The heading's text, by its key: translated where it is drawn.
|
||||
pub title: &'static str,
|
||||
pub body: SectionBody,
|
||||
}
|
||||
|
||||
/// What a line of details is labelled with.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum RowLabel {
|
||||
/// One of ShellRS's labels, by its text's key: 「docker.field.mountpoint」.
|
||||
Field(&'static str),
|
||||
/// One of the host's, as it is: a port, a path, a label's key.
|
||||
Raw(String),
|
||||
}
|
||||
|
||||
impl RowLabel {
|
||||
/// What the line is found by: a field by the last part of its key
|
||||
/// (「mountpoint」), the host's own as it is.
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
RowLabel::Field(key) => key.rsplit('.').next().unwrap_or(key),
|
||||
RowLabel::Raw(label) => label,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(&self) -> SharedString {
|
||||
match self {
|
||||
RowLabel::Field(key) => t!(*key),
|
||||
RowLabel::Raw(label) => label.clone().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for RowLabel {
|
||||
fn from(label: String) -> Self {
|
||||
RowLabel::Raw(label)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SectionBody {
|
||||
/// Labels and their values: 「挂载点」 「/var/lib/docker/volumes/…」.
|
||||
Rows(Vec<(String, String)>),
|
||||
Rows(Vec<(RowLabel, String)>),
|
||||
/// Names, one a line: the containers using it.
|
||||
List(Vec<String>),
|
||||
/// Lines of text as they are, in a fixed-width face: the environment.
|
||||
@@ -583,7 +625,9 @@ impl SectionBody {
|
||||
}
|
||||
|
||||
/// Labels and values, with 「—」 for the empty values.
|
||||
pub fn rows(rows: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>) -> SectionBody {
|
||||
pub fn rows(
|
||||
rows: impl IntoIterator<Item = (impl Into<RowLabel>, impl Into<String>)>,
|
||||
) -> SectionBody {
|
||||
SectionBody::Rows(
|
||||
rows.into_iter()
|
||||
.map(|(label, value)| {
|
||||
@@ -607,34 +651,43 @@ impl ContainerDetails {
|
||||
vec![
|
||||
DetailSection {
|
||||
id: "basics",
|
||||
title: "基础信息",
|
||||
title: "docker.section.basics",
|
||||
body: rows([
|
||||
("名称", self.name.clone()),
|
||||
("ID", self.id.clone()),
|
||||
("镜像", self.image.clone()),
|
||||
("创建时间", self.created.clone()),
|
||||
("入口", self.entrypoint.clone()),
|
||||
("命令", self.command.clone()),
|
||||
(RowLabel::Field("docker.field.name"), self.name.clone()),
|
||||
(RowLabel::Raw("ID".into()), self.id.clone()),
|
||||
(RowLabel::Field("docker.field.image"), self.image.clone()),
|
||||
(
|
||||
RowLabel::Field("docker.field.created"),
|
||||
self.created.clone(),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.entrypoint"),
|
||||
self.entrypoint.clone(),
|
||||
),
|
||||
(
|
||||
RowLabel::Field("docker.field.command"),
|
||||
self.command.clone(),
|
||||
),
|
||||
]),
|
||||
},
|
||||
DetailSection {
|
||||
id: "ports",
|
||||
title: "端口映射",
|
||||
title: "docker.section.ports",
|
||||
body: rows(self.ports.clone()),
|
||||
},
|
||||
DetailSection {
|
||||
id: "mounts",
|
||||
title: "挂载",
|
||||
title: "docker.section.mounts",
|
||||
body: rows(self.mounts.clone()),
|
||||
},
|
||||
DetailSection {
|
||||
id: "environment",
|
||||
title: "环境变量",
|
||||
title: "docker.section.environment",
|
||||
body: SectionBody::Text(self.environment.clone()),
|
||||
},
|
||||
DetailSection {
|
||||
id: "labels",
|
||||
title: "标签",
|
||||
title: "docker.section.labels",
|
||||
body: rows(self.labels.clone()),
|
||||
},
|
||||
]
|
||||
@@ -832,7 +885,7 @@ mod tests {
|
||||
let summary = |object, id: &str| table.summary_of(object, id).expect(id);
|
||||
let data = summary(DockerObject::Volume, "web-data");
|
||||
assert_eq!(data.tag, Some(("使用中".into(), Tone::Good)));
|
||||
assert_eq!(data.removable, Err("有容器在用,不能删除"));
|
||||
assert_eq!(data.removable, Err("有容器在用,不能删除".into()));
|
||||
assert_eq!(data.used_by, ["web"]);
|
||||
assert_eq!(summary(DockerObject::Volume, "old-data").removable, Ok(()));
|
||||
let dangling = summary(DockerObject::Image, "aaa");
|
||||
@@ -840,7 +893,7 @@ mod tests {
|
||||
assert_eq!(dangling.tag, None);
|
||||
assert_eq!(
|
||||
summary(DockerObject::Network, "bridge").removable,
|
||||
Err("Docker 自带的网络,不能删除")
|
||||
Err("Docker 自带的网络,不能删除".into())
|
||||
);
|
||||
assert_eq!(
|
||||
summary(DockerObject::Network, "web_default").tag,
|
||||
|
||||
@@ -12,6 +12,7 @@ use super::details::{Read, message, reading, render_sections, run};
|
||||
use super::linux::{image_details, inspect_command, network_details, volume_details};
|
||||
use super::model::{DetailSection, DockerObject, ObjectSummary, Tone};
|
||||
use crate::app::{CatalogIcon, RemoveDockerObject};
|
||||
use crate::i18n::t;
|
||||
use crate::shared::soft_tag;
|
||||
use crate::terminal::TerminalView;
|
||||
|
||||
@@ -26,7 +27,7 @@ pub(super) fn tone_color(tone: Tone, cx: &App) -> Hsla {
|
||||
}
|
||||
}
|
||||
|
||||
/// What the lines of `object`'s details are found by: 「volume-basics:挂载点」.
|
||||
/// What the lines of `object`'s details are found by: 「volume-basics:mountpoint」.
|
||||
fn prefix(object: DockerObject) -> &'static str {
|
||||
match object {
|
||||
DockerObject::Container => "container",
|
||||
@@ -76,8 +77,8 @@ pub fn open_object_dialog(
|
||||
Button::new("docker-dialog-remove")
|
||||
.outline()
|
||||
.icon(Icon::new(CatalogIcon::Trash))
|
||||
.label("删除")
|
||||
.when_some(summary.removable.err(), |button, why| {
|
||||
.label(t!("common.delete"))
|
||||
.when_some(summary.removable.clone().err(), |button, why| {
|
||||
button.disabled(true).tooltip(why)
|
||||
})
|
||||
.on_click(move |_, window, cx| {
|
||||
@@ -126,10 +127,7 @@ impl ObjectDetailsView {
|
||||
DockerObject::Container => None,
|
||||
} {
|
||||
Some(sections) => Read::Known(sections),
|
||||
None => Read::Unknown(format!(
|
||||
"读不到这个{}:它可能已经被删除",
|
||||
summary.object.label()
|
||||
)),
|
||||
None => Read::Unknown(gone(summary.object).into()),
|
||||
},
|
||||
Err(why) => Read::Unknown(why),
|
||||
};
|
||||
@@ -155,3 +153,13 @@ impl Render for ObjectDetailsView {
|
||||
div().id("docker-details").test_support().child(body)
|
||||
}
|
||||
}
|
||||
|
||||
/// Why there are no details: it went since the list was read.
|
||||
fn gone(object: DockerObject) -> SharedString {
|
||||
match object {
|
||||
DockerObject::Container => t!("docker.details.container_gone"),
|
||||
DockerObject::Image => t!("docker.details.image_gone"),
|
||||
DockerObject::Volume => t!("docker.details.volume_gone"),
|
||||
DockerObject::Network => t!("docker.details.network_gone"),
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ use gpui_kit::*;
|
||||
use super::bash::{self, Parsed};
|
||||
use super::model::{Entry, History};
|
||||
use crate::app::{CatalogIcon, CopyCommand, EnterCommand, RefreshHistory};
|
||||
use crate::i18n::{UiLocale, t, tn};
|
||||
use crate::shared::command_tooltip;
|
||||
use crate::terminal::{ExecResult, ExecTarget, RemoteTerminalId, exec_answer};
|
||||
|
||||
@@ -78,10 +79,15 @@ impl HistoryPanel {
|
||||
pub fn new(dispatch: FocusHandle, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let search = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("搜索命令")
|
||||
.placeholder(t!("history.panel.search"))
|
||||
.clean_on_escape()
|
||||
});
|
||||
let subscriptions = vec![
|
||||
cx.observe_global_in::<UiLocale>(window, |this, window, cx| {
|
||||
this.search.update(cx, |search, cx| {
|
||||
search.set_placeholder(t!("history.panel.search"), window, cx)
|
||||
});
|
||||
}),
|
||||
cx.subscribe(&search, |this, input, event: &InputEvent, cx| {
|
||||
if matches!(event, InputEvent::Change) {
|
||||
this.query = input.read(cx).value().to_string();
|
||||
@@ -192,7 +198,10 @@ impl HistoryPanel {
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let rows = Rc::new(history.matching(&self.query));
|
||||
let summary = format!("共 {} 条 · ~/.bash_history", history.entries().len());
|
||||
let summary = format!(
|
||||
"{} · ~/.bash_history",
|
||||
tn!("history.panel.count", history.entries().len())
|
||||
);
|
||||
let refresh = self.dispatch.clone();
|
||||
let header = v_flex()
|
||||
.flex_shrink_0()
|
||||
@@ -221,8 +230,8 @@ impl HistoryPanel {
|
||||
.xsmall()
|
||||
.icon(Icon::new(CatalogIcon::RefreshCw))
|
||||
.loading(self.loading)
|
||||
.tooltip("刷新")
|
||||
.accessibility_label("刷新")
|
||||
.tooltip(t!("tools.refresh"))
|
||||
.accessibility_label(t!("tools.refresh"))
|
||||
.on_click(move |_, window, cx| {
|
||||
refresh.dispatch_action(&RefreshHistory, window, cx)
|
||||
}),
|
||||
@@ -234,16 +243,12 @@ impl HistoryPanel {
|
||||
.when(history.truncated(), |header| {
|
||||
header.child(note(
|
||||
"history-truncated",
|
||||
format!("历史文件太长,只读了最后 {} KB。", bash::LIMIT / 1024),
|
||||
t!("history.panel.truncated", size = bash::LIMIT / 1024),
|
||||
cx,
|
||||
))
|
||||
})
|
||||
// Why the command just run is not there.
|
||||
.child(note(
|
||||
"history-note",
|
||||
"这个终端里执行的命令,bash 退出后才会出现在这里。".into(),
|
||||
cx,
|
||||
))
|
||||
.child(note("history-note", t!("history.panel.note"), cx))
|
||||
.child(
|
||||
Input::new(&self.search)
|
||||
.id("history-search")
|
||||
@@ -269,15 +274,15 @@ impl HistoryPanel {
|
||||
.map(|list| {
|
||||
if rows.is_empty() {
|
||||
let empty = if self.query.trim().is_empty() {
|
||||
"没有历史命令"
|
||||
t!("history.panel.empty")
|
||||
} else {
|
||||
"没有符合条件的命令"
|
||||
t!("history.panel.no_match")
|
||||
};
|
||||
list.child(
|
||||
div()
|
||||
.id("history-empty")
|
||||
.test_support()
|
||||
.aria_label(empty)
|
||||
.aria_label(empty.clone())
|
||||
.py_8()
|
||||
.text_sm()
|
||||
.text_center()
|
||||
@@ -361,7 +366,7 @@ impl Render for HistoryPanel {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(Spinner::new().small())
|
||||
.child("正在读取…"),
|
||||
.child(t!("tools.reading")),
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -370,19 +375,12 @@ impl Render for HistoryPanel {
|
||||
fn render_problem(problem: &Problem, cx: &App) -> impl IntoElement {
|
||||
let (text, color) = match problem {
|
||||
Problem::NotConnected => (
|
||||
"终端没有连接。连接后这里显示主机上 bash 的历史命令。".to_string(),
|
||||
t!("history.panel.not_connected"),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Missing => (
|
||||
"这台主机上还没有 bash 的历史命令(~/.bash_history)。bash 退出时才会写入它。"
|
||||
.to_string(),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Unsupported => (
|
||||
"暂不支持读取这台主机的历史命令,只支持 bash 的 ~/.bash_history。".to_string(),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Failed(error) => (format!("读取失败:{error}"), cx.theme().danger),
|
||||
Problem::Missing => (t!("history.panel.missing"), cx.theme().muted_foreground),
|
||||
Problem::Unsupported => (t!("history.panel.unsupported"), cx.theme().muted_foreground),
|
||||
Problem::Failed(error) => (t!("tools.read_failed", error = error), cx.theme().danger),
|
||||
};
|
||||
div()
|
||||
.id("history-message")
|
||||
@@ -394,7 +392,7 @@ fn render_problem(problem: &Problem, cx: &App) -> impl IntoElement {
|
||||
}
|
||||
|
||||
/// A line under the summary.
|
||||
fn note(id: &'static str, text: String, cx: &App) -> impl IntoElement {
|
||||
fn note(id: &'static str, text: SharedString, cx: &App) -> impl IntoElement {
|
||||
div()
|
||||
.id(id)
|
||||
.test_support()
|
||||
@@ -409,7 +407,7 @@ fn build_context_menu(hit: Option<String>, menu: PopupMenu) -> PopupMenu {
|
||||
return menu;
|
||||
};
|
||||
menu.menu_with_icon(
|
||||
"输入到终端",
|
||||
t!("tools.menu.insert"),
|
||||
Icon::new(CatalogIcon::SquareTerminal),
|
||||
Box::new(EnterCommand {
|
||||
command: command.clone(),
|
||||
@@ -417,7 +415,7 @@ fn build_context_menu(hit: Option<String>, menu: PopupMenu) -> PopupMenu {
|
||||
}),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"执行",
|
||||
t!("tools.run"),
|
||||
Icon::new(CatalogIcon::Play),
|
||||
Box::new(EnterCommand {
|
||||
command: command.clone(),
|
||||
@@ -426,7 +424,7 @@ fn build_context_menu(hit: Option<String>, menu: PopupMenu) -> PopupMenu {
|
||||
)
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"复制",
|
||||
t!("history.menu.copy"),
|
||||
Icon::new(IconName::Copy),
|
||||
Box::new(CopyCommand(command)),
|
||||
)
|
||||
@@ -510,8 +508,8 @@ fn render_entry(
|
||||
Button::new(SharedString::from(format!("history-run:{command}")))
|
||||
.ghost()
|
||||
.icon(Icon::new(CatalogIcon::Play))
|
||||
.tooltip("执行")
|
||||
.accessibility_label("执行")
|
||||
.tooltip(t!("tools.run"))
|
||||
.accessibility_label(t!("tools.run"))
|
||||
.on_click(move |_, window, cx| {
|
||||
// Not the card's click too, which would only put it on
|
||||
// the line.
|
||||
|
||||
+36
-6
@@ -2,6 +2,8 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::i18n::{t, tn};
|
||||
|
||||
/// A command of the history, listed once however often it ran.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Entry {
|
||||
@@ -22,9 +24,10 @@ impl Entry {
|
||||
|
||||
/// 「3 分钟前 · 执行 5 次」, or without a time 「执行 5 次」.
|
||||
pub fn summary(&self, now: i64) -> String {
|
||||
let runs = tn!("history.entry.runs", self.runs);
|
||||
match self.last_run {
|
||||
Some(at) => format!("{} · 执行 {} 次", format_when(at, now), self.runs),
|
||||
None => format!("执行 {} 次", self.runs),
|
||||
Some(at) => format!("{} · {runs}", format_when(at, now)),
|
||||
None => runs.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,10 +95,10 @@ pub fn format_when(at: i64, now: i64) -> String {
|
||||
// A host whose clock runs ahead says its commands ran in the future.
|
||||
let ago = now - at;
|
||||
match ago {
|
||||
..MINUTE => "刚刚".into(),
|
||||
MINUTE..HOUR => format!("{} 分钟前", ago / MINUTE),
|
||||
HOUR..DAY => format!("{} 小时前", ago / HOUR),
|
||||
DAY..=MONTH => format!("{} 天前", ago / DAY),
|
||||
..MINUTE => t!("history.when.just_now").into(),
|
||||
MINUTE..HOUR => tn!("history.when.minutes", ago / MINUTE).into(),
|
||||
HOUR..DAY => tn!("history.when.hours", ago / HOUR).into(),
|
||||
DAY..=MONTH => tn!("history.when.days", ago / DAY).into(),
|
||||
_ => chrono::DateTime::from_timestamp(at, 0)
|
||||
.map(|time| {
|
||||
time.with_timezone(&chrono::Local)
|
||||
@@ -157,6 +160,33 @@ mod tests {
|
||||
assert!(long_ago.starts_with("2024-08-0"), "{long_ago}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn english_says_how_long_ago_and_how_often() {
|
||||
crate::i18n::isolate_thread();
|
||||
crate::i18n::set_locale("en");
|
||||
let now = 1_727_846_400;
|
||||
assert_eq!(format_when(now - 20, now), "Just now");
|
||||
assert_eq!(format_when(now - 60, now), "1 minute ago");
|
||||
assert_eq!(format_when(now - 5 * 60, now), "5 minutes ago");
|
||||
assert_eq!(format_when(now - 3600, now), "1 hour ago");
|
||||
assert_eq!(format_when(now - 2 * 86_400, now), "2 days ago");
|
||||
let entry = Entry {
|
||||
command: "ls".into(),
|
||||
last_run: Some(now - 86_400),
|
||||
runs: 1,
|
||||
};
|
||||
assert_eq!(entry.summary(now), "1 day ago · Ran once");
|
||||
assert_eq!(
|
||||
Entry {
|
||||
last_run: None,
|
||||
runs: 4,
|
||||
..entry
|
||||
}
|
||||
.summary(now),
|
||||
"Ran 4 times"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_command_of_several_lines_shows_on_one() {
|
||||
let entry = Entry {
|
||||
|
||||
+171
-116
@@ -24,6 +24,7 @@ use super::model::{
|
||||
format_rate,
|
||||
};
|
||||
use crate::app::{CatalogIcon, ToggleMonitorDetail};
|
||||
use crate::i18n::{t, tn};
|
||||
use crate::shared::{format_bytes, format_duration, format_percent};
|
||||
use crate::terminal::{ExecResult, ExecTarget, RemoteTerminalId, exec_answer};
|
||||
|
||||
@@ -276,7 +277,7 @@ impl Render for MonitorPanel {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(Spinner::new().small())
|
||||
.child("正在读取…"),
|
||||
.child(t!("tools.reading")),
|
||||
),
|
||||
})
|
||||
.overflow_y_scrollbar();
|
||||
@@ -291,18 +292,17 @@ impl Render for MonitorPanel {
|
||||
fn render_problem(problem: &Problem, cx: &App) -> impl IntoElement {
|
||||
let (text, color) = match problem {
|
||||
Problem::NotConnected => (
|
||||
"终端没有连接。连接后这里显示主机的 CPU、内存、网络和磁盘。".to_string(),
|
||||
t!("monitor.panel.not_connected"),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Unsupported(Some(system)) => (
|
||||
format!("暂不支持 {system} 的监控,目前只支持 Linux 主机。"),
|
||||
t!("monitor.panel.unsupported_system", system = system),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Unsupported(None) => (
|
||||
"暂不支持这台主机的监控,目前只支持 Linux 主机。".to_string(),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Failed(error) => (format!("读取失败:{error}"), cx.theme().danger),
|
||||
Problem::Unsupported(None) => {
|
||||
(t!("monitor.panel.unsupported"), cx.theme().muted_foreground)
|
||||
}
|
||||
Problem::Failed(error) => (t!("tools.read_failed", error = error), cx.theme().danger),
|
||||
};
|
||||
div()
|
||||
.id("monitor-message")
|
||||
@@ -317,7 +317,7 @@ fn render_problem(problem: &Problem, cx: &App) -> impl IntoElement {
|
||||
fn card(
|
||||
id: &'static str,
|
||||
icon: impl Into<Icon>,
|
||||
title: &'static str,
|
||||
title: impl Into<SharedString>,
|
||||
meta: Option<AnyElement>,
|
||||
cx: &App,
|
||||
) -> GroupBox {
|
||||
@@ -330,14 +330,14 @@ fn card(
|
||||
.flex_1()
|
||||
.text_sm()
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.child(title),
|
||||
.child(title.into()),
|
||||
)
|
||||
.children(meta),
|
||||
)
|
||||
}
|
||||
|
||||
/// A card's quiet note at the end of its title row: 「1 核」.
|
||||
fn note(text: String, cx: &App) -> AnyElement {
|
||||
fn note(text: SharedString, cx: &App) -> AnyElement {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
@@ -348,10 +348,10 @@ fn note(text: String, cx: &App) -> AnyElement {
|
||||
/// In place of the note, when the card folds something away: 「8 核 ⌄」.
|
||||
fn unfold_button(
|
||||
id: &'static str,
|
||||
label: String,
|
||||
label: SharedString,
|
||||
detail: MonitorDetail,
|
||||
unfolded: bool,
|
||||
tooltip: &'static str,
|
||||
tooltip: SharedString,
|
||||
dispatch: &FocusHandle,
|
||||
) -> AnyElement {
|
||||
let dispatch = dispatch.clone();
|
||||
@@ -388,7 +388,7 @@ fn numeric(text: impl Into<SharedString>, cx: &App) -> Div {
|
||||
}
|
||||
|
||||
fn render_system(system: &SystemInfo, uptime: Option<Duration>, cx: &App) -> impl IntoElement {
|
||||
let field = |id: &'static str, label: &'static str, value: String| {
|
||||
let field = |id: &'static str, label: SharedString, value: String| {
|
||||
v_flex()
|
||||
.min_w_0()
|
||||
.gap_0p5()
|
||||
@@ -408,18 +408,37 @@ fn render_system(system: &SystemInfo, uptime: Option<Duration>, cx: &App) -> imp
|
||||
.child(value),
|
||||
)
|
||||
};
|
||||
card("monitor-system", CatalogIcon::Monitor, "系统", None, cx).child(
|
||||
card(
|
||||
"monitor-system",
|
||||
CatalogIcon::Monitor,
|
||||
t!("monitor.system.title"),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.grid()
|
||||
.grid_cols(2)
|
||||
.gap_x_4()
|
||||
.gap_y_3()
|
||||
.child(field("monitor-host", "主机", system.host_name.clone()))
|
||||
.child(field("monitor-arch", "架构", system.arch.clone()))
|
||||
.child(field("monitor-os", "系统", system.os.clone()))
|
||||
.child(field(
|
||||
"monitor-host",
|
||||
t!("monitor.system.host"),
|
||||
system.host_name.clone(),
|
||||
))
|
||||
.child(field(
|
||||
"monitor-arch",
|
||||
t!("monitor.system.arch"),
|
||||
system.arch.clone(),
|
||||
))
|
||||
.child(field(
|
||||
"monitor-os",
|
||||
t!("monitor.system.os"),
|
||||
system.os.clone(),
|
||||
))
|
||||
.child(field(
|
||||
"monitor-uptime",
|
||||
"运行时长",
|
||||
t!("monitor.system.uptime"),
|
||||
uptime.map(format_duration).unwrap_or_default(),
|
||||
)),
|
||||
)
|
||||
@@ -452,7 +471,7 @@ fn gauge(id: &'static str, percent: Option<f32>, cx: &App) -> impl IntoElement {
|
||||
/// A labelled bar: the label and the share above it.
|
||||
fn meter(
|
||||
id: &'static str,
|
||||
label: &'static str,
|
||||
label: SharedString,
|
||||
percent: Option<f32>,
|
||||
cx: &App,
|
||||
) -> impl IntoElement {
|
||||
@@ -512,7 +531,7 @@ fn render_cpu(
|
||||
let more = layout
|
||||
.per_row
|
||||
.filter(|per_row| cores.is_some_and(|cores| cores.len() > *per_row));
|
||||
let count = format!("{} 核", snapshot.cores);
|
||||
let count = tn!("monitor.cpu.cores", snapshot.cores);
|
||||
let meta = if more.is_some() {
|
||||
unfold_button(
|
||||
"monitor-cores-toggle",
|
||||
@@ -520,9 +539,9 @@ fn render_cpu(
|
||||
MonitorDetail::Cores,
|
||||
layout.unfolded,
|
||||
if layout.unfolded {
|
||||
"收起更多核的占用"
|
||||
t!("monitor.cpu.fold_cores")
|
||||
} else {
|
||||
"显示更多核的占用"
|
||||
t!("monitor.cpu.unfold_cores")
|
||||
},
|
||||
dispatch,
|
||||
)
|
||||
@@ -552,7 +571,12 @@ fn render_cpu(
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.gap_3()
|
||||
.child(meter("monitor-cpu-usage", "平均使用率", total, cx))
|
||||
.child(meter(
|
||||
"monitor-cpu-usage",
|
||||
t!("monitor.cpu.average"),
|
||||
total,
|
||||
cx,
|
||||
))
|
||||
.when_some(shown, |column, shown| {
|
||||
column.child(render_cores(shown, layout, cx))
|
||||
}),
|
||||
@@ -565,12 +589,19 @@ fn render_cpu(
|
||||
/// the first row; it measures its width for the next frame to know how many
|
||||
/// make one. Pointing at a bar tells its core's number and share.
|
||||
fn render_cores(cores: &[f32], layout: &CoresLayout, cx: &App) -> impl IntoElement {
|
||||
let core = |index: usize, load: f32| {
|
||||
t!(
|
||||
"monitor.cpu.core",
|
||||
index = index,
|
||||
load = format_percent(load)
|
||||
)
|
||||
};
|
||||
let label = cores
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, load)| format!("核 {index}:{}", format_percent(*load)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
.map(|(index, load)| core(index, *load))
|
||||
.reduce(|list, next| t!("monitor.cpu.core_list", list = list, core = next))
|
||||
.unwrap_or_default();
|
||||
let (width, panel) = (layout.width.clone(), layout.panel);
|
||||
h_flex()
|
||||
.id("monitor-cpu-cores")
|
||||
@@ -586,7 +617,7 @@ fn render_cores(cores: &[f32], layout: &CoresLayout, cx: &App) -> impl IntoEleme
|
||||
})
|
||||
.children(cores.iter().enumerate().map(|(index, load)| {
|
||||
let color = load_color(*load, cx);
|
||||
let tooltip: SharedString = format!("核 {index}:{}", format_percent(*load)).into();
|
||||
let tooltip = core(index, *load);
|
||||
div()
|
||||
.id(("monitor-core", index as u64))
|
||||
.test_support()
|
||||
@@ -638,7 +669,14 @@ fn amount(id: &'static str, used: u64, total: u64, cx: &App) -> impl IntoElement
|
||||
}
|
||||
|
||||
fn render_memory(memory: &Memory, cx: &App) -> impl IntoElement {
|
||||
card("monitor-memory", IconName::MemoryStick, "内存", None, cx).child(
|
||||
card(
|
||||
"monitor-memory",
|
||||
IconName::MemoryStick,
|
||||
t!("monitor.memory.title"),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.child(gauge("monitor-memory-gauge", Some(memory.percent()), cx))
|
||||
@@ -652,7 +690,7 @@ fn render_memory(memory: &Memory, cx: &App) -> impl IntoElement {
|
||||
.gap_1p5()
|
||||
.child(meter(
|
||||
"monitor-memory-usage",
|
||||
"物理内存",
|
||||
t!("monitor.memory.physical"),
|
||||
Some(memory.percent()),
|
||||
cx,
|
||||
))
|
||||
@@ -669,7 +707,7 @@ fn render_memory(memory: &Memory, cx: &App) -> impl IntoElement {
|
||||
.gap_1p5()
|
||||
.child(meter(
|
||||
"monitor-swap-usage",
|
||||
"交换空间",
|
||||
t!("monitor.memory.swap"),
|
||||
Some(memory.swap_percent()),
|
||||
cx,
|
||||
))
|
||||
@@ -695,13 +733,13 @@ fn render_network(
|
||||
let meta = (busy.len() > 1).then(|| {
|
||||
unfold_button(
|
||||
"monitor-interfaces-toggle",
|
||||
format!("{} 个网卡", busy.len()),
|
||||
tn!("monitor.network.interfaces", busy.len()),
|
||||
MonitorDetail::Interfaces,
|
||||
unfolded,
|
||||
if unfolded {
|
||||
"只显示主网卡"
|
||||
t!("monitor.network.fold")
|
||||
} else {
|
||||
"显示全部网卡"
|
||||
t!("monitor.network.unfold")
|
||||
},
|
||||
dispatch,
|
||||
)
|
||||
@@ -712,41 +750,47 @@ fn render_network(
|
||||
snapshot.main_interface().into_iter().collect()
|
||||
};
|
||||
let rate = |rate: Option<f64>| rate.map_or("—".into(), format_rate);
|
||||
card("monitor-network", IconName::Network, "网络", meta, cx)
|
||||
.when(shown.is_empty(), |card| {
|
||||
card.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("没有在用的网卡"),
|
||||
)
|
||||
})
|
||||
.children(shown.into_iter().map(|interface| {
|
||||
let (up, down) = (rate(interface.transmitted), rate(interface.received));
|
||||
h_flex()
|
||||
.id(SharedString::from(format!(
|
||||
"monitor-interface:{}",
|
||||
interface.name
|
||||
)))
|
||||
.test_support()
|
||||
.aria_label(format!("上传 {up},下载 {down}"))
|
||||
.gap_3()
|
||||
card(
|
||||
"monitor-network",
|
||||
IconName::Network,
|
||||
t!("monitor.network.title"),
|
||||
meta,
|
||||
cx,
|
||||
)
|
||||
.when(shown.is_empty(), |card| {
|
||||
card.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.child(
|
||||
numeric(interface.name.clone(), cx)
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.gap_3()
|
||||
.text_xs()
|
||||
.child(direction(IconName::ArrowUp, up, cx))
|
||||
.child(direction(IconName::ArrowDown, down, cx)),
|
||||
)
|
||||
}))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(t!("monitor.network.none")),
|
||||
)
|
||||
})
|
||||
.children(shown.into_iter().map(|interface| {
|
||||
let (up, down) = (rate(interface.transmitted), rate(interface.received));
|
||||
h_flex()
|
||||
.id(SharedString::from(format!(
|
||||
"monitor-interface:{}",
|
||||
interface.name
|
||||
)))
|
||||
.test_support()
|
||||
.aria_label(t!("monitor.network.label", up = up, down = down))
|
||||
.gap_3()
|
||||
.text_sm()
|
||||
.child(
|
||||
numeric(interface.name.clone(), cx)
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.gap_3()
|
||||
.text_xs()
|
||||
.child(direction(IconName::ArrowUp, up, cx))
|
||||
.child(direction(IconName::ArrowDown, down, cx)),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn direction(icon: IconName, rate: String, cx: &App) -> impl IntoElement {
|
||||
@@ -761,54 +805,65 @@ fn direction(icon: IconName, rate: String, cx: &App) -> impl IntoElement {
|
||||
}
|
||||
|
||||
fn render_disks(disks: &[DiskUsage], cx: &App) -> impl IntoElement {
|
||||
card("monitor-disks", IconName::HardDrive, "磁盘", None, cx)
|
||||
.when(disks.is_empty(), |card| {
|
||||
card.child(
|
||||
div()
|
||||
card(
|
||||
"monitor-disks",
|
||||
IconName::HardDrive,
|
||||
t!("monitor.disks.title"),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.when(disks.is_empty(), |card| {
|
||||
card.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(t!("monitor.disks.none")),
|
||||
)
|
||||
})
|
||||
.children(disks.iter().map(|disk| {
|
||||
let percent = disk.percent();
|
||||
let color = load_color(percent, cx);
|
||||
let (used, total) = (format_bytes(disk.used), format_bytes(disk.total));
|
||||
v_flex()
|
||||
.id(SharedString::from(format!("monitor-disk:{}", disk.mount)))
|
||||
.test_support()
|
||||
.aria_label(t!(
|
||||
"monitor.disks.label",
|
||||
percent = format_percent(percent),
|
||||
used = used,
|
||||
total = total
|
||||
))
|
||||
.gap_1p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("没有读到磁盘"),
|
||||
.child(
|
||||
numeric(disk.mount.clone(), cx)
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
numeric(format_percent(percent), cx)
|
||||
.flex_shrink_0()
|
||||
.text_color(color)
|
||||
.font_weight(FontWeight::SEMIBOLD),
|
||||
),
|
||||
)
|
||||
})
|
||||
.children(disks.iter().map(|disk| {
|
||||
let percent = disk.percent();
|
||||
let color = load_color(percent, cx);
|
||||
let (used, total) = (format_bytes(disk.used), format_bytes(disk.total));
|
||||
v_flex()
|
||||
.id(SharedString::from(format!("monitor-disk:{}", disk.mount)))
|
||||
.test_support()
|
||||
.aria_label(format!("{},{used} / {total}", format_percent(percent)))
|
||||
.gap_1p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.text_sm()
|
||||
.child(
|
||||
numeric(disk.mount.clone(), cx)
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
numeric(format_percent(percent), cx)
|
||||
.flex_shrink_0()
|
||||
.text_color(color)
|
||||
.font_weight(FontWeight::SEMIBOLD),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Progress::new(SharedString::from(format!(
|
||||
"monitor-disk-bar:{}",
|
||||
disk.mount
|
||||
)))
|
||||
.small()
|
||||
.value(percent)
|
||||
.color(color),
|
||||
)
|
||||
.child(
|
||||
numeric(format!("{used} / {total}"), cx)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
}))
|
||||
.child(
|
||||
Progress::new(SharedString::from(format!(
|
||||
"monitor-disk-bar:{}",
|
||||
disk.mount
|
||||
)))
|
||||
.small()
|
||||
.value(percent)
|
||||
.color(color),
|
||||
)
|
||||
.child(
|
||||
numeric(format!("{used} / {total}"), cx)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
}))
|
||||
}
|
||||
+58
-31
@@ -2,6 +2,10 @@
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use gpui_kit::SharedString;
|
||||
|
||||
use crate::i18n::{t, tn};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum Protocol {
|
||||
Tcp,
|
||||
@@ -80,10 +84,19 @@ impl SocketState {
|
||||
|
||||
/// How the list says it: 「监听」「已连接」, and the TCP name for the
|
||||
/// states only the TCP-minded look for: 「TIME_WAIT」.
|
||||
pub fn label(self) -> &'static str {
|
||||
pub fn label(self) -> SharedString {
|
||||
match self {
|
||||
SocketState::Listen => "监听",
|
||||
SocketState::Established => "已连接",
|
||||
SocketState::Listen => t!("netstat.state.listen"),
|
||||
SocketState::Established => t!("netstat.state.established"),
|
||||
other => other.netstat_name().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The name netstat gives it, which a search finds as well.
|
||||
fn netstat_name(self) -> &'static str {
|
||||
match self {
|
||||
SocketState::Listen => "LISTEN",
|
||||
SocketState::Established => "ESTABLISHED",
|
||||
SocketState::SynSent => "SYN_SENT",
|
||||
SocketState::SynReceived => "SYN_RECV",
|
||||
SocketState::FinWait1 => "FIN_WAIT1",
|
||||
@@ -96,15 +109,6 @@ impl SocketState {
|
||||
}
|
||||
}
|
||||
|
||||
/// The name netstat gives it, which a search finds as well.
|
||||
fn netstat_name(self) -> &'static str {
|
||||
match self {
|
||||
SocketState::Listen => "LISTEN",
|
||||
SocketState::Established => "ESTABLISHED",
|
||||
other => other.label(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the list puts it: what listens first, then the live
|
||||
/// connections, the ones on their way out last.
|
||||
fn rank(self) -> u8 {
|
||||
@@ -135,11 +139,11 @@ pub enum Role {
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn label(self) -> &'static str {
|
||||
pub fn label(self) -> SharedString {
|
||||
match self {
|
||||
Role::Listening => "监听端口",
|
||||
Role::Inbound => "入站连接",
|
||||
Role::Outbound => "出站连接",
|
||||
Role::Listening => t!("netstat.role.listening"),
|
||||
Role::Inbound => t!("netstat.role.inbound"),
|
||||
Role::Outbound => t!("netstat.role.outbound"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,16 +196,22 @@ impl Socket {
|
||||
let first = self.processes.first()?;
|
||||
Some(match self.processes.len() {
|
||||
1 => format!("{} (PID {})", first.name, first.pid),
|
||||
count => format!("{} (PID {} 等 {count} 个)", first.name, first.pid),
|
||||
count => t!(
|
||||
"netstat.process.several",
|
||||
name = first.name,
|
||||
pid = first.pid,
|
||||
count = count
|
||||
)
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// What the search looks in, lowercased.
|
||||
/// What the search looks in, lowercased; besides the state as the
|
||||
/// list says it, which follows the interface language.
|
||||
fn haystack(&self) -> String {
|
||||
let mut haystack = format!(
|
||||
"{} {} {} {} {}",
|
||||
"{} {} {} {}",
|
||||
self.protocol.label(),
|
||||
self.state.label(),
|
||||
self.state.netstat_name(),
|
||||
self.local,
|
||||
self.peer,
|
||||
@@ -243,8 +253,10 @@ impl Counts {
|
||||
/// 「共 96 条 · 15 个监听端口 · 21 条已连接」.
|
||||
pub fn summary(&self) -> String {
|
||||
format!(
|
||||
"共 {} 条 · {} 个监听端口 · {} 条已连接",
|
||||
self.total, self.listening, self.connected
|
||||
"{} · {} · {}",
|
||||
tn!("netstat.summary.total", self.total),
|
||||
tn!("netstat.summary.listening", self.listening),
|
||||
tn!("netstat.summary.connected", self.connected)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -369,6 +381,13 @@ impl Table {
|
||||
/// The sockets `filter` lets through, by their place in `sockets()`.
|
||||
pub fn filtered(&self, filter: &Filter) -> Vec<usize> {
|
||||
let query = filter.query.trim().to_lowercase();
|
||||
// The states as the list says them now, which the search finds.
|
||||
let mut labels: HashMap<SocketState, bool> = HashMap::new();
|
||||
let mut label_matches = |state: SocketState| {
|
||||
*labels
|
||||
.entry(state)
|
||||
.or_insert_with(|| state.label().to_lowercase().contains(&query))
|
||||
};
|
||||
self.sockets
|
||||
.iter()
|
||||
.zip(&self.haystacks)
|
||||
@@ -376,7 +395,9 @@ impl Table {
|
||||
.filter(|(_, (socket, haystack))| {
|
||||
filter.protocol.lets_through(socket.protocol)
|
||||
&& filter.state.lets_through(socket.state)
|
||||
&& (query.is_empty() || haystack.contains(&query))
|
||||
&& (query.is_empty()
|
||||
|| haystack.contains(&query)
|
||||
|| label_matches(socket.state))
|
||||
})
|
||||
.map(|(index, _)| index)
|
||||
.collect()
|
||||
@@ -398,11 +419,11 @@ impl ProtocolFilter {
|
||||
ProtocolFilter::Udp,
|
||||
];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
pub fn label(self) -> SharedString {
|
||||
match self {
|
||||
ProtocolFilter::All => "全部协议",
|
||||
ProtocolFilter::Tcp => "TCP",
|
||||
ProtocolFilter::Udp => "UDP",
|
||||
ProtocolFilter::All => t!("netstat.filter.all_protocols"),
|
||||
ProtocolFilter::Tcp => "TCP".into(),
|
||||
ProtocolFilter::Udp => "UDP".into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,14 +458,14 @@ impl StateFilter {
|
||||
StateFilter::Other,
|
||||
];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
pub fn label(self) -> SharedString {
|
||||
match self {
|
||||
StateFilter::All => "全部状态",
|
||||
StateFilter::All => t!("netstat.filter.all_states"),
|
||||
StateFilter::Listen => SocketState::Listen.label(),
|
||||
StateFilter::Established => SocketState::Established.label(),
|
||||
StateFilter::TimeWait => SocketState::TimeWait.label(),
|
||||
StateFilter::CloseWait => SocketState::CloseWait.label(),
|
||||
StateFilter::Other => "其他状态",
|
||||
StateFilter::Other => t!("netstat.filter.other_states"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,10 +586,16 @@ mod tests {
|
||||
#[test]
|
||||
fn what_listens_comes_first_and_shares_of_a_port_make_one_line() {
|
||||
let table = sample();
|
||||
let labels: Vec<SharedString> = table
|
||||
.sockets()
|
||||
.iter()
|
||||
.map(|socket| socket.state.label())
|
||||
.collect();
|
||||
let lines: Vec<(&str, &str, Role)> = table
|
||||
.sockets()
|
||||
.iter()
|
||||
.map(|socket| (socket.state.label(), socket.local.as_str(), socket.role))
|
||||
.zip(&labels)
|
||||
.map(|(socket, label)| (label.as_ref(), socket.local.as_str(), socket.role))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
lines,
|
||||
|
||||
@@ -22,6 +22,7 @@ use super::model::{
|
||||
Filter, Processes, ProtocolFilter, Role, Socket, SocketState, StateFilter, Table,
|
||||
};
|
||||
use crate::app::{CatalogIcon, RefreshConnections};
|
||||
use crate::i18n::{UiLocale, t, tn};
|
||||
use crate::terminal::{ExecResult, ExecTarget, RemoteTerminalId, exec_answer};
|
||||
|
||||
/// How long a terminal not connected is left before it is asked again.
|
||||
@@ -77,7 +78,7 @@ impl SearchableListItem for ProtocolFilter {
|
||||
type Value = Self;
|
||||
|
||||
fn title(&self) -> SharedString {
|
||||
self.label().into()
|
||||
self.label()
|
||||
}
|
||||
|
||||
fn value(&self) -> &Self::Value {
|
||||
@@ -89,7 +90,7 @@ impl SearchableListItem for StateFilter {
|
||||
type Value = Self;
|
||||
|
||||
fn title(&self) -> SharedString {
|
||||
self.label().into()
|
||||
self.label()
|
||||
}
|
||||
|
||||
fn value(&self) -> &Self::Value {
|
||||
@@ -101,7 +102,7 @@ impl NetstatPanel {
|
||||
pub fn new(dispatch: FocusHandle, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let search = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("搜索地址、端口、状态、PID 或进程")
|
||||
.placeholder(t!("netstat.panel.search"))
|
||||
.clean_on_escape()
|
||||
});
|
||||
let protocol = cx.new(|cx| {
|
||||
@@ -121,6 +122,11 @@ impl NetstatPanel {
|
||||
)
|
||||
});
|
||||
let subscriptions = vec![
|
||||
cx.observe_global_in::<UiLocale>(window, |this, window, cx| {
|
||||
this.search.update(cx, |search, cx| {
|
||||
search.set_placeholder(t!("netstat.panel.search"), window, cx)
|
||||
});
|
||||
}),
|
||||
cx.subscribe(&search, |this, input, event: &InputEvent, cx| {
|
||||
if matches!(event, InputEvent::Change) {
|
||||
let query = input.read(cx).value().to_string();
|
||||
@@ -297,8 +303,8 @@ impl NetstatPanel {
|
||||
.xsmall()
|
||||
.icon(Icon::new(CatalogIcon::RefreshCw))
|
||||
.loading(self.loading)
|
||||
.tooltip("刷新")
|
||||
.accessibility_label("刷新")
|
||||
.tooltip(t!("tools.refresh"))
|
||||
.accessibility_label(t!("tools.refresh"))
|
||||
.on_click(move |_, window, cx| {
|
||||
dispatch.dispatch_action(&RefreshConnections, window, cx)
|
||||
}),
|
||||
@@ -310,21 +316,17 @@ impl NetstatPanel {
|
||||
.when(table.truncated(), |header| {
|
||||
header.child(note(
|
||||
"netstat-truncated",
|
||||
format!("连接太多,只列出了前 {} 条。", linux::LIMIT),
|
||||
t!("netstat.panel.truncated", limit = linux::LIMIT),
|
||||
cx.theme().warning,
|
||||
))
|
||||
})
|
||||
.map(|header| {
|
||||
let text = match table.processes() {
|
||||
Processes::All => return header,
|
||||
Processes::Own => "当前用户不是 root,只能看到自己的进程。",
|
||||
Processes::None => "这台主机没有 ss 命令,看不到占用连接的进程。",
|
||||
Processes::Own => t!("netstat.panel.own_processes"),
|
||||
Processes::None => t!("netstat.panel.no_ss"),
|
||||
};
|
||||
header.child(note(
|
||||
"netstat-processes",
|
||||
text.into(),
|
||||
cx.theme().muted_foreground,
|
||||
))
|
||||
header.child(note("netstat-processes", text, cx.theme().muted_foreground))
|
||||
})
|
||||
.child(
|
||||
Input::new(&self.search)
|
||||
@@ -342,7 +344,7 @@ impl NetstatPanel {
|
||||
Select::new(&self.protocol)
|
||||
.id("netstat-protocol")
|
||||
.small()
|
||||
.accessibility_label("协议"),
|
||||
.accessibility_label(t!("netstat.panel.protocol")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -350,12 +352,12 @@ impl NetstatPanel {
|
||||
Select::new(&self.state)
|
||||
.id("netstat-state")
|
||||
.small()
|
||||
.accessibility_label("状态"),
|
||||
.accessibility_label(t!("netstat.panel.state")),
|
||||
),
|
||||
),
|
||||
)
|
||||
.when(self.filter.is_active(), |header| {
|
||||
let text = format!("筛选出 {} 条", rows.len());
|
||||
let text = tn!("netstat.panel.matches", rows.len());
|
||||
header.child(
|
||||
div()
|
||||
.id("netstat-matches")
|
||||
@@ -368,9 +370,9 @@ impl NetstatPanel {
|
||||
});
|
||||
|
||||
let empty = if self.filter.is_active() {
|
||||
"没有符合条件的连接"
|
||||
t!("netstat.panel.no_match")
|
||||
} else {
|
||||
"没有网络连接"
|
||||
t!("netstat.panel.empty")
|
||||
};
|
||||
// The scrollbar goes on the list's box, which does not scroll: on
|
||||
// the scrolled rows it would scroll away with them.
|
||||
@@ -388,7 +390,7 @@ impl NetstatPanel {
|
||||
div()
|
||||
.id("netstat-empty")
|
||||
.test_support()
|
||||
.aria_label(empty)
|
||||
.aria_label(empty.clone())
|
||||
.py_8()
|
||||
.text_sm()
|
||||
.text_center()
|
||||
@@ -450,7 +452,7 @@ impl Render for NetstatPanel {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(Spinner::new().small())
|
||||
.child("正在读取…"),
|
||||
.child(t!("tools.reading")),
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -459,18 +461,17 @@ impl Render for NetstatPanel {
|
||||
fn render_problem(problem: &Problem, cx: &App) -> impl IntoElement {
|
||||
let (text, color) = match problem {
|
||||
Problem::NotConnected => (
|
||||
"终端没有连接。连接后这里显示主机的网络连接。".to_string(),
|
||||
t!("netstat.panel.not_connected"),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Unsupported(Some(system)) => (
|
||||
format!("暂不支持查看 {system} 的网络连接,目前只支持 Linux 主机。"),
|
||||
t!("netstat.panel.unsupported_system", system = system),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Unsupported(None) => (
|
||||
"暂不支持查看这台主机的网络连接,目前只支持 Linux 主机。".to_string(),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Failed(error) => (format!("读取失败:{error}"), cx.theme().danger),
|
||||
Problem::Unsupported(None) => {
|
||||
(t!("netstat.panel.unsupported"), cx.theme().muted_foreground)
|
||||
}
|
||||
Problem::Failed(error) => (t!("tools.read_failed", error = error), cx.theme().danger),
|
||||
};
|
||||
div()
|
||||
.id("netstat-message")
|
||||
@@ -482,7 +483,7 @@ fn render_problem(problem: &Problem, cx: &App) -> impl IntoElement {
|
||||
}
|
||||
|
||||
/// A line under the summary: 「连接太多,只列出了前 3000 条。」.
|
||||
fn note(id: &'static str, text: String, color: Hsla) -> impl IntoElement {
|
||||
fn note(id: &'static str, text: SharedString, color: Hsla) -> impl IntoElement {
|
||||
div()
|
||||
.id(id)
|
||||
.test_support()
|
||||
@@ -517,16 +518,16 @@ fn role_icon(role: Role) -> CatalogIcon {
|
||||
fn render_socket(socket: &Socket, cx: &App) -> impl IntoElement {
|
||||
let color = state_color(socket.state, cx);
|
||||
let process = socket.process_label();
|
||||
let label = format!(
|
||||
"{} {} · 本地 {} · 远端 {} · {} · {}",
|
||||
socket.protocol.label(),
|
||||
socket.state.label(),
|
||||
socket.local,
|
||||
socket.peer,
|
||||
process.as_deref().unwrap_or("—"),
|
||||
socket.user.as_deref().unwrap_or("—"),
|
||||
let label = t!(
|
||||
"netstat.card.label",
|
||||
protocol = socket.protocol.label(),
|
||||
state = socket.state.label(),
|
||||
local = socket.local,
|
||||
peer = socket.peer,
|
||||
process = process.as_deref().unwrap_or("—"),
|
||||
user = socket.user.as_deref().unwrap_or("—")
|
||||
);
|
||||
let address = |name: &'static str, value: &str| {
|
||||
let address = |name: SharedString, value: &str| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
@@ -577,8 +578,8 @@ fn render_socket(socket: &Socket, cx: &App) -> impl IntoElement {
|
||||
.child(socket.role.label()),
|
||||
),
|
||||
)
|
||||
.child(address("本地", &socket.local))
|
||||
.child(address("远端", &socket.peer))
|
||||
.child(address(t!("netstat.card.local"), &socket.local))
|
||||
.child(address(t!("netstat.card.peer"), &socket.peer))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
@@ -591,7 +592,7 @@ fn render_socket(socket: &Socket, cx: &App) -> impl IntoElement {
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("进程"),
|
||||
.child(t!("netstat.card.process")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::model::{ProcessInfo, ProcessState, ProcessTimes, Reading};
|
||||
use crate::i18n::t;
|
||||
|
||||
/// The most processes a reading lists: some 300 bytes of `/proc` each, and
|
||||
/// a reading's output holds 1 MB.
|
||||
@@ -194,17 +195,17 @@ pub fn end_command(pid: u32, force: bool) -> String {
|
||||
pub fn ended(output: &str) -> Result<(), String> {
|
||||
let (said, status) = output
|
||||
.rsplit_once("@@status")
|
||||
.ok_or_else(|| "主机没有回答".to_string())?;
|
||||
.ok_or_else(|| t!("tools.no_answer").to_string())?;
|
||||
if status.trim() == "0" {
|
||||
return Ok(());
|
||||
}
|
||||
let said = said.trim();
|
||||
Err(if said.contains("not permitted") {
|
||||
"没有权限:只能结束自己的进程,root 才能结束所有进程".into()
|
||||
t!("processes.error.not_permitted").into()
|
||||
} else if said.contains("No such process") {
|
||||
"进程已经退出".into()
|
||||
t!("processes.error.gone").into()
|
||||
} else if said.is_empty() {
|
||||
format!("kill 返回 {}", status.trim())
|
||||
t!("processes.error.status", status = status.trim()).into()
|
||||
} else {
|
||||
said.to_owned()
|
||||
})
|
||||
|
||||
+12
-8
@@ -3,6 +3,10 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui_kit::SharedString;
|
||||
|
||||
use crate::i18n::t;
|
||||
|
||||
/// One look at a host's processes.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Reading {
|
||||
@@ -81,15 +85,15 @@ impl ProcessState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
pub fn label(self) -> SharedString {
|
||||
match self {
|
||||
ProcessState::Running => "运行中",
|
||||
ProcessState::Sleeping => "休眠",
|
||||
ProcessState::Uninterruptible => "等待 I/O",
|
||||
ProcessState::Stopped => "已暂停",
|
||||
ProcessState::Zombie => "僵尸进程",
|
||||
ProcessState::Idle => "空闲",
|
||||
ProcessState::Other => "其他",
|
||||
ProcessState::Running => t!("processes.state.running"),
|
||||
ProcessState::Sleeping => t!("processes.state.sleeping"),
|
||||
ProcessState::Uninterruptible => t!("processes.state.uninterruptible"),
|
||||
ProcessState::Stopped => t!("processes.state.stopped"),
|
||||
ProcessState::Zombie => t!("processes.state.zombie"),
|
||||
ProcessState::Idle => t!("processes.state.idle"),
|
||||
ProcessState::Other => t!("processes.state.misc"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use gpui_kit::*;
|
||||
use super::linux::{command_line, command_line_command};
|
||||
use super::model::{ProcessDetails, format_started};
|
||||
use crate::app::{CatalogIcon, EndProcess};
|
||||
use crate::i18n::t;
|
||||
use crate::shared::{format_bytes, format_duration, format_percent};
|
||||
use crate::terminal::{TerminalView, exec_answer};
|
||||
|
||||
@@ -74,7 +75,7 @@ pub fn open_process_dialog(
|
||||
.justify_between()
|
||||
.child(copy_button(
|
||||
"process-copy-pid",
|
||||
"复制 PID",
|
||||
t!("processes.details.copy_pid"),
|
||||
Some(pid.to_string()),
|
||||
this.copied == Some(Copied::Pid),
|
||||
Copied::Pid,
|
||||
@@ -87,13 +88,13 @@ pub fn open_process_dialog(
|
||||
end("process-end", false)
|
||||
.outline()
|
||||
.icon(Icon::new(CatalogIcon::CircleStop))
|
||||
.label("结束进程"),
|
||||
.label(t!("processes.details.end")),
|
||||
)
|
||||
.child(
|
||||
end("process-force-end", true)
|
||||
.danger()
|
||||
.icon(Icon::new(CatalogIcon::OctagonX))
|
||||
.label("强制结束"),
|
||||
.label(t!("processes.details.kill")),
|
||||
),
|
||||
);
|
||||
dialog
|
||||
@@ -148,15 +149,15 @@ impl ProcessDetailsView {
|
||||
});
|
||||
let reading = cx.spawn(async move |this, cx| {
|
||||
let command = match reply {
|
||||
None => CommandLine::Unknown("终端没有连接".into()),
|
||||
None => CommandLine::Unknown(t!("tools.not_connected").into()),
|
||||
Some(reply) => match exec_answer(reply, cx).await {
|
||||
None => CommandLine::Unknown("终端没有连接".into()),
|
||||
Some(Err(error)) => CommandLine::Unknown(format!("读取失败:{error}")),
|
||||
None => CommandLine::Unknown(t!("tools.not_connected").into()),
|
||||
Some(Err(error)) => {
|
||||
CommandLine::Unknown(t!("tools.read_failed", error = error).into())
|
||||
}
|
||||
Some(Ok(output)) => match command_line(&output) {
|
||||
Some(line) => CommandLine::Known(line),
|
||||
None => {
|
||||
CommandLine::Unknown("没有命令行:进程已经退出,或是僵尸进程".into())
|
||||
}
|
||||
None => CommandLine::Unknown(t!("processes.details.no_command").into()),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -193,7 +194,7 @@ impl ProcessDetailsView {
|
||||
/// 「复制命令」, which says 「已复制」 for a moment once it has.
|
||||
fn copy_button(
|
||||
id: &'static str,
|
||||
label: &'static str,
|
||||
label: SharedString,
|
||||
text: Option<String>,
|
||||
copied: bool,
|
||||
which: Copied,
|
||||
@@ -207,7 +208,11 @@ fn copy_button(
|
||||
} else {
|
||||
IconName::Copy
|
||||
}))
|
||||
.label(if copied { "已复制" } else { label })
|
||||
.label(if copied {
|
||||
t!("processes.details.copied")
|
||||
} else {
|
||||
label
|
||||
})
|
||||
.disabled(text.is_none())
|
||||
.on_click(move |_, _, cx| {
|
||||
if let Some(text) = text.clone() {
|
||||
@@ -221,38 +226,59 @@ impl Render for ProcessDetailsView {
|
||||
let details = &self.details;
|
||||
let process = &details.process;
|
||||
let info = &process.info;
|
||||
let fields: [(&str, String); 16] = [
|
||||
("PID", process.pid.to_string()),
|
||||
// Each label by its text's key; its line is found by the key's
|
||||
// last part: 「process-field:parent」.
|
||||
let none = || t!("processes.details.none").to_string();
|
||||
let fields: [(&'static str, String); 16] = [
|
||||
("processes.field.pid", process.pid.to_string()),
|
||||
(
|
||||
"父进程",
|
||||
details.parent.clone().unwrap_or_else(|| "无".into()),
|
||||
"processes.field.parent",
|
||||
details.parent.clone().unwrap_or_else(none),
|
||||
),
|
||||
("用户", process.user.clone().unwrap_or_else(|| "—".into())),
|
||||
(
|
||||
"状态",
|
||||
"processes.field.user",
|
||||
process.user.clone().unwrap_or_else(|| "—".into()),
|
||||
),
|
||||
(
|
||||
"processes.field.state",
|
||||
format!("{} ({})", process.state.label(), info.letters),
|
||||
),
|
||||
("启动时间", format_started(process.started)),
|
||||
("运行时长", format_duration(process.running)),
|
||||
("终端", info.terminal.clone().unwrap_or_else(|| "无".into())),
|
||||
("优先级", info.priority.to_string()),
|
||||
("Nice", info.nice.to_string()),
|
||||
("CPU 使用率", process.cpu.map_or("—".into(), format_percent)),
|
||||
("累计 CPU 时间", format_duration(process.cpu_time)),
|
||||
("processes.field.started", format_started(process.started)),
|
||||
("processes.field.running", format_duration(process.running)),
|
||||
(
|
||||
"常驻内存",
|
||||
"processes.field.terminal",
|
||||
info.terminal.clone().unwrap_or_else(none),
|
||||
),
|
||||
("processes.field.priority", info.priority.to_string()),
|
||||
("processes.field.nice", info.nice.to_string()),
|
||||
(
|
||||
"processes.field.cpu",
|
||||
process.cpu.map_or("—".into(), format_percent),
|
||||
),
|
||||
(
|
||||
"processes.field.cpu_time",
|
||||
format_duration(process.cpu_time),
|
||||
),
|
||||
(
|
||||
"processes.field.memory",
|
||||
format!(
|
||||
"{} ({})",
|
||||
format_bytes(process.memory),
|
||||
format_percent(process.memory_percent)
|
||||
),
|
||||
),
|
||||
("虚拟内存", format_bytes(info.virtual_memory)),
|
||||
("线程", info.threads.to_string()),
|
||||
("直接子进程", details.children.to_string()),
|
||||
("全部后代进程", details.descendants.to_string()),
|
||||
(
|
||||
"processes.field.virtual_memory",
|
||||
format_bytes(info.virtual_memory),
|
||||
),
|
||||
("processes.field.threads", info.threads.to_string()),
|
||||
("processes.field.children", details.children.to_string()),
|
||||
(
|
||||
"processes.field.descendants",
|
||||
details.descendants.to_string(),
|
||||
),
|
||||
];
|
||||
let heading = |text: &'static str| {
|
||||
let heading = |text: SharedString| {
|
||||
div()
|
||||
.text_sm()
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
@@ -268,49 +294,56 @@ impl Render for ProcessDetailsView {
|
||||
.test_support()
|
||||
.gap_5()
|
||||
.child(
|
||||
v_flex().gap_1().child(heading("进程信息")).child(
|
||||
div()
|
||||
.grid()
|
||||
.grid_cols(2)
|
||||
.gap_x_8()
|
||||
.children(fields.into_iter().map(|(label, value)| {
|
||||
h_flex()
|
||||
.id(SharedString::from(format!("process-field:{label}")))
|
||||
.test_support()
|
||||
.aria_label(value.clone())
|
||||
.min_w_0()
|
||||
.gap_3()
|
||||
.py_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.w(rems(6.5))
|
||||
.flex_shrink_0()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(label),
|
||||
)
|
||||
.child(div().flex_1().min_w_0().truncate().child(value))
|
||||
})),
|
||||
),
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(heading(t!("processes.details.info")))
|
||||
.child(
|
||||
div()
|
||||
.grid()
|
||||
.grid_cols(2)
|
||||
.gap_x_8()
|
||||
.children(fields.into_iter().map(|(label, value)| {
|
||||
let id = label.rsplit('.').next().unwrap_or(label);
|
||||
h_flex()
|
||||
.id(SharedString::from(format!("process-field:{id}")))
|
||||
.test_support()
|
||||
.aria_label(value.clone())
|
||||
.min_w_0()
|
||||
.gap_3()
|
||||
.py_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.w(rems(6.5))
|
||||
.flex_shrink_0()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(t!(label)),
|
||||
)
|
||||
.child(div().flex_1().min_w_0().truncate().child(value))
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex().justify_between().child(heading("完整命令")).child(
|
||||
copy_button(
|
||||
"process-copy-command",
|
||||
"复制命令",
|
||||
command_text,
|
||||
self.copied == Some(Copied::Command),
|
||||
Copied::Command,
|
||||
&view,
|
||||
)
|
||||
.ghost()
|
||||
.small(),
|
||||
),
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(heading(t!("processes.details.command")))
|
||||
.child(
|
||||
copy_button(
|
||||
"process-copy-command",
|
||||
t!("processes.details.copy_command"),
|
||||
command_text,
|
||||
self.copied == Some(Copied::Command),
|
||||
Copied::Command,
|
||||
&view,
|
||||
)
|
||||
.ghost()
|
||||
.small(),
|
||||
),
|
||||
)
|
||||
.child(match &self.command {
|
||||
CommandLine::Reading => h_flex()
|
||||
@@ -321,7 +354,7 @@ impl Render for ProcessDetailsView {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(Spinner::new().small())
|
||||
.child("正在读取…")
|
||||
.child(t!("tools.reading"))
|
||||
.into_any_element(),
|
||||
CommandLine::Known(line) => div()
|
||||
.id("process-command")
|
||||
|
||||
@@ -23,6 +23,7 @@ use super::model::{
|
||||
Order, Process, ProcessDetails, ProcessSort, ProcessState, Snapshot, Tracker, format_started,
|
||||
};
|
||||
use crate::app::{CatalogIcon, EndProcess, RefreshProcesses, ShowProcess, SortProcesses};
|
||||
use crate::i18n::{UiLocale, t, tn};
|
||||
use crate::shared::{format_bytes, format_percent};
|
||||
use crate::terminal::{ExecResult, ExecTarget, RemoteTerminalId, exec_answer};
|
||||
|
||||
@@ -88,10 +89,15 @@ impl ProcessPanel {
|
||||
pub fn new(dispatch: FocusHandle, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let search = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("搜索进程名称、PID 或用户")
|
||||
.placeholder(t!("processes.panel.search"))
|
||||
.clean_on_escape()
|
||||
});
|
||||
let subscriptions = vec![
|
||||
cx.observe_global_in::<UiLocale>(window, |this, window, cx| {
|
||||
this.search.update(cx, |search, cx| {
|
||||
search.set_placeholder(t!("processes.panel.search"), window, cx)
|
||||
});
|
||||
}),
|
||||
cx.subscribe(&search, |this, input, event: &InputEvent, cx| {
|
||||
if matches!(event, InputEvent::Change) {
|
||||
this.query = input.read(cx).value().to_string();
|
||||
@@ -252,10 +258,10 @@ impl ProcessPanel {
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let rows = Rc::new(snapshot.listed(&self.query, self.order));
|
||||
let summary = format!(
|
||||
"共 {} 个进程 · 每 {} 秒刷新",
|
||||
let summary = tn!(
|
||||
"processes.panel.summary",
|
||||
snapshot.processes().len(),
|
||||
INTERVAL.as_secs()
|
||||
seconds = INTERVAL.as_secs()
|
||||
);
|
||||
let refresh = self.dispatch.clone();
|
||||
let header = v_flex()
|
||||
@@ -285,8 +291,8 @@ impl ProcessPanel {
|
||||
.xsmall()
|
||||
.icon(Icon::new(CatalogIcon::RefreshCw))
|
||||
.loading(self.loading)
|
||||
.tooltip("刷新")
|
||||
.accessibility_label("刷新")
|
||||
.tooltip(t!("tools.refresh"))
|
||||
.accessibility_label(t!("tools.refresh"))
|
||||
.on_click(move |_, window, cx| {
|
||||
refresh.dispatch_action(&RefreshProcesses, window, cx)
|
||||
}),
|
||||
@@ -296,7 +302,7 @@ impl ProcessPanel {
|
||||
header.child(render_problem(&problem, cx))
|
||||
})
|
||||
.when(snapshot.truncated(), |header| {
|
||||
let text = format!("进程太多,只列出了前 {} 个。", linux::LIMIT);
|
||||
let text = t!("processes.panel.truncated", limit = linux::LIMIT);
|
||||
header.child(
|
||||
div()
|
||||
.id("processes-truncated")
|
||||
@@ -362,15 +368,15 @@ impl ProcessPanel {
|
||||
.map(|list| {
|
||||
if rows.is_empty() {
|
||||
let empty = if self.query.trim().is_empty() {
|
||||
"没有进程"
|
||||
t!("processes.panel.empty")
|
||||
} else {
|
||||
"没有符合条件的进程"
|
||||
t!("processes.panel.no_match")
|
||||
};
|
||||
list.child(
|
||||
div()
|
||||
.id("processes-empty")
|
||||
.test_support()
|
||||
.aria_label(empty)
|
||||
.aria_label(empty.clone())
|
||||
.py_8()
|
||||
.text_sm()
|
||||
.text_center()
|
||||
@@ -452,7 +458,7 @@ impl Render for ProcessPanel {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(Spinner::new().small())
|
||||
.child("正在读取…"),
|
||||
.child(t!("tools.reading")),
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -461,18 +467,18 @@ impl Render for ProcessPanel {
|
||||
fn render_problem(problem: &Problem, cx: &App) -> impl IntoElement {
|
||||
let (text, color) = match problem {
|
||||
Problem::NotConnected => (
|
||||
"终端没有连接。连接后这里显示主机的进程。".to_string(),
|
||||
t!("processes.panel.not_connected"),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Unsupported(Some(system)) => (
|
||||
format!("暂不支持管理 {system} 的进程,目前只支持 Linux 主机。"),
|
||||
t!("processes.panel.unsupported_system", system = system),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Unsupported(None) => (
|
||||
"暂不支持管理这台主机的进程,目前只支持 Linux 主机。".to_string(),
|
||||
t!("processes.panel.unsupported"),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Failed(error) => (format!("读取失败:{error}"), cx.theme().danger),
|
||||
Problem::Failed(error) => (t!("tools.read_failed", error = error), cx.theme().danger),
|
||||
};
|
||||
div()
|
||||
.id("processes-message")
|
||||
@@ -493,13 +499,15 @@ fn sort_button(
|
||||
dispatch: &FocusHandle,
|
||||
) -> impl IntoElement {
|
||||
let active = order.by == by;
|
||||
let label = match by {
|
||||
ProcessSort::Memory => "内存",
|
||||
ProcessSort::Cpu => "CPU",
|
||||
let label: SharedString = match by {
|
||||
ProcessSort::Memory => t!("processes.memory"),
|
||||
ProcessSort::Cpu => "CPU".into(),
|
||||
};
|
||||
let tooltip = match (active, order.descending) {
|
||||
(true, true) => format!("改为按{label}从少到多排列"),
|
||||
_ => format!("按{label}从多到少排列"),
|
||||
let tooltip = match (by, active && order.descending) {
|
||||
(ProcessSort::Memory, true) => t!("processes.sort.memory_ascending"),
|
||||
(ProcessSort::Memory, false) => t!("processes.sort.memory_descending"),
|
||||
(ProcessSort::Cpu, true) => t!("processes.sort.cpu_ascending"),
|
||||
(ProcessSort::Cpu, false) => t!("processes.sort.cpu_descending"),
|
||||
};
|
||||
let dispatch = dispatch.clone();
|
||||
Button::new(id)
|
||||
@@ -525,18 +533,18 @@ fn build_context_menu(hit: Option<u32>, menu: PopupMenu) -> PopupMenu {
|
||||
return menu;
|
||||
};
|
||||
menu.menu_with_icon(
|
||||
"查看详情",
|
||||
t!("tools.menu.details"),
|
||||
Icon::new(IconName::Info),
|
||||
Box::new(ShowProcess(pid)),
|
||||
)
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"结束进程…",
|
||||
t!("processes.menu.end"),
|
||||
Icon::new(CatalogIcon::CircleStop),
|
||||
Box::new(EndProcess { pid, force: false }),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"强制结束进程…",
|
||||
t!("processes.menu.kill"),
|
||||
Icon::new(CatalogIcon::OctagonX),
|
||||
Box::new(EndProcess { pid, force: true }),
|
||||
)
|
||||
@@ -573,12 +581,16 @@ fn render_process(
|
||||
format_percent(process.memory_percent)
|
||||
);
|
||||
let cpu = process.cpu.map_or("—".into(), format_percent);
|
||||
let label = format!(
|
||||
"{} · PID {pid} · {} · {user} · 内存 {memory} · CPU {cpu}",
|
||||
process.name,
|
||||
process.state.label(),
|
||||
let label = t!(
|
||||
"processes.card.label",
|
||||
name = process.name,
|
||||
pid = pid,
|
||||
state = process.state.label(),
|
||||
user = user,
|
||||
memory = memory,
|
||||
cpu = cpu
|
||||
);
|
||||
let state: SharedString = process.state.label().into();
|
||||
let state = process.state.label();
|
||||
let numeric = |text: String| {
|
||||
div()
|
||||
.font_family(cx.theme().mono_font_family.clone())
|
||||
@@ -652,7 +664,11 @@ fn render_process(
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.gap_1()
|
||||
.child(div().text_color(cx.theme().muted_foreground).child("内存"))
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(t!("processes.memory")),
|
||||
)
|
||||
.child(numeric(memory).text_color(cx.theme().info)),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::model::{Service, ServiceCommand, ServiceStatus, ServiceTable};
|
||||
use crate::i18n::t;
|
||||
|
||||
/// What the list asks `systemctl show` for each service.
|
||||
const LIST_PROPERTIES: [&str; 6] = [
|
||||
@@ -102,7 +103,7 @@ pub fn control_command(name: &str, command: ServiceCommand) -> Option<String> {
|
||||
pub fn controlled(output: &str) -> Result<(), String> {
|
||||
let (said, status) = output
|
||||
.rsplit_once("@@status")
|
||||
.ok_or_else(|| "主机没有回答".to_string())?;
|
||||
.ok_or_else(|| t!("tools.no_answer").to_string())?;
|
||||
if status.trim() == "0" {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -113,11 +114,11 @@ pub fn controlled(output: &str) -> Result<(), String> {
|
||||
|| said.contains("Access denied")
|
||||
|| said.contains("not in the sudoers")
|
||||
{
|
||||
"需要 root 权限:以 root 登录,或给这个用户配置免密码的 sudo".into()
|
||||
t!("services.error.needs_root").into()
|
||||
} else if said.contains("command not found") && said.contains("sudo") {
|
||||
"需要 root 权限:这台主机没有 sudo,请以 root 登录".into()
|
||||
t!("services.error.needs_root_no_sudo").into()
|
||||
} else if said.is_empty() {
|
||||
format!("systemctl 返回 {}", status.trim())
|
||||
t!("services.error.status", status = status.trim()).into()
|
||||
} else {
|
||||
said.lines().next().unwrap_or(said).trim().to_owned()
|
||||
},
|
||||
|
||||
+69
-75
@@ -1,6 +1,10 @@
|
||||
//! What 系统服务 reads off a host, and how its tabs, groups and search
|
||||
//! pick from it.
|
||||
|
||||
use gpui_kit::SharedString;
|
||||
|
||||
use crate::i18n::t;
|
||||
|
||||
/// Where a service's unit file lives, which the list groups by.
|
||||
const CUSTOM_UNITS: &str = "/etc/systemd/system/";
|
||||
|
||||
@@ -29,14 +33,14 @@ impl ActiveState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
pub fn label(self) -> SharedString {
|
||||
match self {
|
||||
ActiveState::Active => "运行中",
|
||||
ActiveState::Reloading => "正在重载",
|
||||
ActiveState::Inactive | ActiveState::Other => "已停止",
|
||||
ActiveState::Failed => "失败",
|
||||
ActiveState::Activating => "正在启动",
|
||||
ActiveState::Deactivating => "正在停止",
|
||||
ActiveState::Active => t!("services.active.active"),
|
||||
ActiveState::Reloading => t!("services.active.reloading"),
|
||||
ActiveState::Inactive | ActiveState::Other => t!("services.active.inactive"),
|
||||
ActiveState::Failed => t!("services.active.failed"),
|
||||
ActiveState::Activating => t!("services.active.activating"),
|
||||
ActiveState::Deactivating => t!("services.active.deactivating"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,70 +66,69 @@ pub enum ServiceKind {
|
||||
|
||||
/// The finer state under `ActiveState`, as systemd's `SubState` says: 「已退出」
|
||||
/// for a one-shot service that did its work.
|
||||
pub fn sub_state_label(state: &str) -> String {
|
||||
pub fn sub_state_label(state: &str) -> SharedString {
|
||||
match state {
|
||||
"running" => "运行中",
|
||||
"exited" => "已退出",
|
||||
"dead" => "未运行",
|
||||
"failed" => "失败",
|
||||
"waiting" => "等待中",
|
||||
"listening" => "监听中",
|
||||
"reload" => "正在重载",
|
||||
"auto-restart" => "等待自动重启",
|
||||
"condition" => "条件不满足",
|
||||
state if state.starts_with("start") => "正在启动",
|
||||
state if state.starts_with("stop") || state.starts_with("final") => "正在停止",
|
||||
state => return state.to_owned(),
|
||||
"running" => t!("services.sub.running"),
|
||||
"exited" => t!("services.sub.exited"),
|
||||
"dead" => t!("services.sub.dead"),
|
||||
"failed" => t!("services.sub.failed"),
|
||||
"waiting" => t!("services.sub.waiting"),
|
||||
"listening" => t!("services.sub.listening"),
|
||||
"reload" => t!("services.sub.reload"),
|
||||
"auto-restart" => t!("services.sub.auto_restart"),
|
||||
"condition" => t!("services.sub.condition"),
|
||||
state if state.starts_with("start") => t!("services.sub.starting"),
|
||||
state if state.starts_with("stop") || state.starts_with("final") => {
|
||||
t!("services.sub.stopping")
|
||||
}
|
||||
state => state.to_owned().into(),
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
/// Whether it starts at boot, as systemd's `UnitFileState` says; `None`
|
||||
/// with nothing to say.
|
||||
pub fn file_state_label(state: &str) -> Option<&'static str> {
|
||||
pub fn file_state_label(state: &str) -> Option<SharedString> {
|
||||
Some(match state {
|
||||
"enabled" => "已启用",
|
||||
"enabled-runtime" => "已启用(本次运行)",
|
||||
"disabled" => "已禁用",
|
||||
"static" => "静态",
|
||||
"masked" | "masked-runtime" => "已屏蔽",
|
||||
"generated" => "自动生成",
|
||||
"indirect" => "间接启用",
|
||||
"linked" | "linked-runtime" => "已链接",
|
||||
"transient" => "临时",
|
||||
"alias" => "别名",
|
||||
"bad" => "出错",
|
||||
"enabled" => t!("services.file.enabled"),
|
||||
"enabled-runtime" => t!("services.file.enabled_runtime"),
|
||||
"disabled" => t!("services.file.disabled"),
|
||||
"static" => t!("services.file.static"),
|
||||
"masked" | "masked-runtime" => t!("services.file.masked"),
|
||||
"generated" => t!("services.file.generated"),
|
||||
"indirect" => t!("services.file.indirect"),
|
||||
"linked" | "linked-runtime" => t!("services.file.linked"),
|
||||
"transient" => t!("services.file.transient"),
|
||||
"alias" => t!("services.file.alias"),
|
||||
"bad" => t!("services.file.bad"),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the unit file was found and read, as systemd's `LoadState` says.
|
||||
pub fn load_state_label(state: &str) -> String {
|
||||
pub fn load_state_label(state: &str) -> SharedString {
|
||||
match state {
|
||||
"loaded" => "已加载",
|
||||
"not-found" => "未找到",
|
||||
"masked" => "已屏蔽",
|
||||
"error" => "出错",
|
||||
"bad-setting" => "配置有误",
|
||||
state => return state.to_owned(),
|
||||
"loaded" => t!("services.load.loaded"),
|
||||
"not-found" => t!("services.load.not_found"),
|
||||
"masked" => t!("services.load.masked"),
|
||||
"error" => t!("services.load.error"),
|
||||
"bad-setting" => t!("services.load.bad_setting"),
|
||||
state => state.to_owned().into(),
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
/// How the machine as a whole is doing, as `systemctl is-system-running`
|
||||
/// says: 「降级运行」 when some unit failed.
|
||||
pub fn system_state_label(state: &str) -> String {
|
||||
pub fn system_state_label(state: &str) -> SharedString {
|
||||
match state {
|
||||
"running" => "运行正常",
|
||||
"degraded" => "降级运行",
|
||||
"starting" => "正在启动",
|
||||
"initializing" => "正在初始化",
|
||||
"maintenance" => "维护模式",
|
||||
"stopping" => "正在关机",
|
||||
"offline" => "离线",
|
||||
state => return state.to_owned(),
|
||||
"running" => t!("services.system.running"),
|
||||
"degraded" => t!("services.system.degraded"),
|
||||
"starting" => t!("services.system.starting"),
|
||||
"initializing" => t!("services.system.initializing"),
|
||||
"maintenance" => t!("services.system.maintenance"),
|
||||
"stopping" => t!("services.system.stopping"),
|
||||
"offline" => t!("services.system.offline"),
|
||||
state => state.to_owned().into(),
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
/// What can be done to a service.
|
||||
@@ -153,24 +156,13 @@ impl ServiceCommand {
|
||||
}
|
||||
|
||||
/// The button: 「停止」「禁用开机启动」.
|
||||
pub fn label(self) -> &'static str {
|
||||
pub fn label(self) -> SharedString {
|
||||
match self {
|
||||
ServiceCommand::Start => "启动",
|
||||
ServiceCommand::Stop => "停止",
|
||||
ServiceCommand::Restart => "重启",
|
||||
ServiceCommand::Enable => "启用开机启动",
|
||||
ServiceCommand::Disable => "禁用开机启动",
|
||||
}
|
||||
}
|
||||
|
||||
/// What it did, after the service's name: 「已停止」.
|
||||
pub fn done(self) -> &'static str {
|
||||
match self {
|
||||
ServiceCommand::Start => "已启动",
|
||||
ServiceCommand::Stop => "已停止",
|
||||
ServiceCommand::Restart => "已重启",
|
||||
ServiceCommand::Enable => "已设为开机启动",
|
||||
ServiceCommand::Disable => "已取消开机启动",
|
||||
ServiceCommand::Start => t!("services.command.start"),
|
||||
ServiceCommand::Stop => t!("services.command.stop"),
|
||||
ServiceCommand::Restart => t!("services.command.restart"),
|
||||
ServiceCommand::Enable => t!("services.command.enable"),
|
||||
ServiceCommand::Disable => t!("services.command.disable"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,12 +235,12 @@ impl ServiceFilter {
|
||||
ServiceFilter::Failed,
|
||||
];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
pub fn label(self) -> SharedString {
|
||||
match self {
|
||||
ServiceFilter::All => "全部",
|
||||
ServiceFilter::Running => "运行中",
|
||||
ServiceFilter::Stopped => "已停止",
|
||||
ServiceFilter::Failed => "失败",
|
||||
ServiceFilter::All => t!("services.filter.all"),
|
||||
ServiceFilter::Running => t!("services.filter.running"),
|
||||
ServiceFilter::Stopped => t!("services.filter.stopped"),
|
||||
ServiceFilter::Failed => t!("services.filter.failed"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +295,9 @@ impl ServiceTable {
|
||||
pub fn summary(&self) -> String {
|
||||
[
|
||||
self.version.clone(),
|
||||
self.state.as_deref().map(system_state_label),
|
||||
self.state
|
||||
.as_deref()
|
||||
.map(|state| system_state_label(state).into()),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -525,7 +519,7 @@ mod tests {
|
||||
assert_eq!(sub_state_label("exited"), "已退出");
|
||||
assert_eq!(sub_state_label("stop-sigterm"), "正在停止");
|
||||
assert_eq!(sub_state_label("mounted"), "mounted");
|
||||
assert_eq!(file_state_label("generated"), Some("自动生成"));
|
||||
assert_eq!(file_state_label("generated"), Some("自动生成".into()));
|
||||
assert_eq!(file_state_label(""), None);
|
||||
assert_eq!(load_state_label("loaded"), "已加载");
|
||||
assert_eq!(system_state_label("running"), "运行正常");
|
||||
|
||||
@@ -18,6 +18,7 @@ use super::model::{
|
||||
};
|
||||
use super::service_panel::{command_icon, state_color};
|
||||
use crate::app::{CatalogIcon, ControlService};
|
||||
use crate::i18n::t;
|
||||
use crate::shared::{format_bytes, soft_tag};
|
||||
use crate::terminal::{ExecResult, TerminalView, exec_answer};
|
||||
|
||||
@@ -131,7 +132,7 @@ impl ServiceDetailsView {
|
||||
this.status = match answer {
|
||||
Ok(output) => match parse_status(&output) {
|
||||
Some(status) => Read::Known(status),
|
||||
None => Read::Unknown("读不到这个服务的状态".into()),
|
||||
None => Read::Unknown(t!("services.details.unreadable").into()),
|
||||
},
|
||||
Err(why) => Read::Unknown(why),
|
||||
};
|
||||
@@ -173,7 +174,9 @@ impl ServiceDetailsView {
|
||||
cx,
|
||||
|this, answer, _| {
|
||||
this.journal = match answer {
|
||||
Ok(output) if output.trim().is_empty() => Read::Unknown("没有日志".into()),
|
||||
Ok(output) if output.trim().is_empty() => {
|
||||
Read::Unknown(t!("services.details.no_journal").into())
|
||||
}
|
||||
Ok(output) => Read::Known(output.trim_end().to_owned()),
|
||||
Err(why) => Read::Unknown(why),
|
||||
};
|
||||
@@ -193,34 +196,38 @@ impl ServiceDetailsView {
|
||||
};
|
||||
let active = ActiveState::parse(&status.active);
|
||||
let number = |value: Option<u64>| value.map_or("—".to_owned(), |value| value.to_string());
|
||||
let fields: [(&str, String); 10] = [
|
||||
("加载状态", load_state_label(&status.load)),
|
||||
let fields: [(&'static str, String); 10] = [
|
||||
("services.field.load", load_state_label(&status.load).into()),
|
||||
(
|
||||
"运行状态",
|
||||
"services.field.active",
|
||||
format!("{} / {}", active.label(), sub_state_label(&status.sub)),
|
||||
),
|
||||
(
|
||||
"开机启动",
|
||||
file_state_label(&status.file_state)
|
||||
.unwrap_or("—")
|
||||
.to_owned(),
|
||||
"services.field.boot",
|
||||
file_state_label(&status.file_state).map_or("—".into(), Into::into),
|
||||
),
|
||||
(
|
||||
"主进程 PID",
|
||||
"services.field.main_pid",
|
||||
status.main_pid.map_or("—".into(), |pid| pid.to_string()),
|
||||
),
|
||||
("内存", status.memory.map_or("—".into(), format_bytes)),
|
||||
("任务数", number(status.tasks)),
|
||||
("重启次数", number(status.restarts)),
|
||||
(
|
||||
"退出状态",
|
||||
"services.field.memory",
|
||||
status.memory.map_or("—".into(), format_bytes),
|
||||
),
|
||||
("services.field.tasks", number(status.tasks)),
|
||||
("services.field.restarts", number(status.restarts)),
|
||||
(
|
||||
"services.field.exit_status",
|
||||
status
|
||||
.exit_status
|
||||
.map_or("—".into(), |status| status.to_string()),
|
||||
),
|
||||
("启动时间", status.started.clone().unwrap_or("—".into())),
|
||||
(
|
||||
"停止时间",
|
||||
"services.field.started",
|
||||
status.started.clone().unwrap_or("—".into()),
|
||||
),
|
||||
(
|
||||
"services.field.stopped",
|
||||
// When it last went down; while it runs, that is history.
|
||||
status
|
||||
.stopped
|
||||
@@ -238,7 +245,7 @@ impl ServiceDetailsView {
|
||||
),
|
||||
)
|
||||
.child(field(
|
||||
"Unit 文件",
|
||||
"services.field.unit_file",
|
||||
status.path.clone().unwrap_or("—".into()),
|
||||
cx,
|
||||
))
|
||||
@@ -282,10 +289,10 @@ fn run(
|
||||
let reply = command.and_then(|command| terminal.upgrade()?.read(cx).exec(command, cx));
|
||||
cx.spawn(async move |this, cx| {
|
||||
let answer: ExecResult = match reply {
|
||||
None => Err("终端没有连接".into()),
|
||||
None => Err(t!("tools.not_connected").into()),
|
||||
Some(reply) => match exec_answer(reply, cx).await {
|
||||
None => Err("终端没有连接".into()),
|
||||
Some(Err(error)) => Err(format!("读取失败:{error}")),
|
||||
None => Err(t!("tools.not_connected").into()),
|
||||
Some(Err(error)) => Err(t!("tools.read_failed", error = error).into()),
|
||||
Some(Ok(output)) => Ok(output),
|
||||
},
|
||||
};
|
||||
@@ -297,10 +304,12 @@ fn run(
|
||||
})
|
||||
}
|
||||
|
||||
/// A label and its value, over a line.
|
||||
/// A label, by its text's key, and its value, over a line. The line is
|
||||
/// found by the last part of the key: 「service-field:main_pid」.
|
||||
fn field(label: &'static str, value: String, cx: &App) -> impl IntoElement {
|
||||
let id = label.rsplit('.').next().unwrap_or(label);
|
||||
h_flex()
|
||||
.id(SharedString::from(format!("service-field:{label}")))
|
||||
.id(SharedString::from(format!("service-field:{id}")))
|
||||
.test_support()
|
||||
.aria_label(value.clone())
|
||||
.min_w_0()
|
||||
@@ -314,7 +323,7 @@ fn field(label: &'static str, value: String, cx: &App) -> impl IntoElement {
|
||||
.w(rems(6.5))
|
||||
.flex_shrink_0()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(label),
|
||||
.child(t!(label)),
|
||||
)
|
||||
.child(div().flex_1().min_w_0().truncate().child(value))
|
||||
}
|
||||
@@ -329,7 +338,7 @@ fn reading(id: &'static str, cx: &App) -> AnyElement {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(Spinner::new().small())
|
||||
.child("正在读取…")
|
||||
.child(t!("tools.reading"))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -362,12 +371,12 @@ impl Render for ServiceDetailsView {
|
||||
.child(
|
||||
Tab::new()
|
||||
.icon(Icon::new(CatalogIcon::CircleDot))
|
||||
.label("状态"),
|
||||
.label(t!("services.details.tab_status")),
|
||||
)
|
||||
.child(
|
||||
Tab::new()
|
||||
.icon(Icon::new(CatalogIcon::FileText))
|
||||
.label("日志"),
|
||||
.label(t!("services.details.tab_journal")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ use super::model::{
|
||||
ActiveState, Service, ServiceCommand, ServiceFilter, ServiceRow, ServiceTable, file_state_label,
|
||||
};
|
||||
use crate::app::{CatalogIcon, ControlService, RefreshServices, ShowService};
|
||||
use crate::i18n::{UiLocale, t};
|
||||
use crate::shared::{soft_tag, tinted};
|
||||
use crate::terminal::{ExecResult, ExecTarget, RemoteTerminalId, exec_answer};
|
||||
|
||||
@@ -87,10 +88,15 @@ impl ServicePanel {
|
||||
pub fn new(dispatch: FocusHandle, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let search = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("搜索服务名称或描述")
|
||||
.placeholder(t!("services.panel.search"))
|
||||
.clean_on_escape()
|
||||
});
|
||||
let subscriptions = vec![
|
||||
cx.observe_global_in::<UiLocale>(window, |this, window, cx| {
|
||||
this.search.update(cx, |search, cx| {
|
||||
search.set_placeholder(t!("services.panel.search"), window, cx)
|
||||
});
|
||||
}),
|
||||
cx.subscribe(&search, |this, input, event: &InputEvent, cx| {
|
||||
if matches!(event, InputEvent::Change) {
|
||||
this.query = input.read(cx).value().to_string();
|
||||
@@ -267,8 +273,8 @@ impl ServicePanel {
|
||||
.xsmall()
|
||||
.icon(Icon::new(CatalogIcon::RefreshCw))
|
||||
.loading(self.loading)
|
||||
.tooltip("刷新")
|
||||
.accessibility_label("刷新")
|
||||
.tooltip(t!("tools.refresh"))
|
||||
.accessibility_label(t!("tools.refresh"))
|
||||
.on_click(move |_, window, cx| {
|
||||
refresh.dispatch_action(&RefreshServices, window, cx)
|
||||
}),
|
||||
@@ -324,9 +330,9 @@ impl ServicePanel {
|
||||
let clear_hit = self.menu_hit.clone();
|
||||
let menu_hit = self.menu_hit.clone();
|
||||
let empty = if self.query.trim().is_empty() {
|
||||
"没有服务"
|
||||
t!("services.panel.empty")
|
||||
} else {
|
||||
"没有符合条件的服务"
|
||||
t!("services.panel.no_match")
|
||||
};
|
||||
// The scrollbar goes on the list's box, which does not scroll: on
|
||||
// the scrolled lines it would scroll away with them. The menu hangs
|
||||
@@ -344,7 +350,7 @@ impl ServicePanel {
|
||||
div()
|
||||
.id("services-empty")
|
||||
.test_support()
|
||||
.aria_label(empty)
|
||||
.aria_label(empty.clone())
|
||||
.py_8()
|
||||
.text_sm()
|
||||
.text_center()
|
||||
@@ -427,7 +433,7 @@ impl Render for ServicePanel {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(Spinner::new().small())
|
||||
.child("正在读取…")
|
||||
.child(t!("tools.reading"))
|
||||
.into_any_element(),
|
||||
};
|
||||
v_flex()
|
||||
@@ -442,22 +448,21 @@ impl Render for ServicePanel {
|
||||
fn render_problem(problem: &Problem, cx: &App) -> impl IntoElement {
|
||||
let (text, color) = match problem {
|
||||
Problem::NotConnected => (
|
||||
"终端没有连接。连接后这里显示主机的系统服务。".to_string(),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Unsupported(Some(system)) if system == "Linux" => (
|
||||
"这台主机没有使用 systemd,暂不支持管理它的服务。".to_string(),
|
||||
t!("services.panel.not_connected"),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Unsupported(Some(system)) if system == "Linux" => {
|
||||
(t!("services.panel.no_systemd"), cx.theme().muted_foreground)
|
||||
}
|
||||
Problem::Unsupported(Some(system)) => (
|
||||
format!("暂不支持管理 {system} 的服务,目前只支持使用 systemd 的 Linux 主机。"),
|
||||
t!("services.panel.unsupported_system", system = system),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Unsupported(None) => (
|
||||
"暂不支持管理这台主机的服务,目前只支持使用 systemd 的 Linux 主机。".to_string(),
|
||||
t!("services.panel.unsupported"),
|
||||
cx.theme().muted_foreground,
|
||||
),
|
||||
Problem::Failed(error) => (format!("读取失败:{error}"), cx.theme().danger),
|
||||
Problem::Failed(error) => (t!("tools.read_failed", error = error), cx.theme().danger),
|
||||
};
|
||||
div()
|
||||
.id("services-message")
|
||||
@@ -480,7 +485,7 @@ fn build_context_menu(hit: Option<&Service>, menu: PopupMenu) -> PopupMenu {
|
||||
};
|
||||
let menu = menu
|
||||
.menu_with_icon(
|
||||
"查看详情",
|
||||
t!("tools.menu.details"),
|
||||
Icon::new(IconName::Info),
|
||||
Box::new(ShowService(service.name.clone())),
|
||||
)
|
||||
@@ -521,9 +526,9 @@ pub(super) fn state_color(state: ActiveState, cx: &App) -> Hsla {
|
||||
/// 「自定义服务 3」.
|
||||
fn render_group(custom: bool, count: usize, cx: &App) -> impl IntoElement {
|
||||
let label = if custom {
|
||||
"自定义服务"
|
||||
t!("services.group.custom")
|
||||
} else {
|
||||
"系统服务"
|
||||
t!("services.group.system")
|
||||
};
|
||||
h_flex()
|
||||
.id(if custom {
|
||||
@@ -599,10 +604,10 @@ fn render_service(
|
||||
let color = state_color(service.active, cx);
|
||||
let boot = file_state_label(&service.file_state);
|
||||
let label = [
|
||||
Some(service.name.as_str()),
|
||||
Some(service.description.as_str()).filter(|text| !text.is_empty()),
|
||||
Some(SharedString::from(service.name.clone())),
|
||||
Some(SharedString::from(service.description.clone())).filter(|text| !text.is_empty()),
|
||||
Some(service.active.label()),
|
||||
boot,
|
||||
boot.clone(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -663,9 +668,9 @@ fn render_service(
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(if service.description.is_empty() {
|
||||
"无描述".to_owned()
|
||||
t!("services.panel.no_description")
|
||||
} else {
|
||||
service.description.clone()
|
||||
service.description.clone().into()
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
@@ -703,8 +708,8 @@ fn render_service(
|
||||
.ghost()
|
||||
.small()
|
||||
.icon(IconName::Ellipsis)
|
||||
.tooltip("详情和日志")
|
||||
.accessibility_label("详情和日志")
|
||||
.tooltip(t!("tools.details_and_logs"))
|
||||
.accessibility_label(t!("tools.details_and_logs"))
|
||||
.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
dispatch.dispatch_action(&ShowService(name.clone()), window, cx)
|
||||
|
||||
@@ -10,11 +10,9 @@ use gpui_kit::component::{
|
||||
use gpui_kit::*;
|
||||
|
||||
use crate::host::{HostStore, SnippetCategoryId, SnippetDraft, SnippetId, SnippetScope, by_name};
|
||||
use crate::i18n::t;
|
||||
use crate::shared::{commit_footer, dismiss_form_error, form_error_notification};
|
||||
|
||||
/// The category list's row for a snippet of none.
|
||||
const NO_CATEGORY: &str = "未分类";
|
||||
|
||||
/// How wide the snippet dialog is: room for a command of a fair length on
|
||||
/// one line.
|
||||
const DIALOG_WIDTH: f32 = 36.;
|
||||
@@ -71,7 +69,8 @@ impl SnippetForm {
|
||||
};
|
||||
categories.sort_by(|a, b| by_name((&a.1, a.0.0), (&b.1, b.0.0)));
|
||||
let mut category_ids: Vec<Option<SnippetCategoryId>> = vec![None];
|
||||
let mut labels: Vec<SharedString> = vec![NO_CATEGORY.into()];
|
||||
// The category list's row for a snippet of none.
|
||||
let mut labels: Vec<SharedString> = vec![t!("snippets.uncategorized")];
|
||||
for (id, name) in categories {
|
||||
category_ids.push(Some(id));
|
||||
labels.push(name);
|
||||
@@ -83,7 +82,7 @@ impl SnippetForm {
|
||||
|
||||
let name = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("例如 查看容器")
|
||||
.placeholder(t!("snippets.dialog.name_placeholder"))
|
||||
.default_value(draft.name.clone())
|
||||
});
|
||||
let category =
|
||||
@@ -91,7 +90,7 @@ impl SnippetForm {
|
||||
let command = cx.new(|cx| {
|
||||
TextareaState::new(window, cx)
|
||||
.auto_grow(4, 12)
|
||||
.placeholder("例如 docker ps -a")
|
||||
.placeholder(t!("snippets.dialog.command_placeholder"))
|
||||
.default_value(draft.command.clone())
|
||||
});
|
||||
Self {
|
||||
@@ -146,12 +145,12 @@ impl Render for SnippetForm {
|
||||
Form::new()
|
||||
.child(
|
||||
Field::new()
|
||||
.label("名称")
|
||||
.label(t!("snippets.dialog.name"))
|
||||
.required(true)
|
||||
.child(Input::new(&self.name).id("snippet-name").small()),
|
||||
)
|
||||
.child(
|
||||
Field::new().label("分类").child(
|
||||
Field::new().label(t!("snippets.dialog.category")).child(
|
||||
div()
|
||||
.id("snippet-category")
|
||||
.test_support()
|
||||
@@ -161,9 +160,9 @@ impl Render for SnippetForm {
|
||||
)
|
||||
.child(
|
||||
Field::new()
|
||||
.label("命令")
|
||||
.label(t!("snippets.dialog.command"))
|
||||
.required(true)
|
||||
.description("可以有多行。")
|
||||
.description(t!("snippets.dialog.command_description"))
|
||||
.child(
|
||||
div().id("snippet-command").test_support().w_full().child(
|
||||
Textarea::new(&self.command)
|
||||
@@ -175,7 +174,7 @@ impl Render for SnippetForm {
|
||||
.child(
|
||||
Field::new().child(
|
||||
Checkbox::new("snippet-run-on-click")
|
||||
.label("点击时自动执行")
|
||||
.label(t!("snippets.dialog.run_on_click"))
|
||||
.checked(self.run_on_click)
|
||||
.small()
|
||||
.on_click(cx.listener(|this, checked: &bool, _, cx| {
|
||||
@@ -186,7 +185,7 @@ impl Render for SnippetForm {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("启用后将自动执行命令,否则只将内容填入终端"),
|
||||
.child(t!("snippets.dialog.run_on_click_description")),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -203,9 +202,12 @@ pub fn open_snippet_dialog(
|
||||
let form = cx.new(|cx| SnippetForm::new(dialog, store, window, cx));
|
||||
let editing = form.read(cx).editing.is_some();
|
||||
let (title, commit): (SharedString, SharedString) = if editing {
|
||||
("编辑命令片段".into(), "保存".into())
|
||||
(t!("snippets.dialog.edit_title"), t!("common.save"))
|
||||
} else {
|
||||
("新建命令片段".into(), "创建".into())
|
||||
(
|
||||
t!("snippets.dialog.new_title"),
|
||||
t!("snippets.dialog.create"),
|
||||
)
|
||||
};
|
||||
window.open_dialog(cx, {
|
||||
let form = form.clone();
|
||||
@@ -262,7 +264,7 @@ impl CategoryForm {
|
||||
};
|
||||
let name = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("例如 Docker")
|
||||
.placeholder(t!("snippets.category.name_placeholder"))
|
||||
.default_value(current)
|
||||
});
|
||||
Self {
|
||||
@@ -282,9 +284,9 @@ impl CategoryForm {
|
||||
.iter()
|
||||
.any(|category| category.name == name.as_str() && Some(category.id) != editing);
|
||||
let error = if name.is_empty() {
|
||||
Some("请输入分类名称")
|
||||
Some(t!("snippets.category.name_missing"))
|
||||
} else if taken {
|
||||
Some("已有同名分类")
|
||||
Some(t!("snippets.category.name_taken"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -308,7 +310,7 @@ impl Render for CategoryForm {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
Form::new().child(
|
||||
Field::new()
|
||||
.label("名称")
|
||||
.label(t!("snippets.category.name"))
|
||||
.required(true)
|
||||
.child(Input::new(&self.name).id("snippet-category-name").small()),
|
||||
)
|
||||
@@ -325,9 +327,12 @@ pub fn open_category_dialog(
|
||||
let form = cx.new(|cx| CategoryForm::new(dialog, store, window, cx));
|
||||
let renaming = form.read(cx).editing.is_some();
|
||||
let (title, commit): (SharedString, SharedString) = if renaming {
|
||||
("重命名分类".into(), "保存".into())
|
||||
(t!("snippets.category.rename_title"), t!("common.save"))
|
||||
} else {
|
||||
("新建分类".into(), "创建".into())
|
||||
(
|
||||
t!("snippets.category.new_title"),
|
||||
t!("snippets.dialog.create"),
|
||||
)
|
||||
};
|
||||
window.open_dialog(cx, {
|
||||
let form = form.clone();
|
||||
|
||||
@@ -20,11 +20,9 @@ use crate::app::{
|
||||
NewSnippet, NewSnippetCategory, NewSnippetIn, RenameSnippetCategory, ToggleSnippetCategory,
|
||||
};
|
||||
use crate::host::{HostStore, Snippet, SnippetCategoryId, SnippetId};
|
||||
use crate::i18n::{UiLocale, t, tn};
|
||||
use crate::shared::{command_tooltip, one_line};
|
||||
|
||||
/// What 未分类 is called, as a category's heading.
|
||||
const UNCATEGORIZED: &str = "未分类";
|
||||
|
||||
/// 命令片段: the commands kept for every host, in categories one level
|
||||
/// deep. A click puts one on the input line of the SSH terminal in front to
|
||||
/// edit; 执行 runs it there.
|
||||
@@ -68,11 +66,16 @@ impl SnippetPanel {
|
||||
) -> Self {
|
||||
let search = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("搜索名称或命令")
|
||||
.placeholder(t!("snippets.panel.search"))
|
||||
.clean_on_escape()
|
||||
});
|
||||
let subscriptions = vec![
|
||||
cx.observe(&store, |_, _, cx| cx.notify()),
|
||||
cx.observe_global_in::<UiLocale>(window, |this, window, cx| {
|
||||
this.search.update(cx, |search, cx| {
|
||||
search.set_placeholder(t!("snippets.panel.search"), window, cx)
|
||||
});
|
||||
}),
|
||||
cx.subscribe(&search, |this, input, event: &InputEvent, cx| {
|
||||
if matches!(event, InputEvent::Change) {
|
||||
this.query = input.read(cx).value().to_string();
|
||||
@@ -116,10 +119,15 @@ impl SnippetPanel {
|
||||
}
|
||||
|
||||
fn render_header(&self, snippets: usize, categories: usize, cx: &App) -> impl IntoElement {
|
||||
let summary = if categories == 0 {
|
||||
format!("共 {snippets} 个片段")
|
||||
let summary: SharedString = if categories == 0 {
|
||||
tn!("snippets.panel.count", snippets)
|
||||
} else {
|
||||
format!("共 {snippets} 个片段 · {categories} 个分类")
|
||||
format!(
|
||||
"{} · {}",
|
||||
tn!("snippets.panel.count", snippets),
|
||||
tn!("snippets.panel.categories", categories)
|
||||
)
|
||||
.into()
|
||||
};
|
||||
let (new_category, new_snippet) = (self.dispatch.clone(), self.dispatch.clone());
|
||||
v_flex()
|
||||
@@ -148,8 +156,8 @@ impl SnippetPanel {
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(Icon::new(IconName::Plus))
|
||||
.tooltip("新建片段")
|
||||
.accessibility_label("新建片段")
|
||||
.tooltip(t!("snippets.panel.new_snippet"))
|
||||
.accessibility_label(t!("snippets.panel.new_snippet"))
|
||||
.on_click(move |_, window, cx| {
|
||||
new_snippet.dispatch_action(&NewSnippet, window, cx)
|
||||
}),
|
||||
@@ -159,8 +167,8 @@ impl SnippetPanel {
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(Icon::new(CatalogIcon::FolderPlus))
|
||||
.tooltip("新建分类")
|
||||
.accessibility_label("新建分类")
|
||||
.tooltip(t!("snippets.panel.new_category"))
|
||||
.accessibility_label(t!("snippets.panel.new_category"))
|
||||
.on_click(move |_, window, cx| {
|
||||
new_category.dispatch_action(&NewSnippetCategory, window, cx)
|
||||
}),
|
||||
@@ -182,18 +190,18 @@ impl SnippetPanel {
|
||||
v_flex()
|
||||
.id("snippets-empty")
|
||||
.test_support()
|
||||
.aria_label("还没有命令片段")
|
||||
.aria_label(t!("snippets.panel.empty"))
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_4()
|
||||
.py_8()
|
||||
.child(div().text_sm().child("还没有命令片段"))
|
||||
.child(div().text_sm().child(t!("snippets.panel.empty")))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("把常用的命令存成片段,点一下就输入到当前终端。"),
|
||||
.child(t!("snippets.panel.empty_description")),
|
||||
)
|
||||
.child(
|
||||
Button::new("snippets-empty-new")
|
||||
@@ -201,7 +209,7 @@ impl SnippetPanel {
|
||||
.primary()
|
||||
.mt_2()
|
||||
.icon(Icon::new(IconName::Plus))
|
||||
.label("新建片段")
|
||||
.label(t!("snippets.panel.new_snippet"))
|
||||
.on_click(move |_, window, cx| {
|
||||
dispatch.dispatch_action(&NewSnippet, window, cx)
|
||||
}),
|
||||
@@ -255,12 +263,12 @@ impl Render for SnippetPanel {
|
||||
if nothing {
|
||||
list.child(self.render_empty(cx))
|
||||
} else if rows.is_empty() {
|
||||
let empty = "没有符合条件的片段";
|
||||
let empty = t!("snippets.panel.no_match");
|
||||
list.child(
|
||||
div()
|
||||
.id("snippets-no-match")
|
||||
.test_support()
|
||||
.aria_label(empty)
|
||||
.aria_label(empty.clone())
|
||||
.py_8()
|
||||
.text_sm()
|
||||
.text_center()
|
||||
@@ -281,7 +289,10 @@ impl Render for SnippetPanel {
|
||||
SnippetRow::Category { id, count, folded } => {
|
||||
let name = id
|
||||
.and_then(|id| names.iter().find(|(each, _)| *each == id))
|
||||
.map_or(UNCATEGORIZED.into(), |(_, name)| name.clone());
|
||||
.map_or_else(
|
||||
|| t!("snippets.uncategorized"),
|
||||
|(_, name)| name.clone(),
|
||||
);
|
||||
render_category(
|
||||
Heading {
|
||||
id,
|
||||
@@ -354,24 +365,28 @@ impl Render for SnippetPanel {
|
||||
/// is then.
|
||||
fn build_context_menu(hit: Option<MenuHit>, store: &HostStore, menu: PopupMenu) -> PopupMenu {
|
||||
let new_snippet = |menu: PopupMenu, action: Box<dyn Action>| {
|
||||
menu.menu_with_icon("新建片段…", Icon::new(IconName::Plus), action)
|
||||
menu.menu_with_icon(
|
||||
t!("snippets.menu.new_snippet"),
|
||||
Icon::new(IconName::Plus),
|
||||
action,
|
||||
)
|
||||
};
|
||||
match hit {
|
||||
None => new_snippet(menu, Box::new(NewSnippet)).menu_with_icon(
|
||||
"新建分类…",
|
||||
t!("snippets.menu.new_category"),
|
||||
Icon::new(CatalogIcon::FolderPlus),
|
||||
Box::new(NewSnippetCategory),
|
||||
),
|
||||
Some(MenuHit::Category(None)) => new_snippet(menu, Box::new(NewSnippet)),
|
||||
Some(MenuHit::Category(Some(id))) => new_snippet(menu, Box::new(NewSnippetIn(id)))
|
||||
.menu_with_icon(
|
||||
"重命名分类…",
|
||||
t!("snippets.menu.rename_category"),
|
||||
Icon::new(CatalogIcon::Pencil),
|
||||
Box::new(RenameSnippetCategory(id)),
|
||||
)
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"删除分类…",
|
||||
t!("snippets.menu.delete_category"),
|
||||
Icon::new(CatalogIcon::Trash),
|
||||
Box::new(DeleteSnippetCategory(id)),
|
||||
),
|
||||
@@ -381,7 +396,7 @@ fn build_context_menu(hit: Option<MenuHit>, store: &HostStore, menu: PopupMenu)
|
||||
};
|
||||
let command = snippet.command.clone();
|
||||
menu.menu_with_icon(
|
||||
"输入到终端",
|
||||
t!("tools.menu.insert"),
|
||||
Icon::new(CatalogIcon::SquareTerminal),
|
||||
Box::new(EnterCommand {
|
||||
command: command.clone(),
|
||||
@@ -389,7 +404,7 @@ fn build_context_menu(hit: Option<MenuHit>, store: &HostStore, menu: PopupMenu)
|
||||
}),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"执行",
|
||||
t!("tools.run"),
|
||||
Icon::new(CatalogIcon::Play),
|
||||
Box::new(EnterCommand {
|
||||
command: command.clone(),
|
||||
@@ -397,18 +412,18 @@ fn build_context_menu(hit: Option<MenuHit>, store: &HostStore, menu: PopupMenu)
|
||||
}),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"复制命令",
|
||||
t!("snippets.menu.copy"),
|
||||
Icon::new(IconName::Copy),
|
||||
Box::new(CopyCommand(command)),
|
||||
)
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"编辑…",
|
||||
t!("snippets.menu.edit"),
|
||||
Icon::new(CatalogIcon::Pencil),
|
||||
Box::new(EditSnippet(id)),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"删除…",
|
||||
t!("snippets.menu.delete"),
|
||||
Icon::new(CatalogIcon::Trash),
|
||||
Box::new(DeleteSnippet(id)),
|
||||
)
|
||||
@@ -520,7 +535,11 @@ fn render_snippet(
|
||||
.test_support()
|
||||
.group(card.clone())
|
||||
.aria_label(if runs {
|
||||
format!("{} · {line} · 点击时自动执行", snippet.name)
|
||||
format!(
|
||||
"{} · {line} · {}",
|
||||
snippet.name,
|
||||
t!("snippets.card.runs_on_click")
|
||||
)
|
||||
} else {
|
||||
format!("{} · {line}", snippet.name)
|
||||
})
|
||||
@@ -587,12 +606,14 @@ fn render_snippet(
|
||||
.icon(
|
||||
Icon::new(CatalogIcon::Zap).text_color(cx.theme().warning),
|
||||
)
|
||||
.tooltip("点击时自动执行")
|
||||
.tooltip(t!("snippets.card.runs_on_click"))
|
||||
} else {
|
||||
button.icon(Icon::new(CatalogIcon::Play)).tooltip("执行")
|
||||
button
|
||||
.icon(Icon::new(CatalogIcon::Play))
|
||||
.tooltip(t!("tools.run"))
|
||||
}
|
||||
})
|
||||
.accessibility_label("执行")
|
||||
.accessibility_label(t!("tools.run"))
|
||||
.on_click(move |_, window, cx| {
|
||||
// Not the card's click too, which would run it
|
||||
// again or only type it.
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::app::{CatalogIcon, DOCKER_ICON, ToggleTool, ToolKind};
|
||||
use crate::docker::{Container, DockerObject, DockerPanel, ObjectSummary};
|
||||
use crate::history::HistoryPanel;
|
||||
use crate::host::{HostId, HostOs, HostStore, SnippetCategoryId};
|
||||
use crate::i18n::t;
|
||||
use crate::monitor::{MonitorDetail, MonitorPanel};
|
||||
use crate::netstat::NetstatPanel;
|
||||
use crate::processes::{Process, ProcessDetails, ProcessPanel, ProcessSort};
|
||||
@@ -19,15 +20,15 @@ use crate::snippets::SnippetPanel;
|
||||
use crate::terminal::{ExecTarget, RemoteTerminalId, TerminalView};
|
||||
|
||||
impl ToolKind {
|
||||
pub fn label(self) -> &'static str {
|
||||
pub fn label(self) -> SharedString {
|
||||
match self {
|
||||
ToolKind::Snippets => "命令片段",
|
||||
ToolKind::History => "历史命令",
|
||||
ToolKind::Docker => "Docker",
|
||||
ToolKind::Services => "系统服务",
|
||||
ToolKind::Processes => "进程管理",
|
||||
ToolKind::Connections => "网络连接",
|
||||
ToolKind::Monitor => "系统监控",
|
||||
ToolKind::Snippets => t!("tools.kind.snippets"),
|
||||
ToolKind::History => t!("tools.kind.history"),
|
||||
ToolKind::Docker => "Docker".into(),
|
||||
ToolKind::Services => t!("tools.kind.services"),
|
||||
ToolKind::Processes => t!("tools.kind.processes"),
|
||||
ToolKind::Connections => t!("tools.kind.connections"),
|
||||
ToolKind::Monitor => t!("tools.kind.monitor"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+194
-72
@@ -25,9 +25,11 @@ use crate::app::{
|
||||
ToggleDockerProject, ToggleMonitorDetail, ToggleTool, ToggleToolSidebar, ToolKind,
|
||||
};
|
||||
use crate::docker::{
|
||||
self, ContainerCommand, DockerObject, open_container_dialog, open_object_dialog,
|
||||
self, ContainerCommand, ContainerSubject, DockerObject, open_container_dialog,
|
||||
open_object_dialog,
|
||||
};
|
||||
use crate::host::HostOs;
|
||||
use crate::i18n::t;
|
||||
use crate::processes::{end_command, ended, open_process_dialog};
|
||||
use crate::services::{ServiceCommand, control_command, controlled, open_service_dialog};
|
||||
use crate::shared::confirm_danger;
|
||||
@@ -226,8 +228,12 @@ impl Workspace {
|
||||
});
|
||||
}
|
||||
Err(why) => {
|
||||
let verb = if *run { "执行" } else { "输入" };
|
||||
window.push_notification(Notification::error(format!("无法{verb}命令:{why}")), cx);
|
||||
let message = if *run {
|
||||
t!("tools.command.run_failed", error = why)
|
||||
} else {
|
||||
t!("tools.command.enter_failed", error = why)
|
||||
};
|
||||
window.push_notification(Notification::error(message), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,7 +246,7 @@ impl Workspace {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(action.0.clone()));
|
||||
window.push_notification(Notification::success("已复制命令"), cx);
|
||||
window.push_notification(Notification::success(t!("tools.command.copied")), cx);
|
||||
}
|
||||
|
||||
/// Docker's 刷新: read the host's Docker again.
|
||||
@@ -307,12 +313,13 @@ impl Workspace {
|
||||
let run = Rc::new({
|
||||
let subject = subject.clone();
|
||||
move |window: &mut Window, cx: &mut App| {
|
||||
let failed = subject.clone();
|
||||
run_on_host(
|
||||
HostCommand {
|
||||
script: docker::control_command(&ids, command),
|
||||
outcome: docker::done,
|
||||
done: format!("{subject}{}", command.done()),
|
||||
failed: format!("无法{}{subject}", command.label()),
|
||||
done: containers_done(&subject, command),
|
||||
failed: Box::new(move |why| containers_failed(&failed, command, why)),
|
||||
then: ToolSidebar::refresh_docker,
|
||||
},
|
||||
&terminal,
|
||||
@@ -322,19 +329,26 @@ impl Workspace {
|
||||
)
|
||||
}
|
||||
});
|
||||
let description = match command {
|
||||
ContainerCommand::Start => return run(window, cx),
|
||||
ContainerCommand::Stop => "停止后它提供的服务就不可用了,直到再次启动。",
|
||||
ContainerCommand::Restart => "会先停止再启动,中间短暂不可用。",
|
||||
let (title, description) = match (&subject, command) {
|
||||
(_, ContainerCommand::Start) => return run(window, cx),
|
||||
(ContainerSubject::Container(name), ContainerCommand::Stop) => (
|
||||
t!("tools.container.stop.title", name = name),
|
||||
t!("tools.container.stop.description"),
|
||||
),
|
||||
(ContainerSubject::Project(name), ContainerCommand::Stop) => (
|
||||
t!("tools.project.stop.title", name = name),
|
||||
t!("tools.project.stop.description"),
|
||||
),
|
||||
(ContainerSubject::Container(name), ContainerCommand::Restart) => (
|
||||
t!("tools.container.restart.title", name = name),
|
||||
t!("tools.container.restart.description"),
|
||||
),
|
||||
(ContainerSubject::Project(name), ContainerCommand::Restart) => (
|
||||
t!("tools.project.restart.title", name = name),
|
||||
t!("tools.project.restart.description"),
|
||||
),
|
||||
};
|
||||
confirm_danger(
|
||||
format!("{}{subject}?", command.label()).into(),
|
||||
Some(description.into()),
|
||||
command.label(),
|
||||
run,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
confirm_danger(title, Some(description), command.label(), run, window, cx);
|
||||
}
|
||||
|
||||
/// Ask, then remove a container, an image, a volume or a network.
|
||||
@@ -348,25 +362,37 @@ impl Workspace {
|
||||
return;
|
||||
};
|
||||
let RemoveDockerObject { object, id, name } = action.clone();
|
||||
let subject = format!("{}“{name}”", object.label());
|
||||
let description = match object {
|
||||
DockerObject::Container => "容器和它里面没有存进卷的数据会一起删除,不能恢复。",
|
||||
DockerObject::Image => "删除后再要用它,需要重新拉取或构建。",
|
||||
DockerObject::Volume => "卷里的数据会一起删除,不能恢复。",
|
||||
DockerObject::Network => "删除后再要用它,需要重新创建。",
|
||||
let (title, description) = match object {
|
||||
DockerObject::Container => (
|
||||
t!("tools.remove.container.title", name = name),
|
||||
t!("tools.remove.container.description"),
|
||||
),
|
||||
DockerObject::Image => (
|
||||
t!("tools.remove.image.title", name = name),
|
||||
t!("tools.remove.image.description"),
|
||||
),
|
||||
DockerObject::Volume => (
|
||||
t!("tools.remove.volume.title", name = name),
|
||||
t!("tools.remove.volume.description"),
|
||||
),
|
||||
DockerObject::Network => (
|
||||
t!("tools.remove.network.title", name = name),
|
||||
t!("tools.remove.network.description"),
|
||||
),
|
||||
};
|
||||
let workspace = cx.entity().downgrade();
|
||||
confirm_danger(
|
||||
format!("删除{subject}?").into(),
|
||||
Some(description.into()),
|
||||
"删除",
|
||||
title,
|
||||
Some(description),
|
||||
t!("common.delete"),
|
||||
Rc::new(move |window, cx| {
|
||||
let failed = name.clone();
|
||||
run_on_host(
|
||||
HostCommand {
|
||||
script: docker::remove_command(object, &id),
|
||||
outcome: docker::done,
|
||||
done: format!("{subject}已删除"),
|
||||
failed: format!("无法删除{subject}"),
|
||||
done: removed(object, &name),
|
||||
failed: Box::new(move |why| not_removed(object, &failed, why)),
|
||||
then: ToolSidebar::refresh_docker,
|
||||
},
|
||||
&terminal,
|
||||
@@ -430,12 +456,13 @@ impl Workspace {
|
||||
let run = Rc::new({
|
||||
let name = name.clone();
|
||||
move |window: &mut Window, cx: &mut App| {
|
||||
let failed = name.clone();
|
||||
run_on_host(
|
||||
HostCommand {
|
||||
script: control_command(&name, command),
|
||||
outcome: controlled,
|
||||
done: format!("{name} {}", command.done()),
|
||||
failed: format!("无法{} {name}", command.label()),
|
||||
done: service_done(&name, command),
|
||||
failed: Box::new(move |why| service_failed(&failed, command, why)),
|
||||
then: ToolSidebar::refresh_services,
|
||||
},
|
||||
&terminal,
|
||||
@@ -450,27 +477,20 @@ impl Workspace {
|
||||
let ssh = matches!(name.as_str(), "ssh.service" | "sshd.service");
|
||||
let (title, description) = match command {
|
||||
ServiceCommand::Stop => (
|
||||
format!("停止服务“{name}”?"),
|
||||
t!("tools.service.stop.title", name = name),
|
||||
if ssh {
|
||||
"这是 SSH 服务:停止后新的连接都连不上这台主机,直到它再次启动。"
|
||||
t!("tools.service.stop.description_ssh")
|
||||
} else {
|
||||
"停止后它提供的功能就不可用了,直到再次启动。"
|
||||
t!("tools.service.stop.description")
|
||||
},
|
||||
),
|
||||
ServiceCommand::Restart => (
|
||||
format!("重启服务“{name}”?"),
|
||||
"服务会先停止再启动,中间短暂不可用。",
|
||||
t!("tools.service.restart.title", name = name),
|
||||
t!("tools.service.restart.description"),
|
||||
),
|
||||
_ => return run(window, cx),
|
||||
};
|
||||
confirm_danger(
|
||||
title.into(),
|
||||
Some(description.into()),
|
||||
command.label(),
|
||||
run,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
confirm_danger(title, Some(description), command.label(), run, window, cx);
|
||||
}
|
||||
|
||||
/// A process's details, from 进程管理's list and its command line.
|
||||
@@ -515,25 +535,21 @@ impl Workspace {
|
||||
let user = process.user.unwrap_or_else(|| "—".into());
|
||||
let (title, description, verb) = if force {
|
||||
(
|
||||
format!("强制结束进程“{name}”?"),
|
||||
format!(
|
||||
"PID {pid},用户 {user}。进程会立即被终止(SIGKILL),来不及保存数据; 只在「结束进程」不起作用时使用。"
|
||||
),
|
||||
"强制结束",
|
||||
t!("tools.process.kill.title", name = name),
|
||||
t!("tools.process.kill.description", pid = pid, user = user),
|
||||
t!("tools.process.kill.confirm"),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!("结束进程“{name}”?"),
|
||||
format!(
|
||||
"PID {pid},用户 {user}。进程会收到结束信号(SIGTERM),可以先做完收尾再退出。"
|
||||
),
|
||||
"结束进程",
|
||||
t!("tools.process.end.title", name = name),
|
||||
t!("tools.process.end.description", pid = pid, user = user),
|
||||
t!("tools.process.end.confirm"),
|
||||
)
|
||||
};
|
||||
let workspace = cx.entity().downgrade();
|
||||
confirm_danger(
|
||||
title.into(),
|
||||
Some(description.into()),
|
||||
title,
|
||||
Some(description),
|
||||
verb,
|
||||
Rc::new(move |window, cx| {
|
||||
let Some(view) = terminal.view.upgrade() else {
|
||||
@@ -541,7 +557,12 @@ impl Workspace {
|
||||
};
|
||||
let Some(reply) = view.read(cx).exec(end_command(pid, force), cx) else {
|
||||
window.push_notification(
|
||||
Notification::error(format!("无法结束 {name}(PID {pid}):终端没有连接")),
|
||||
Notification::error(t!(
|
||||
"tools.process.end_failed",
|
||||
name = name,
|
||||
pid = pid,
|
||||
error = t!("tools.not_connected")
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
@@ -550,20 +571,27 @@ impl Workspace {
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let outcome = match exec_answer(reply, cx).await {
|
||||
None => Err("终端没有连接".to_string()),
|
||||
None => Err(t!("tools.not_connected").to_string()),
|
||||
Some(Err(error)) => Err(error),
|
||||
Some(Ok(output)) => ended(&output),
|
||||
};
|
||||
cx.update(|window, cx| {
|
||||
let notification = match outcome {
|
||||
Ok(()) if force => {
|
||||
Notification::success(format!("已强制结束 {name}(PID {pid})"))
|
||||
}
|
||||
Ok(()) => Notification::success(format!(
|
||||
"已向 {name}(PID {pid})发送结束信号"
|
||||
Ok(()) if force => Notification::success(t!(
|
||||
"tools.process.killed",
|
||||
name = name,
|
||||
pid = pid
|
||||
)),
|
||||
Err(why) => Notification::error(format!(
|
||||
"无法结束 {name}(PID {pid}):{why}"
|
||||
Ok(()) => Notification::success(t!(
|
||||
"tools.process.ended",
|
||||
name = name,
|
||||
pid = pid
|
||||
)),
|
||||
Err(why) => Notification::error(t!(
|
||||
"tools.process.end_failed",
|
||||
name = name,
|
||||
pid = pid,
|
||||
error = why
|
||||
)),
|
||||
};
|
||||
window.push_notification(notification, cx);
|
||||
@@ -663,10 +691,10 @@ struct HostCommand {
|
||||
/// How it went, from what it printed.
|
||||
outcome: fn(&str) -> Result<(), String>,
|
||||
/// The notification when it went: 「nginx.service 已停止」.
|
||||
done: String,
|
||||
/// The notification's start when it did not, before why: 「无法停止
|
||||
/// nginx.service」.
|
||||
failed: String,
|
||||
done: SharedString,
|
||||
/// The notification when it did not, from why: 「无法停止
|
||||
/// nginx.service:…」.
|
||||
failed: Box<dyn FnOnce(&str) -> SharedString>,
|
||||
/// What the sidebar does after: read the tool's list again.
|
||||
then: fn(&mut ToolSidebar, &mut Context<ToolSidebar>),
|
||||
}
|
||||
@@ -689,20 +717,20 @@ fn run_on_host(
|
||||
} = command;
|
||||
let reply = script.and_then(|script| terminal.view.upgrade()?.read(cx).exec(script, cx));
|
||||
let Some(reply) = reply else {
|
||||
window.push_notification(Notification::error(format!("{failed}:终端没有连接")), cx);
|
||||
window.push_notification(Notification::error(failed(&t!("tools.not_connected"))), cx);
|
||||
return;
|
||||
};
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let result = match exec_answer(reply, cx).await {
|
||||
None => Err("终端没有连接".to_string()),
|
||||
None => Err(t!("tools.not_connected").to_string()),
|
||||
Some(Err(error)) => Err(error),
|
||||
Some(Ok(output)) => outcome(&output),
|
||||
};
|
||||
cx.update(|window, cx| {
|
||||
let notification = match result {
|
||||
Ok(()) => Notification::success(done),
|
||||
Err(why) => Notification::error(format!("{failed}:{why}")),
|
||||
Err(why) => Notification::error(failed(&why)),
|
||||
};
|
||||
window.push_notification(notification, cx);
|
||||
workspace
|
||||
@@ -713,3 +741,97 @@ fn run_on_host(
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// The notification when containers did as `command` said.
|
||||
fn containers_done(subject: &ContainerSubject, command: ContainerCommand) -> SharedString {
|
||||
match (subject, command) {
|
||||
(ContainerSubject::Container(name), ContainerCommand::Start) => {
|
||||
t!("tools.container.start.done", name = name)
|
||||
}
|
||||
(ContainerSubject::Container(name), ContainerCommand::Stop) => {
|
||||
t!("tools.container.stop.done", name = name)
|
||||
}
|
||||
(ContainerSubject::Container(name), ContainerCommand::Restart) => {
|
||||
t!("tools.container.restart.done", name = name)
|
||||
}
|
||||
(ContainerSubject::Project(name), ContainerCommand::Start) => {
|
||||
t!("tools.project.start.done", name = name)
|
||||
}
|
||||
(ContainerSubject::Project(name), ContainerCommand::Stop) => {
|
||||
t!("tools.project.stop.done", name = name)
|
||||
}
|
||||
(ContainerSubject::Project(name), ContainerCommand::Restart) => {
|
||||
t!("tools.project.restart.done", name = name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The notification when they did not, and why.
|
||||
fn containers_failed(
|
||||
subject: &ContainerSubject,
|
||||
command: ContainerCommand,
|
||||
why: &str,
|
||||
) -> SharedString {
|
||||
match (subject, command) {
|
||||
(ContainerSubject::Container(name), ContainerCommand::Start) => {
|
||||
t!("tools.container.start.failed", name = name, error = why)
|
||||
}
|
||||
(ContainerSubject::Container(name), ContainerCommand::Stop) => {
|
||||
t!("tools.container.stop.failed", name = name, error = why)
|
||||
}
|
||||
(ContainerSubject::Container(name), ContainerCommand::Restart) => {
|
||||
t!("tools.container.restart.failed", name = name, error = why)
|
||||
}
|
||||
(ContainerSubject::Project(name), ContainerCommand::Start) => {
|
||||
t!("tools.project.start.failed", name = name, error = why)
|
||||
}
|
||||
(ContainerSubject::Project(name), ContainerCommand::Stop) => {
|
||||
t!("tools.project.stop.failed", name = name, error = why)
|
||||
}
|
||||
(ContainerSubject::Project(name), ContainerCommand::Restart) => {
|
||||
t!("tools.project.restart.failed", name = name, error = why)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The notification when a container, an image, a volume or a network went.
|
||||
fn removed(object: DockerObject, name: &str) -> SharedString {
|
||||
match object {
|
||||
DockerObject::Container => t!("tools.remove.container.done", name = name),
|
||||
DockerObject::Image => t!("tools.remove.image.done", name = name),
|
||||
DockerObject::Volume => t!("tools.remove.volume.done", name = name),
|
||||
DockerObject::Network => t!("tools.remove.network.done", name = name),
|
||||
}
|
||||
}
|
||||
|
||||
/// The notification when it did not, and why.
|
||||
fn not_removed(object: DockerObject, name: &str, why: &str) -> SharedString {
|
||||
match object {
|
||||
DockerObject::Container => t!("tools.remove.container.failed", name = name, error = why),
|
||||
DockerObject::Image => t!("tools.remove.image.failed", name = name, error = why),
|
||||
DockerObject::Volume => t!("tools.remove.volume.failed", name = name, error = why),
|
||||
DockerObject::Network => t!("tools.remove.network.failed", name = name, error = why),
|
||||
}
|
||||
}
|
||||
|
||||
/// The notification when a service did as `command` said.
|
||||
fn service_done(name: &str, command: ServiceCommand) -> SharedString {
|
||||
match command {
|
||||
ServiceCommand::Start => t!("tools.service.start.done", name = name),
|
||||
ServiceCommand::Stop => t!("tools.service.stop.done", name = name),
|
||||
ServiceCommand::Restart => t!("tools.service.restart.done", name = name),
|
||||
ServiceCommand::Enable => t!("tools.service.enable.done", name = name),
|
||||
ServiceCommand::Disable => t!("tools.service.disable.done", name = name),
|
||||
}
|
||||
}
|
||||
|
||||
/// The notification when it did not, and why.
|
||||
fn service_failed(name: &str, command: ServiceCommand, why: &str) -> SharedString {
|
||||
match command {
|
||||
ServiceCommand::Start => t!("tools.service.start.failed", name = name, error = why),
|
||||
ServiceCommand::Stop => t!("tools.service.stop.failed", name = name, error = why),
|
||||
ServiceCommand::Restart => t!("tools.service.restart.failed", name = name, error = why),
|
||||
ServiceCommand::Enable => t!("tools.service.enable.failed", name = name, error = why),
|
||||
ServiceCommand::Disable => t!("tools.service.disable.failed", name = name, error = why),
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -278,7 +278,7 @@ async fn a_click_on_a_container_shows_its_details_and_output(cx: &mut TestAppCon
|
||||
wait_for_label(
|
||||
cx,
|
||||
handle,
|
||||
"container-basics:命令",
|
||||
"container-basics:command",
|
||||
"node ./dist/server/entry.mjs",
|
||||
)
|
||||
.await;
|
||||
@@ -289,9 +289,9 @@ async fn a_click_on_a_container_shows_its_details_and_output(cx: &mut TestAppCon
|
||||
.label()
|
||||
.map(str::to_owned)
|
||||
};
|
||||
assert_eq!(row("container-basics:名称").as_deref(), Some("forex-web"));
|
||||
assert_eq!(row("container-basics:name").as_deref(), Some("forex-web"));
|
||||
assert_eq!(
|
||||
row("container-basics:入口").as_deref(),
|
||||
row("container-basics:entrypoint").as_deref(),
|
||||
Some("docker-entrypoint.sh")
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -400,7 +400,7 @@ async fn a_click_on_a_volume_an_image_or_a_network_shows_its_details(cx: &mut Te
|
||||
wait_for_label(
|
||||
cx,
|
||||
handle,
|
||||
"volume-basics:挂载点",
|
||||
"volume-basics:mountpoint",
|
||||
"/var/lib/docker/volumes/vw-data/_data",
|
||||
)
|
||||
.await;
|
||||
@@ -419,9 +419,9 @@ async fn a_click_on_a_volume_an_image_or_a_network_shows_its_details(cx: &mut Te
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.click("docker-image:hello-world:latest", cx)
|
||||
});
|
||||
wait_for_label(cx, handle, "image-basics:平台", "linux/amd64").await;
|
||||
wait_for_label(cx, handle, "image-basics:platform", "linux/amd64").await;
|
||||
in_frame(cx, handle, |window, _| {
|
||||
assert_eq!(window.find("image-basics:大小").label(), Some("13.00 KB"));
|
||||
assert_eq!(window.find("image-basics:size").label(), Some("13.00 KB"));
|
||||
assert!(window.try_find("image-users:exciting_tesla").is_some());
|
||||
assert_eq!(
|
||||
window.find("image-environment").label(),
|
||||
@@ -470,7 +470,7 @@ async fn an_unused_volume_is_removed_from_its_details_after_asking(cx: &mut Test
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.click("docker-volume:old", cx)
|
||||
});
|
||||
wait_for_label(cx, handle, "volume-basics:名称", "old").await;
|
||||
wait_for_label(cx, handle, "volume-basics:name", "old").await;
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.click("docker-dialog-remove", cx)
|
||||
});
|
||||
|
||||
+19
-19
@@ -294,32 +294,32 @@ async fn a_click_on_a_process_shows_its_details_and_command_line(cx: &mut TestAp
|
||||
.await;
|
||||
assert_eq!(factory.exec_commands()[2], "sh -c 'cat /proc/2202/cmdline'");
|
||||
in_frame(cx, handle, |window, _| {
|
||||
let field = |label: &str| {
|
||||
let field = |id: &str| {
|
||||
window
|
||||
.find(SharedString::from(format!("process-field:{label}")))
|
||||
.find(SharedString::from(format!("process-field:{id}")))
|
||||
.label()
|
||||
.map(str::to_owned)
|
||||
};
|
||||
for (label, value) in [
|
||||
("PID", "2202"),
|
||||
("父进程", "systemd (1)"),
|
||||
("用户", "root"),
|
||||
for (id, value) in [
|
||||
("pid", "2202"),
|
||||
("parent", "systemd (1)"),
|
||||
("user", "root"),
|
||||
// A session leader with threads.
|
||||
("状态", "休眠 (Ssl)"),
|
||||
("state", "休眠 (Ssl)"),
|
||||
// Up since tick 5000 of a host up 1002 s.
|
||||
("运行时长", "00:15:52"),
|
||||
("终端", "无"),
|
||||
("优先级", "20"),
|
||||
("Nice", "0"),
|
||||
("CPU 使用率", "5.0%"),
|
||||
("累计 CPU 时间", "00:00:10"),
|
||||
("常驻内存", "468.75 MB (24.0%)"),
|
||||
("虚拟内存", "858.31 MB"),
|
||||
("线程", "30"),
|
||||
("直接子进程", "0"),
|
||||
("全部后代进程", "0"),
|
||||
("running", "00:15:52"),
|
||||
("terminal", "无"),
|
||||
("priority", "20"),
|
||||
("nice", "0"),
|
||||
("cpu", "5.0%"),
|
||||
("cpu_time", "00:00:10"),
|
||||
("memory", "468.75 MB (24.0%)"),
|
||||
("virtual_memory", "858.31 MB"),
|
||||
("threads", "30"),
|
||||
("children", "0"),
|
||||
("descendants", "0"),
|
||||
] {
|
||||
assert_eq!(field(label).as_deref(), Some(value), "{label}");
|
||||
assert_eq!(field(id).as_deref(), Some(value), "{id}");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+15
-15
@@ -253,27 +253,27 @@ async fn a_click_on_a_service_shows_its_state_and_journal(cx: &mut TestAppContex
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.click("service:nginx.service", cx)
|
||||
});
|
||||
wait_for_label(cx, handle, "service-field:主进程 PID", "781").await;
|
||||
wait_for_label(cx, handle, "service-field:main_pid", "781").await;
|
||||
in_frame(cx, handle, |window, _| {
|
||||
let field = |label: &str| {
|
||||
let field = |id: &str| {
|
||||
window
|
||||
.find(SharedString::from(format!("service-field:{label}")))
|
||||
.find(SharedString::from(format!("service-field:{id}")))
|
||||
.label()
|
||||
.map(str::to_owned)
|
||||
};
|
||||
for (label, value) in [
|
||||
("加载状态", "已加载"),
|
||||
("运行状态", "运行中 / 运行中"),
|
||||
("开机启动", "已启用"),
|
||||
("内存", "1.90 MB"),
|
||||
("任务数", "5"),
|
||||
("重启次数", "0"),
|
||||
("退出状态", "0"),
|
||||
("启动时间", "Tue 2026-09-15 17:49:07 CST"),
|
||||
("停止时间", "—"),
|
||||
("Unit 文件", "/lib/systemd/system/nginx.service"),
|
||||
for (id, value) in [
|
||||
("load", "已加载"),
|
||||
("active", "运行中 / 运行中"),
|
||||
("boot", "已启用"),
|
||||
("memory", "1.90 MB"),
|
||||
("tasks", "5"),
|
||||
("restarts", "0"),
|
||||
("exit_status", "0"),
|
||||
("started", "Tue 2026-09-15 17:49:07 CST"),
|
||||
("stopped", "—"),
|
||||
("unit_file", "/lib/systemd/system/nginx.service"),
|
||||
] {
|
||||
assert_eq!(field(label).as_deref(), Some(value), "{label}");
|
||||
assert_eq!(field(id).as_deref(), Some(value), "{id}");
|
||||
}
|
||||
assert!(window.try_find("service-dialog-stop").is_some());
|
||||
assert!(window.try_find("service-dialog-disable").is_some());
|
||||
|
||||
@@ -344,3 +344,32 @@ async fn on_windows_a_snippet_is_typed_after_what_is_there(cx: &mut TestAppConte
|
||||
// No Ctrl-E, Ctrl-U: its shells do not edit the line with them.
|
||||
assert_eq!(&factory.written_text()[before..], "df -h\r");
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
async fn the_sidebar_follows_a_switch_to_english(cx: &mut TestAppContext) {
|
||||
let (handle, workspace, _) = open_snippets(cx, store_with_snippets()).await;
|
||||
let settings = cx.update(|cx| workspace.read(cx).settings().clone());
|
||||
cx.update(|cx| {
|
||||
settings.update(cx, |settings, cx| {
|
||||
settings.update(
|
||||
|settings| settings.language = InterfaceLanguage::English,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
in_frame(cx, handle, |window, _| {
|
||||
assert_eq!(
|
||||
window.find(("tool-sidebar", INITIAL_WEB_TERMINAL)).label(),
|
||||
Some("Snippets")
|
||||
);
|
||||
assert_eq!(
|
||||
window.find("snippets-summary").label(),
|
||||
Some("4 snippets · 2 categories")
|
||||
);
|
||||
assert_eq!(
|
||||
window.find("snippet-category:none").label(),
|
||||
Some("Uncategorized 1")
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user