
后端【免费下载链接】wishMake SSH apps, just like that! 项目地址https://gitcode.com/gh_mirrors/wi/wish点击查看免费下载本指南完整讲解如何将基于 Charmbracelet Wish 的 SSH 应用从 v1 升级到 v2。核心变化集中在三处全部包迁移到charm.land域名下的 v2 模块、Bubble Tea 从 v2 开始采用返回tea.View的声明式视图模式、以及移除终端渲染相关的底层胶水代码。读完本文你将掌握导入路径改写、Handler 签名调整、ProgramOption 迁移到 View 字段、按键/鼠标/粘贴/剪贴板消息的新写法以及如何在 SSH 会话中正确获取客户端环境变量。升级概览大多数改动都是机械性的Wish v2 的升级并不复杂官方指引概括为四步将导入路径更新为charm.land/wish/v2将 Bubble Tea 升级到 v2改用声明式视图删除颜色配置color profile探测代码更新 Program 选项的写法。对大多数应用而言改动主要就是导入路径与视图模式的适配不需要重写业务逻辑。从当前仓库的 go.mod 可以看到Wish v2 的模块声明为charm.land/wish/v2依赖charm.land/bubbletea/v2、charm.land/log/v2、charm.land/ssh与charm.land/lipgloss/v2因此升级时只需把go.mod中相关 require 一并替换即可。导入路径统一迁移到 charm.land 域名Charm 系列库在 v2 统一使用charm.land这个 vanity domain导入路径变化如下// Before import ( github.com/charmbracelet/wish github.com/charmbracelet/wish/bubbletea github.com/charmbracelet/wish/logging github.com/charmbracelet/wish/activeterm tea github.com/charmbracelet/bubbletea github.com/charmbracelet/lipgloss github.com/charmbracelet/log ) // After import ( charm.land/wish/v2 charm.land/wish/v2/bubbletea charm.land/wish/v2/logging charm.land/wish/v2/activeterm tea charm.land/bubbletea/v2 charm.land/lipgloss/v2 charm.land/log/v2 )所有中间件包都遵循同一模式charm.land/wish/v2/accesscontrolcharm.land/wish/v2/commentcharm.land/wish/v2/elapsedcharm.land/wish/v2/gitcharm.land/wish/v2/ratelimitercharm.land/wish/v2/recovercharm.land/wish/v2/scp注意charm.land/ssh底层 SSH 服务器本身不带/v2后缀——它独立版本化与 Wish v2 是同一个依赖体系。在 bubbletea/tea.go 中可以看到 Wish v2 源码自身的导入方式与升级后的应用完全一致。Bubble Tea Handler 的四个变化点移除颜色配置探测MakeRenderer 已删除v1 时代需要手动通过bubbletea.MakeRenderer(s)基于 SSH 会话构造渲染器再判断终端背景色v2 中该函数已删除Bubble Tea v2 会自动处理颜色配置探测。// Before func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { renderer : bubbletea.MakeRenderer(s) txtStyle : renderer.NewStyle().Foreground(lipgloss.Color(10)) bg : light if renderer.HasDarkBackground() { bg dark } m : model{ txtStyle: txtStyle, bg: bg, } return m, []tea.ProgramOption{tea.WithAltScreen()} } // After func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { m : model{ txtStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(10)), } return m, []tea.ProgramOption{} }升级后直接使用 Lip Gloss v2 的lipgloss.NewStyle()即可。从源码层面看Wish v2 的 bubbletea 中间件在 bubbletea/tea_unix.go 的makeOpts中已经完成了颜色配置相关的铺垫当会话带有 PTY 时会补全TERM环境变量对于EmulatedPty无真实 PTY 的会话则会通过tea.WithColorProfile(colorprofile.Env(envs))基于环境变量强制设定颜色配置。也就是说颜色探测的职责整体下沉到了 Wish 中间件与 Bubble Tea 运行时应用层无需再关心。声明式视图View() 返回 tea.ViewBubble Tea v2 中View()不再返回string而是返回tea.View结构体// Before func (m model) View() string { return Hello, world! } // After func (m model) View() tea.View { v : tea.NewView(Hello, world!) v.AltScreen true // 把 tea.WithAltScreen() 移到这里 return v }仓库中的 examples/bubbletea/main.go 是 v2 写法的完整参考其View()构造tea.NewView(...)后设置v.AltScreen true并返回与官方升级指南保持一致。从消息中获取背景色监听 BackgroundColorMsgv1 是在初始化时同步查询渲染器v2 改为在Init()中发起tea.RequestBackgroundColor在Update()中通过tea.BackgroundColorMsg异步接收结果// Before func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { renderer : bubbletea.MakeRenderer(s) bg : light if renderer.HasDarkBackground() { bg dark } m : model{bg: bg} return m, nil } // After func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { m : model{bg: light} // 默认值 return m, nil } func (m model) Init() tea.Cmd { return tea.RequestBackgroundColor } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg : msg.(type) { case tea.BackgroundColorMsg: if msg.IsDark() { m.bg dark } else { m.bg light } } return m, nil }这正是 examples/bubbletea/main.go 中model.Init()与Update()的实际写法Init()通过tea.Batch(tea.RequestBackgroundColor)发起请求Update()中case tea.BackgroundColorMsg判断msg.IsDark()更新m.bg。从消息中获取颜色配置ColorProfileMsg同理颜色配置也通过消息传递func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg : msg.(type) { case tea.ColorProfileMsg: m.profile msg.String() // TrueColor、ANSI256、ANSI 等 } return m, nil }示例应用中tea.ColorProfileMsg的msg.String()结果会直接展示在界面上。由于消息是异步到达的模型字段应提供合理的默认值再在Update中覆盖。中间件 API 变更已删除的函数以下函数已从bubbletea中间件中移除MakeRenderer()—— 直接使用 Lip Gloss 样式颜色配置自动处理MiddlewareWithColorProfile()—— 不再需要QueryTerminalFilter—— 终端查询由 Bubble Tea v2 自己处理。在本次仓库源码中已搜索不到这三个标识符的任何实现确认它们属于 v1 遗留 API。函数签名变化所有中间件函数现在返回charm.land/wish/v2.Middleware// Before func Middleware(handler Handler) wish.Middleware // After func Middleware(handler Handler) wish.Middleware // 签名不变导入路径换了MiddlewareWithProgramHandler的签名被简化去掉了termenv.Profile参数// Before func MiddlewareWithProgramHandler( handler ProgramHandler, profile termenv.Profile, ) wish.Middleware // After func MiddlewareWithProgramHandler(handler ProgramHandler) wish.Middleware从源码看bubbletea/tea.go 中MiddlewareWithProgramHandler现在只接受一个ProgramHandler签名func(sess ssh.Session) *tea.Program其职责是为每个连接创建独立tea.Program、把 PTY 的窗口尺寸变化转换为tea.WindowSizeMsg发送给程序、并在会话结束时执行program.Kill()以恢复终端原始状态。若你想在标准 Handler 之外获得tea.Program的直接访问权例如定时p.Send()消息可以参考 examples/bubbleteaprogram/main.go它用newProg包装tea.NewProgram并起 goroutine 每秒发送timeMsg最终通过bubbletea.MiddlewareWithProgramHandler(teaHandler)接入。Program Options迁移到 View 字段Bubble Tea v2 中大部分 ProgramOption 移动到了View结构体上// Before return m, []tea.ProgramOption{ tea.WithAltScreen(), tea.WithMouseCellMotion(), } // After func (m model) View() tea.View { v : tea.NewView(m.content) v.AltScreen true v.MouseMode tea.MouseModeCellMotion return v }输入/输出等 I/O 相关配置仍可保留为 ProgramOption其中 SSH I/O 的接入必须继续使用bubbletea.MakeOptions(s)return m, bubbletea.MakeOptions(s) // SSH I/O 接入仍然需要MakeOptions是 Wish v2 中粘合 SSH 会话与 Bubble Tea 程序的关键函数。查看 bubbletea/tea.go 源码可知它做了三件事调用makeOpts(s)返回基于会话的 ProgramOption、追加一个消息过滤器把tea.SuspendMsg转为tea.ResumeMsg避免 SSH 场景下挂起语义异常。而makeOpts见 bubbletea/tea_unix.go会按会话是否有真实 PTY 分别接线无 PTYtea.WithInput(s)tea.WithOutput(s)tea.WithEnvironment(envs)模拟 PTYEmulatedPty额外强制tea.WithColorProfile(colorprofile.Env(envs))并设置窗口尺寸真实 PTY以pty.Slave作为输入输出并设置环境变量与窗口尺寸。这套逻辑正是升级指南强调MakeOptions(s)仍为 SSH I/O 所必需的底层原因——它能同时处理好真实与模拟 PTY 两种会话形态。Bubble Tea v2 的新消息体系升级到 v2 后SSH 应用自动获得 Bubble Tea v2 的全部能力其中最显著的是消息类型拆分。按键消息KeyPressMsg 与 KeyReleaseMsg按键消息拆分为KeyPressMsg和KeyReleaseMsg// Before case tea.KeyMsg: switch msg.String() { case : // space } // After case tea.KeyPressMsg: switch msg.String() { case space: // 注意space 而不是 // space case shiftenter: // 现在可以实现了 }注意两处细节一是空格的字符串表示由 变为space二是组合键如shiftenter现在可以直接匹配。鼠标消息按类型拆分鼠标消息按事件类型拆分为三个独立消息// Before case tea.MouseMsg: switch msg.Type { case tea.MouseLeft: // click } // After case tea.MouseClickMsg: if msg.Button tea.MouseLeft { // click } case tea.MouseWheelMsg: // scroll case tea.MouseMotionMsg: // movement粘贴事件独立 PasteMsg粘贴事件成为独立消息类型不再作为KeyMsg上的Paste标志// Before case tea.KeyMsg: if msg.Paste { // paste } // After case tea.PasteMsg: m.text msg.Content剪贴板支持OSC52 在 SSH 下也可用v2 支持读写剪贴板OSC52 转义序列在 SSH 会话中同样有效case tea.KeyPressMsg: switch msg.String() { case ctrlc: return m, tea.SetClipboard(Copied text) case ctrlv: return m, tea.ReadClipboard() } case tea.ClipboardMsg: m.clipboard msg.String()日志中间件的变化结构化日志中间件的签名变化主要体现在log.Logger类型来源的迁移// Before import github.com/charmbracelet/log logging.StructuredMiddlewareWithLogger(logger, log.InfoLevel) // After import charm.land/log/v2 logging.StructuredMiddlewareWithLogger(logger, log.InfoLevel)log.Logger类型现在来自charm.land/log/v2。查看 logging/logging.go 源码可以看到 v2 的完整 APIMiddleware()与MiddlewareWithLogger(Logger)提供非结构化的连接日志记录用户名、远端地址、是否公钥认证、命令、TERM、窗口尺寸、客户端版本与连接时长StructuredMiddleware()与StructuredMiddlewareWithLogger(logger *log.Logger, level log.Level)提供结构化日志默认使用log.Default()与log.InfoLevel。完整示例典型 Wish 应用的 Before/AfterBefore (v1)package main import ( tea github.com/charmbracelet/bubbletea github.com/charmbracelet/lipgloss github.com/charmbracelet/ssh github.com/charmbracelet/wish github.com/charmbracelet/wish/bubbletea github.com/charmbracelet/wish/logging ) func main() { s, _ : wish.NewServer( wish.WithAddress(:2222), wish.WithMiddleware( bubbletea.Middleware(teaHandler), logging.Middleware(), ), ) s.ListenAndServe() } func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { renderer : bubbletea.MakeRenderer(s) style : renderer.NewStyle().Foreground(lipgloss.Color(10)) m : model{style: style} return m, []tea.ProgramOption{tea.WithAltScreen()} } type model struct { style lipgloss.Style } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg : msg.(type) { case tea.KeyMsg: if msg.String() q { return m, tea.Quit } } return m, nil } func (m model) View() string { return m.style.Render(Hello, SSH!\n\nPress q to quit) }After (v2)package main import ( tea charm.land/bubbletea/v2 charm.land/lipgloss/v2 charm.land/wish/v2 charm.land/wish/v2/bubbletea charm.land/wish/v2/logging github.com/charmbracelet/ssh ) func main() { s, _ : wish.NewServer( wish.WithAddress(:2222), wish.WithMiddleware( bubbletea.Middleware(teaHandler), logging.Middleware(), ), ) s.ListenAndServe() } func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { style : lipgloss.NewStyle().Foreground(lipgloss.Color(10)) m : model{style: style} return m, nil } type model struct { style lipgloss.Style } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg : msg.(type) { case tea.KeyPressMsg: if msg.String() q { return m, tea.Quit } } return m, nil } func (m model) View() tea.View { v : tea.NewView(m.style.Render(Hello, SSH!\n\nPress q to quit)) v.AltScreen true return v }注意ssh包在 v2 中仍来自github.com/charmbracelet/ssh原路径不随 Wish 模块移动这与升级指南中的写法一致。关键变化清单导入路径使用charm.land/*/v2MakeRenderer已删除——直接使用 Lip Glosstea.WithAltScreen()迁移到v.AltScreen trueView()返回tea.View而非stringtea.KeyMsg改为tea.KeyPressMsg。如果你需要更完整的参考仓库的 examples 目录提供了按难度排列的 16 个示例其中 examples/bubbletea/main.go 展示了 v2 下完整的 SSH Bubble Tea 应用含优雅关闭、activeterm.Middleware()强制 PTY、logging.Middleware()日志examples/simple/main.go 则是最小化的中间件链路示例。迁移检查清单更新go.modrequirecharm.land/wish/v2将所有导入路径更新为charm.land/*删除bubbletea.MakeRenderer()调用删除MiddlewareWithColorProfile()的使用将View() string改为View() tea.View将 Program 选项迁移到 view 字段v.AltScreen等将tea.KeyMsg更新为tea.KeyPressMsg将tea.MouseMsg更新为具体的鼠标消息类型通过tea.BackgroundColorMsg处理背景色通过tea.ColorProfileMsg处理颜色配置在多种终端上测试你的 SSH 应用。在 SSH 应用中获得客户端环境变量Bubble Tea v2 中可以通过两种方式访问 SSH 客户端的环境变量。重要前提当你使用bubbletea.MakeOptions()时Wish 会自动把客户端的运行环境传递给 Bubble Tea。这意味着tea.EnvMsg中包含的是客户端的环境而不是服务端的方法一使用 tea.EnvMsg推荐Bubble Tea v2 会自动发送携带客户端环境的EnvMsgfunc teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { return model{}, bubbletea.MakeOptions(s) // 传递客户端环境 } type model struct { envMsg tea.EnvMsg } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg : msg.(type) { case tea.EnvMsg: m.envMsg msg // 访问具体的客户端变量 term : msg.Getenv(TERM) lang : msg.Getenv(LANG) user : msg.Getenv(USER) fmt.Printf(Client TERM: %s\n, term) } return m, nil }这一机制的底层支撑可以在 bubbletea/tea_unix.go 的makeOpts中看到envs : s.Environ()取自 SSH 会话随后通过tea.WithEnvironment(envs)注入 Program并在真实/模拟 PTY 场景下补上TERMpty.Term。方法二在 Handler 中提取如果需要在Init()运行之前就拿到环境变量可以在 Handler 里直接从ssh.Session提取func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { // 从 SSH 会话获取客户端环境变量 env : make(map[string]string) for _, e : range s.Environ() { parts : strings.SplitN(e, , 2) if len(parts) 2 { env[parts[0]] parts[1] } } m : model{ env: env, // 传给模型 } return m, bubbletea.MakeOptions(s) } type model struct { env map[string]string } func (m model) View() tea.View { // 访问客户端的环境 term : m.env[TERM] // 客户端的 TERM lang : m.env[LANG] // 客户端的 LANG user : m.env[USER] // 客户端的 USER return tea.NewView(fmt.Sprintf(Your TERM: %s, term)) }⚠️警告不要在 SSH 应用中使用os.Getenv()——它返回的是服务端的环境请始终使用tea.EnvMsg推荐或ssh.Session.Environ()。关键点这里拿到的是客户端的环境变量而不是服务端的。这对 SSH 应用尤为重要——在服务端执行os.Getenv()会得到完全错误的值。小结Wish v2 升级的实质是一次职责上移终端探测、颜色配置、窗口尺寸等底层细节从应用代码下沉到 Wish 中间件与 Bubble Tea 运行时应用层只需拥抱tea.View声明式视图与新的消息类型。按照本文的迁移清单逐项核对配合 examples 目录中的可运行示例绝大多数应用都能在短时间内平滑升级到 v2。赞分享后端【免费下载链接】wishMake SSH apps, just like that! 项目地址https://gitcode.com/gh_mirrors/wi/wish点击查看免费下载相关推荐Bubble Tea v2 升级迁移完全指南从命令式选项到声明式 View 架构Bubble Tea v2 升级迁移完全指南从命令式选项到声明式 View 架构 本篇指南以 UPGRADE_GUIDE_V2.md https://link机器学习深度学习数据可视化可观测性macOS 菜单栏管理完全指南3分钟装好 Ice把拥挤的菜单栏整理成三區macOS 菜单栏管理完全指南3分钟装好 Ice把拥挤的菜单栏整理成三區 Ice 是一款免费开源的 macOS 菜单栏管理工具它把屏幕顶部的菜单栏拆成可桌面应用10分钟如何搭建属于自己的知识库与知识图谱问答系统Yuxi-Know完整指南10分钟如何搭建属于自己的知识库与知识图谱问答系统Yuxi Know完整指南 企业里的流程文档、产品手册、技术方案散落在各个系统里要查一个问题得翻半天直接人工智能大模型AI AgentRAG多智能体知识图谱后端前端上一篇Istio配置版本控制Git版本管理与回滚下一篇极智嘉港股IPO深度解析全球AMR仓储机器人第一股挂牌实录与AI物流产业风向创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考