【记录】Ubuntu26|通过网页和ydotool用手机远程输入文本到电脑上,方便接入手机上优越的语音输入法

Linux Wayland 手机远程输入方案

背景

在 Linux Wayland 环境下,GSConnect 等蓝牙键盘工具发送的是底层键盘扫描码,手机输入法和电脑输入法会互相打架,无法做到"手机打出什么字,电脑就输出什么字"。

本方案通过在电脑上搭建一个极简 Web 服务器,手机浏览器访问后自由使用手机输入法(拼音/语音/手写),点发送后文字自动粘贴到电脑当前光标位置。

核心原理:手机浏览器完成所有输入法运算,电脑只接收最终文本,通过剪贴板+模拟按键实现"立即上屏"。

环境要求

  • Ubuntu 22.04+(Wayland,GNOME 桌面)
  • Python 3(系统自带)
  • 手机和电脑在同一局域网

安装依赖

bash 复制代码
sudo apt install -y wl-clipboard ydotool
  • wl-clipboard :Wayland 剪贴板读写工具(wl-copy / wl-paste
  • ydotool :通过 /dev/uinput 在内核层面注入键盘事件,绕过 Wayland 沙盒

安装后需要确保用户在 input 组中:

bash 复制代码
sudo usermod -aG input $USER

重新登录或重启生效。启动 ydotoold 守护进程:

bash 复制代码
sudo systemctl enable --now ydotoold
# 或者用户级服务
systemctl --user enable --now ydotool

完整代码

代码已上传到 GitHub:https://github.com/shandianchengzi/remote_input_page

可以直接 clone:

bash 复制代码
git clone https://github.com/shandianchengzi/remote_input_page.git
cd remote_input_page

也可以手动新建文件 remote_input.py,把下面的代码复制进去:

python 复制代码
#!/usr/bin/env python3
"""Remote input server - use phone browser to type into PC."""

from http.server import BaseHTTPRequestHandler, HTTPServer
import subprocess
import time

HTML = """
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Remote Input</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { background: #121212; color: white; padding: 20px; font-family: -apple-system, sans-serif; }
        h2 { margin-bottom: 15px; font-weight: 400; color: #888; }
        textarea {
            width: 100%; height: 200px; font-size: 18px; padding: 12px;
            background: #1e1e1e; color: white; border: 1px solid #333;
            border-radius: 8px; resize: vertical;
        }
        button {
            width: 100%; height: 56px; font-size: 20px; margin-top: 12px;
            background: #2563eb; color: white; border: none;
            border-radius: 8px; cursor: pointer; transition: background 0.2s;
        }
        button:active { background: #1d4ed8; }
        #status { margin-top: 10px; color: #4ade80; font-size: 14px; min-height: 20px; }
    </style>
</head>
<body>
    <h2>Remote Input</h2>
    <textarea id="text" placeholder="Type here... (use phone IME freely)"></textarea>
    <button onclick="send()">Send to PC</button>
    <div id="status"></div>
    <script>
        let sending = false;
        function send() {
            if (sending) return;
            const t = document.getElementById('text');
            const s = document.getElementById('status');
            if (t.value.trim() === '') return;
            sending = true;
            fetch('/', { method: 'POST', body: t.value })
                .then(r => { s.textContent = 'Sent!'; t.value = ''; sending = false; setTimeout(() => s.textContent = '', 2000); })
                .catch(() => { s.textContent = 'Failed'; sending = false; });
        }
    </script>
</body>
</html>
"""


class Handler(BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        print(f"[LOG] {format % args}")

    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-type", "text/html; charset=utf-8")
        self.end_headers()
        self.wfile.write(HTML.encode("utf-8"))

    def do_POST(self):
        length = int(self.headers["Content-Length"])
        text = self.rfile.read(length).decode("utf-8")
        if text:
            import os
            env = os.environ.copy()
            env["DISPLAY"] = ":0"
            # 写入 CLIPBOARD(供 Ctrl+V 粘贴)
            subprocess.run(["wl-copy"], input=text.encode("utf-8"), env=env)
            # 写入 PRIMARY(供 Shift+Insert 粘贴)
            subprocess.run(["wl-copy", "-p"], input=text.encode("utf-8"), env=env)
            time.sleep(0.15)  # 等待剪贴板同步
            # ydotool 模拟 Shift+Insert
            subprocess.run(["ydotool", "key", "-d", "50",
                           "42:1", "110:1", "110:0", "42:0"], env=env)
        self.send_response(200)
        self.end_headers()


if __name__ == "__main__":
    server = HTTPServer(("0.0.0.0", 8080), Handler)
    # 替换为你电脑的实际局域网 IP
    print("Server running. Open http://<YOUR_IP>:8080 on your phone")
    server.serve_forever()

使用方法

1. 查看电脑局域网 IP

bash 复制代码
ip -4 addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d/ -f1

2. 启动服务器

bash 复制代码
python3 remote_input.py

3. 手机访问

手机连同一 Wi-Fi,浏览器打开 http://<电脑IP>:8080,会看到一个黑色输入框。自由使用手机输入法输入文字,点击 "Send to PC" 即可粘贴到电脑当前光标位置。

关键技术细节

为什么不用 xdotool?

xdotool 通过 X11 协议注入按键,在 Wayland 下只能对 XWayland 应用生效。GNOME 原生应用(终端、文件管理器、Chrome Wayland 模式等)会直接拦截丢弃 xdotool 的事件。ydotool 通过 /dev/uinput 在内核层面注入,不受 Wayland 沙盒限制。

为什么不用 wtype?

wtype 依赖 Wayland 的 virtual keyboard protocol,GNOME (Mutter) 不支持该协议,直接报错退出。

为什么用 Shift+Insert 而不是 Ctrl+V?

Linux 终端中 Ctrl+V 被保留为终端控制信号(字面量输入),粘贴快捷键是 Ctrl+Shift+V。但浏览器中粘贴是 Ctrl+V。Shift+Insert 是 Linux 下的通用粘贴键,终端和浏览器都认。

为什么要同时写入 CLIPBOARD 和 PRIMARY?

Linux 有两个独立的剪贴板:

  • CLIPBOARD:Ctrl+C/V 操作的常规剪贴板
  • PRIMARY:鼠标选中即复制的主选择区,Shift+Insert 读取的就是这个

终端在处理 Shift+Insert 时会读 PRIMARY,浏览器则读 CLIPBOARD。只写一个会导致"终端粘出老内容,浏览器粘出新内容"的问题。wl-copy 默认写 CLIPBOARD,wl-copy -p 写 PRIMARY。

为什么 ydotool 需要 -d 50 延迟?

ydotool 通过 /dev/uinput 注入按键事件。如果按键事件之间没有间隔,系统输入状态机来不及同步"修饰键已按下"的状态,会导致按键被当作字面字符输出(比如出现 ^V 字面量)。-d 50 表示每个按键事件之间延迟 50ms。

常见问题

手机打不开网页

  • 确认手机和电脑在同一 Wi-Fi
  • 检查电脑防火墙是否放行 8080 端口:sudo ufw allow 8080
  • 确认服务器正在运行:ss -tlnp | grep 8080

粘贴后没有反应

  • 确认 ydotoold 正在运行:systemctl --user status ydotool
  • 确认用户在 input 组:id -nG $USER | grep input
  • 检查终端输出的 [LOG] 日志

中文粘贴为空

  • 检查 wl-copy 是否正常:echo "测试" | wl-copy && wl-paste
  • 如果 wl-paste 输出为空,可能是 Wayland 剪贴板守护进程问题,尝试重启 GNOME Shell(Alt+F2 输入 r

本账号所有文章均为原创,欢迎转载,请注明文章出处:https://shandianchengzi.blog.csdn.net/article/details/161542561。百度和各类采集站皆不可信,搜索请谨慎鉴别。技术类文章一般都有时效性,本人习惯不定期对自己的博文进行修正和更新,因此请访问出处以查看本文的最新版本。

相关推荐
shandianchengzi2 小时前
【记录】Claude Code|Ubuntu26给Claude Code新增任务消息提示音
运维·服务器·ubuntu·ai·大模型·音频·claude
大明者省4 小时前
Ubuntu Python 部署终极版教程
开发语言·python·ubuntu
承渊政道4 小时前
Linux系统学习【进程控制:进程创建、终止与等待、进程程序替换、自主shell命令行解释器详解】
linux·服务器·c++·学习·ubuntu·bash·远程工作
大明者省4 小时前
CentOS 与 Ubuntu Python 部署差异
笔记·python·ubuntu·centos
芯巧电子17 小时前
11. IC实例新增子类别 I 芯巧Cadence 25.1新功能深入学习
科技·cadence·软件·allegro·orcad
andlbds1 天前
解决Ubuntu20.04进入系统卡死在厂商Logo界面问题
linux·ubuntu
MIXLLRED1 天前
解决: Ubuntu 22.04上树莓派4B扩展板ROS2兼容性修复指南
linux·ubuntu·树莓派
QFIUNE1 天前
使用 MMseqs2 计算多个 DTI 数据集的蛋白序列相似度
linux·python·ubuntu
Vick_Zhang1 天前
ubuntu上rabbitmq
服务器·ubuntu·rabbitmq