Files
u-tabs/internal/key.go
T

551 lines
12 KiB
Go

package internal
import tea "charm.land/bubbletea/v2"
// --- 按键分发 ---
func (m *Model) handleKey(s string) (*Model, tea.Cmd) {
// 编辑标题时优先走输入处理,避免 q/1/2/u 等被全局拦截
if m.editTitleMode {
return m.handleEditTitleInput(s)
}
switch s {
case "q", "ctrl+c":
return m, tea.Quit
case "u":
if m.update.Available && !m.update.Updating {
m.update.Updating = true
return m, SelfUpdateCmd(m.update.DownloadURL, m.update.SHA256, m.update.NewVersion)
}
}
// Tab 快捷切换: 1=工作空间 2=历史对话 3=技能
if s == "1" {
m.editTitleMode = false
m.editTitleBuf = ""
if m.IsHistoryTab() || m.IsSkillsTab() {
m.activeGroup = 0
m.resetCursor()
m.wsFocus = 1
}
return m, nil
}
if s == "2" {
if !m.IsHistoryTab() {
m.activeGroup = len(Groups) // HISTORY
m.resetCursor()
m.wsFocus = 1
return m, m.onTabSwitch()
}
return m, nil
}
if s == "3" {
if !m.IsSkillsTab() {
m.activeGroup = len(Groups) + 1 // SKILLS
m.resetCursor()
m.wsFocus = 1
return m, m.onSkillsTabSwitch()
}
return m, nil
}
if m.IsSkillsTab() {
return m.handleSkillsKey(s)
}
if m.IsHistoryTab() {
return m.handleHistoryKey(s)
}
return m.handleWorkspaceKey(s)
}
func (m *Model) onTabSwitch() tea.Cmd {
if !m.IsHistoryTab() || m.history.Scanning {
return nil
}
m.history.Scanning = true
return ScanSessionsCmd()
}
func (m *Model) onSkillsTabSwitch() tea.Cmd {
if m.Skills.Loaded {
return nil
}
m.Skills.Scanning = true
return ScanSkillsCmd()
}
// --- 技能 tab 按键 ---
func (m *Model) handleSkillsKey(s string) (*Model, tea.Cmd) {
switch s {
case "up", "k":
switch m.Skills.FocusPanel {
case 0:
m.moveSkillsSkill(-1)
case 1:
m.moveSkillsFile(-1)
case 2:
m.scrollSkillContent(-1)
}
case "down", "j":
switch m.Skills.FocusPanel {
case 0:
m.moveSkillsSkill(1)
case 1:
m.moveSkillsFile(1)
case 2:
m.scrollSkillContent(1)
}
case "pgup":
if m.Skills.FocusPanel == 2 {
m.scrollSkillContent(-10)
}
case "pgdown":
if m.Skills.FocusPanel == 2 {
m.scrollSkillContent(10)
}
case "tab", "right", "l":
// 进内容面板需已加载内容
if m.Skills.FocusPanel < 1 {
m.Skills.FocusPanel = 1
m.autoLoadCurrentFile()
} else if m.Skills.FocusPanel == 1 && m.Skills.ContentPath != "" {
m.Skills.FocusPanel = 2
}
case "shift+tab", "left", "h":
if m.Skills.FocusPanel > 0 {
m.Skills.FocusPanel--
}
case "enter":
switch m.Skills.FocusPanel {
case 0:
m.Skills.FocusPanel = 1
m.Skills.FileCur = 0
m.autoLoadCurrentFile()
case 1:
fn := m.currentFlatNode()
if fn != nil {
if fn.IsDir {
fn.Expanded = !fn.Expanded
flat := m.skillsFlattened()
if m.Skills.FileCur >= len(flat) {
m.Skills.FileCur = len(flat) - 1
}
} else {
m.loadSkillContent(fn.Path)
m.Skills.FocusPanel = 2 // 打开文件即聚焦内容,便于滚动
}
}
}
case "r", "f5":
m.Skills.Loaded = false
m.Skills.Scanning = true
return m, ScanSkillsCmd()
}
return m, nil
}
// scrollSkillContent 滚动内容面板,边界由 View 层按实际高度再夹紧
func (m *Model) scrollSkillContent(dir int) {
m.Skills.ContentScroll += dir
if m.Skills.ContentScroll < 0 {
m.Skills.ContentScroll = 0
}
}
// autoLoadCurrentFile 若当前文件树光标停在文件上,自动加载其内容
func (m *Model) autoLoadCurrentFile() {
if fn := m.currentFlatNode(); fn != nil && !fn.IsDir {
m.loadSkillContent(fn.Path)
}
}
func (m *Model) moveSkillsSkill(dir int) {
if len(m.Skills.Skills) == 0 {
return
}
m.Skills.SkillCur += dir
if m.Skills.SkillCur < 0 {
m.Skills.SkillCur = len(m.Skills.Skills) - 1
}
if m.Skills.SkillCur >= len(m.Skills.Skills) {
m.Skills.SkillCur = 0
}
m.Skills.FileCur = 0
m.Skills.Content = ""
m.Skills.ContentPath = ""
}
func (m *Model) moveSkillsFile(dir int) {
flat := m.skillsFlattened()
if len(flat) == 0 {
return
}
m.Skills.FileCur += dir
if m.Skills.FileCur < 0 {
m.Skills.FileCur = len(flat) - 1
}
if m.Skills.FileCur >= len(flat) {
m.Skills.FileCur = 0
}
// 光标移到文件即自动加载内容;目录则清空内容区
if fn := m.currentFlatNode(); fn != nil {
if fn.IsDir {
m.Skills.Content = ""
m.Skills.ContentPath = ""
m.Skills.ContentScroll = 0
} else {
m.loadSkillContent(fn.Path)
}
}
}
// --- Workspace 按键 ---
func (m *Model) handleWorkspaceKey(s string) (*Model, tea.Cmd) {
switch s {
case "left", "h":
if m.wsFocus > 0 {
m.wsFocus = 0
}
case "right", "l":
if m.wsFocus < 1 {
m.wsFocus = 1
}
case "tab", "shift+tab":
m.wsFocus = (m.wsFocus + 1) % 2
case "up", "k":
if m.wsFocus == 0 {
m.moveGroupCursor(-1)
} else {
m.moveCursor(-1)
}
case "down", "j":
if m.wsFocus == 0 {
m.moveGroupCursor(1)
} else {
m.moveCursor(1)
}
case "enter":
if m.inputBuf != "" {
return m.launchByInput()
}
if m.wsFocus == 0 {
if m.wsTabCur == len(Groups) {
m.activeGroup = len(Groups)
m.resetCursor()
return m, m.onTabSwitch()
}
if m.wsTabCur == len(Groups)+1 {
m.activeGroup = len(Groups) + 1
m.resetCursor()
return m, m.onSkillsTabSwitch()
}
m.activeGroup = m.wsTabCur
m.wsFocus = 1
} else {
return m.launchSelected()
}
default:
if len(s) == 1 && s[0] >= '0' && s[0] <= '9' && len(m.inputBuf) < 4 {
m.inputBuf += s
}
}
return m, nil
}
func (m *Model) resetCursor() {
m.cursor = 0
m.inputBuf = ""
}
func (m *Model) moveGroupCursor(dir int) {
total := len(Groups) + 2 // 含历史对话入口和技能入口
m.wsTabCur = (m.wsTabCur + dir + total) % total
if m.wsTabCur < len(Groups) {
m.activeGroup = m.wsTabCur
}
m.resetCursor()
}
// --- HISTORY 按键 ---
func (m *Model) handleHistoryKey(s string) (*Model, tea.Cmd) {
if m.forkMode {
return m.handleForkInput(s)
}
if m.editTitleMode {
return m.handleEditTitleInput(s)
}
switch s {
case "up", "k":
if m.history.FocusPanel == 0 {
m.moveHistoryDir(-1)
} else {
m.moveHistorySess(-1)
}
case "down", "j":
if m.history.FocusPanel == 0 {
m.moveHistoryDir(1)
} else {
m.moveHistorySess(1)
}
case "tab", "right", "l":
if m.history.FocusPanel < 1 {
m.history.FocusPanel = 1
}
case "shift+tab", "left", "h":
if m.history.FocusPanel > 0 {
m.history.FocusPanel = 0
}
case "enter":
if m.history.FocusPanel == 0 {
if m.history.DirCursor == len(m.history.Projects)+2 {
m.activeGroup = 0
m.resetCursor()
m.wsTabCur = 0
m.wsFocus = 1
}
m.history.FocusPanel = 1
} else {
return m.resumeSelected()
}
case "n":
return m.newSessionFromHistory()
case "r", "f5":
if cmd := m.onTabSwitch(); cmd != nil {
return m, cmd
}
case "f":
m.toggleFavorite()
case "e", "E":
if m.currentSession() != nil && !m.forkMode {
s := m.currentSession()
m.editTitleMode = true
m.editTitleBuf = s.DisplayTitle()
m.editTitleCursor = len([]rune(m.editTitleBuf))
m.history.FocusPanel = 1
}
case "c":
if m.currentSession() != nil && m.history.FocusPanel == 1 {
m.forkMode = true
m.forkBuf = ""
}
}
return m, nil
}
func (m *Model) handleEditTitleInput(s string) (*Model, tea.Cmd) {
switch s {
case "enter":
return m.confirmEditTitle()
case "esc":
m.editTitleMode = false
m.editTitleBuf = ""
m.editTitleCursor = 0
case "backspace":
m.backspaceTitle()
case "delete":
m.deleteTitleChar()
case "left", "h":
if m.editTitleCursor > 0 {
m.editTitleCursor--
}
case "right", "l":
r := []rune(m.editTitleBuf)
if m.editTitleCursor < len(r) {
m.editTitleCursor++
}
case "home", "ctrl+a":
m.editTitleCursor = 0
case "end", "ctrl+e":
m.editTitleCursor = len([]rune(m.editTitleBuf))
case "space":
m.insertTitleRune(' ')
default:
switch s {
case "up", "down", "tab", "shift+tab":
default:
if len(s) == 1 && s[0] >= 32 {
m.insertTitleRune(rune(s[0]))
} else if len(s) > 1 {
m.insertTitleString(s)
}
}
}
return m, nil
}
func (m *Model) insertTitleRune(r rune) {
buf := []rune(m.editTitleBuf)
pos := m.editTitleCursor
if pos > len(buf) {
pos = len(buf)
}
buf = append(buf[:pos], append([]rune{r}, buf[pos:]...)...)
m.editTitleBuf = string(buf)
m.editTitleCursor = pos + 1
}
func (m *Model) insertTitleString(s string) {
buf := []rune(m.editTitleBuf)
pos := m.editTitleCursor
if pos > len(buf) {
pos = len(buf)
}
ins := []rune(s)
buf = append(buf[:pos], append(ins, buf[pos:]...)...)
m.editTitleBuf = string(buf)
m.editTitleCursor = pos + len(ins)
}
func (m *Model) backspaceTitle() {
buf := []rune(m.editTitleBuf)
if m.editTitleCursor > 0 && len(buf) > 0 {
pos := m.editTitleCursor - 1
if pos > len(buf) {
pos = len(buf) - 1
}
buf = append(buf[:pos], buf[pos+1:]...)
m.editTitleBuf = string(buf)
m.editTitleCursor--
}
}
func (m *Model) deleteTitleChar() {
buf := []rune(m.editTitleBuf)
pos := m.editTitleCursor
if len(buf) > 0 && pos < len(buf) {
buf = append(buf[:pos], buf[pos+1:]...)
m.editTitleBuf = string(buf)
}
}
func (m *Model) handleForkInput(s string) (*Model, tea.Cmd) {
switch s {
case "enter":
return m.executeFork()
case "esc":
m.forkMode = false
m.forkBuf = ""
case "backspace":
if len(m.forkBuf) > 0 {
m.forkBuf = m.forkBuf[:len(m.forkBuf)-1]
}
default:
if len(s) == 1 && s[0] >= 32 && s[0] < 127 {
m.forkBuf += s
} else if len(s) > 1 {
m.forkBuf += s
}
}
return m, nil
}
// --- 光标移动 ---
func (m *Model) moveHistoryDir(dir int) {
if len(m.history.Projects) == 0 {
return
}
m.history.DirCursor += dir
maxDir := len(m.history.Projects) + 3
if m.history.DirCursor < 0 {
m.history.DirCursor = maxDir - 1
}
if m.history.DirCursor >= maxDir {
m.history.DirCursor = 0
}
m.history.SessCursor = 0
}
func (m *Model) moveHistorySess(dir int) {
sessions := m.currentSessions()
if len(sessions) == 0 {
return
}
m.history.SessCursor += dir
if m.history.SessCursor < 0 {
m.history.SessCursor = len(sessions) - 1
}
if m.history.SessCursor >= len(sessions) {
m.history.SessCursor = 0
}
}
func (m *Model) currentSessions() []*Session {
if m.history.DirCursor == 0 {
if m.allSessionsDirty || m.allSessionsCache == nil {
total := 0
for _, pd := range m.history.Projects {
total += len(pd.Sessions)
}
all := make([]*Session, 0, total)
for _, pd := range m.history.Projects {
all = append(all, pd.Sessions...)
}
m.allSessionsCache = all
m.allSessionsDirty = false
}
return m.allSessionsCache
}
if m.history.DirCursor == 1 {
return collectFavorites(m.history.Projects, m.history.Favorites)
}
idx := m.history.DirCursor - 2
if idx >= len(m.history.Projects) {
return nil
}
return m.history.Projects[idx].Sessions
}
func (m *Model) currentSession() *Session {
sessions := m.currentSessions()
if len(sessions) == 0 {
return nil
}
if m.history.SessCursor >= len(sessions) {
m.history.SessCursor = len(sessions) - 1
}
return sessions[m.history.SessCursor]
}
func (m *Model) currentDirCwd() string {
if m.history.DirCursor <= 1 {
return ""
}
idx := m.history.DirCursor - 2
if idx >= len(m.history.Projects) {
return ""
}
return m.history.Projects[idx].Dir
}
func (m *Model) moveCursor(dir int) {
svcs := WorkspacesByGroup(Groups[m.activeGroup].Label)
if len(svcs) == 0 {
return
}
m.cursor += dir
if m.cursor < 0 {
m.cursor = len(svcs) - 1
}
if m.cursor >= len(svcs) {
m.cursor = 0
}
}
func (m *Model) toggleMultiSelect() {
svcs := WorkspacesByGroup(Groups[m.activeGroup].Label)
if m.cursor < len(svcs) {
idx := svcs[m.cursor].Index
if m.selected[idx] {
delete(m.selected, idx)
} else {
m.selected[idx] = true
}
}
}