🎉0

我怀念的 AutoHotKey

它是什么?一款效率工具,一种脚本语言,可以把我们(的键盘&鼠标)变强大。

具体看官网就好啦:https://www.autohotkey.com/

作为十多年的 Windows 用户,转而主要使用 Mac 后,最怀念的软件就是 AutoHotKey。尽管,现在我找到了 macOS 上好用的 hammerspoon 作为替代品,但 AHK 的陪伴值得纪念。

互联网上不乏优秀的 AHK 教程,这里就不作详细介绍,只随便谈谈它帮我 +1s 的几个点。

懒得看废话可以直接去拿代码

1. 热词

我最怀念的功能,得首先讲。尽管 macOS 在系统层面提供了热词,但实际可用场景有所限制,比如密码输入框就无法使用。

热词是什么?

按顺序输入的字符,会被替换成别的内容。

比如我敲一个 email,它会变成 hi@gnimoay.com ,magic。

这很类似输入法里面的自定义短语。不过,实际上我不会这样设置,否则真的想敲出 email 的时候,只能先输入 mail,然后把鼠标挪到左边,再输入 e,很傻。所以,聪明的我,会给所有热词加上 ;; 作为开头,毕竟几乎不存在需要连续输入两个分号的场景。

那么怎么定义热词?

:*:你的热词::会变成这个

虽然这是中文,但AHK并不支持中文。要怎么才能支持中文?我忘了

但事实上我不需要中文热词

:*:;;g::wodegugeyouxiang@gmail.com        //看得出来这是邮箱地址
:*:;;o::wodeoutlookyouxiang@outlook.com   //看得出来这也是邮箱地址
:*:;;dh::186xxxxxx70                      //看得出来这是电话号码
:*:;;icbc::622836762384xxxxxx             //看得出来这是银行卡号
:*:;;id::456789202012120202               //看得出来这是身份证号码

不过每次添加热词都要修改代码这么麻烦吗?

不用,我们可以绑定一个快捷键,直接为选中的文字设置 热词,然后自动修改 AHK 脚本文件。

// 通过 win+h 添加的热词会自动补充到这个文件末尾
// 记得把文件路径替换为自己的

// 其实注释不是这么写的
; 注释应该用分号开头

#h:: ; Win+H hotkey
  ; 获取当前选择的文本. 使用剪贴板代替
  ; "ControlGet Selected", 是因为它可以工作于更大范围的编辑器
  ; (即字处理软件).  保存剪贴板当前的内容
  ; 以便在后面恢复. 尽管这里只能处理纯文本,
  ; 但总比没有好:
  AutoTrim on ; 保留剪贴板中任何前导和尾随空白字符.
  ClipboardOld = %ClipboardAll%
  Clipboard = ; 必须清空, 才能检测是否有效.
  Send ^c
  ClipWait 1
  if ErrorLevel ; ClipWait 超时.
    return
  ; 替换 CRLF/LF`n 以便用于 "发送原始模式的" 热字串:
  ; 对其他任何在原始模式下可能出现问题
  ; 的字符进行相同的处理:
  StringReplace, Hotstring, Clipboard, ``, ````, All ; 首先进行此替换以避免和后面的操作冲突.
  StringReplace, Hotstring, Hotstring, `r`n, ``r, All ; 在 MS Word 等软件中中使用 `r 会比 `n 工作的更好.
  StringReplace, Hotstring, Hotstring, `n, ``r, All
  StringReplace, Hotstring, Hotstring, %A_Tab%, ``t, All
  StringReplace, Hotstring, Hotstring, `;, ```;, All
  Clipboard = %ClipboardOld% ; 恢复剪贴板之前的内容.
  ; 这里会移动 InputBox 的光标到更人性化的位置:
  SetTimer, MoveCaret, 10
  ; 显示 InputBox, 提供默认的热字串:
  InputBox, Hotstring, New Hotstring, Type your abreviation at the indicated insertion point. You can also edit the replacement text if you wish.`n`nExample entry: :R:btw`::by the way,,,,,,,, :*:`::%Hotstring%
    if ErrorLevel ; 用户选择了取消.
    return
IfInString, Hotstring, :*`:::
  {
    MsgBox You didn't provide an abbreviation. The hotstring has not been added.
    return
  }
  ; 否则添加热字串并重新加载脚本:
  FileAppend, `n%Hotstring%, D:\SourceCode\AutoHotKey\addHotString.ahk ; 在开始处放置 `n 以防文件末尾没有空行.
  Reload
  Sleep 200 ; 如果加载成功, reload 会在 Sleep 期间关闭当前实例, 所以永远不会执行到下面的语句.
  MsgBox, 4,, The hotstring just added appears to be improperly formatted. Would you like to open the script for editing? Note that the bad hotstring is at the bottom of the script.
    IfMsgBox, Yes, Edit
return

MoveCaret:
  IfWinNotActive, New Hotstring
    return
  ; 否则移动 InputBox 中的光标到用户输入缩写的位置.
  Send {Home}{Right 3}
  SetTimer, MoveCaret, Off
return

2. 按键映射

按键映射是什么?

把你输入的按键 A 变成按键 B

怎么设置按键映射?

按键A::按键B

在 AHK 中, ^!+# 分别表示 Ctrl Alt Shift Win

按键映射方案千千万,分享几个我觉得最有价值的:

// 中文直角引号
![::send,{U+300C}            // alt + [  转换为「
!]::send,{U+300D}            // alt + ]  转换为 」

// alt + ikjl 映射为「上下左右」方向键
!i::send,{up}
!k::send,{down}
!j::send,{left}
!l::send,{right}

// 音量调节
!-::send,{Volume_Down}
!=::send,{Volume_Up}

// 完整方案就直接去看代码吧

3. 窗口移动

为了拖动窗口,需要把鼠标挪到窄窄的标题栏上,不好。有了这个脚本,按住 alt + 空格 就能拖动窗口了。

LAlt & Space::
    ToolTip, 移动鼠标以改变窗口位置
    ;设置鼠标坐标模式为相对屏幕
    CoordMode, Mouse, Screen
    ;获取初始鼠标位置
    MouseGetPos, mX0, mY0 , hwnd
    IfWinExist, ahk_id %hwnd%
    {
        ;获取初始窗口位置
        WinGetPos, wX0, wY0
        ;激活鼠标下窗口
         WinActivate, ahk_id %hwnd%
    }
    Else
        Return
    Loop{
        GetKeyState, state, LAlt, P
        if state = U
        {
            ToolTip 
            break
        }
        GetKeyState, mState, Space, P 
        if mState = U        
         {
            ToolTip 
            break
        }

        ;获取当前鼠标位置
        MouseGetPos, mX, mY
        SetWinDelay, -1
        WinMove, ahk_id %hwnd%,  , wX0+mX-mX0, wY0+mY-mY0
    }
Return

4. 还有什么好玩的?

关闭显示器

#o::
  Sleep 1000 ; 让用户有机会释放按键 (以防释放它们时再次唤醒显示器).
  ; 关闭显示器:
  SendMessage, 0x112, 0xF170, 2,, Program Manager ; 0x112WM_SYSCOMMAND, 0xF170SC_MONITORPOWER.
  ; 对上面命令的注释: 使用 -1 代替 2 来打开显示器.
  ; 使用 1 代替 2 来激活显示器的节能模式.
return

“锁定”电脑

输入 ;;lock 锁定电脑,输入 magic 才能解锁

:*:;;lock::
  key:= "magic"
  i:=1
  BlockInput MouseMove
  MouseMove,3, 3
  ; MsgBox,  , Locked up, 鼠标键盘已锁定
  Gui, locker:New
  gui, locker:
  gui, font,s15,MS sans serif
  Gui, locker:Color,0x00000000
  Gui, locker:Add, Text,x280 y15 cWhite , Mouse and Keyboard Locked
  Gui ,locker:+AlwaysOnTop -Caption +Owner
  Gui, locker:Show,W800 H60 Center
  Loop
  {
    Input, c, L1
    if(c = SubStr(key, i, 1)){
      i++
    }
    if(i>StrLen(key)){
      BlockInput, MouseMoveOff
      BlockInput, Off
      Gui, Hide
      GuiClose:
        break
    }
  }
Return

番茄钟

;;pom 唤醒或停止

CapsLock & p::
global showWin:=not showWin
Gosub, ToggleApp
Return

CapsLock & z::
    isPomo:=not isPomo
Return

CapsLock & q::
:*:qwer::
:*:;;pom::
    if not isInited{
        isInited:=True
        Gosub, Init
    }
    WinShow, sPomodoro
    Gui sPomo:+LastFound 

    if(PomoRunning){
        PomoRunning:=False
        GuiControl,sPomo:,PomoStatus,(╯°Д° ) ╯ ┻━┻
        GuiControl,sPomo:Move,PomoStatus,x5
        WinSet, Region,0-0 w150 h40 R2-2
        Goto, ClosePomo
    }
    PomoRunning:=True
    GuiControl,sPomo:,PomoStatus,(╯°Д° ) ╯
    WinSet, Region,0-0 w100 h40 R2-2
    ; Gui, sPomo:Show,NoActivate
    WinMove, sPomodoro,,0,%yPos%
    Goto, StartPomo
Return


Init:
    ;; Variables
    showWin=true
    isPomo:=True  ;; Pomo or EyeCare
    AppRunning:=True
    PomoRunning:=False
    tomatoTime:=1500 ;;25min*60s
    readingTime:=1200 ;;20min*60s
    eyeCareTime:=20000 ;;20s*1000ms
    relaxTime:=300000 ;;5min*60s*1000ms
    yPos:=A_ScreenHeight*0.85
    isOnTop:=True
    isHidden:=False
    ;; Register Windows Events
    OnMessage(0x200,"WM_MOUSEMOVE")
    OnMessage(0x201, "WM_LBUTTONDOWN")
    OnMessage(0x202, "WM_LBUTTONUP")
    OnMessage(0x204, "WM_RBUTTONDOWN")
    ;; Monitor tasks
    SetTimer, CheckFullScrn, 3000    ; Periodically check Fullscreen application
    ;; GUI for eye care
    Gui ,eyeCare:+LastFound +AlwaysOnTop 
    Gui, eyeCare:-Caption +Owner -SysMenu
    gui, eyeCare:font,s16 w700 q5,Tahoma
    Gui, eyeCare:Color,000000
    WinSet, Transparent, 180
    xx:=A_ScreenWidth/3
    Gui, eyeCare: Add,Text,cWhite w%A_ScreenWidth% x%xx%, 眼睛可以休息一下
    Gui,eyeCare:Show,NoActivate center, eyeRelax
    WinHide,eyeRelax
    ;; GUI
    Gui ,sPomo:+LastFound +AlwaysOnTop 
    Gui, sPomo:-Caption +Owner -SysMenu
    gui, sPomo:font,s16 w700 q5,Tahoma
    Gui, sPomo:Color,000000
    Gui, sPomo:Add, Text, xp-15 yp+7 w200 cWhite vPomoStatus,(╯°Д° ) ╯ 
    WinSet, Transparent, 210
    WinSet, Region,0-0 w100 h40 R2-2
    Gui, sPomo:Show,NoActivate x0 y%yPos%,sPomodoro

    ;; Righ Click Menu
    Menu, rMenu, Add, Hide, ToggleApp
    Menu, rMenu, Add  ; Add a separator line.
    Menu, rMenu, Add, sPomo by @GnimOay, rMenuHandler
    ;; Tray Menu
    Menu,Tray,Add
    Menu,Tray,Add,Heyyyy,ClosePomo
Return

StartPomo:
    WinShow, sPomodoro
    WinHide,eyeRelax
    SoundPlay, start.mp3
    SetTimer, StartPomo, Off
    SetTimer, TickTick, 1000
    timeElapsed:=0
    energy:=1
    min:=1
Return

TickTick:
    timeElapsed+=1.0  ;; +1s
    totalTime:=isPomo?tomatoTime:readingTime
    timeLeft:=totalTime-timeElapsed
    min:=Format("{:02i}",Floor(timeLeft/60))
    sec:=Format("{:02i}",Floor(timeLeft-min*60))
    ; energy:=1-timeElapsed/()
    if(energy<=0 or min<0){
        SetTimer, TickTick, Off
        SoundPlay, end.mp3
        Goto, Relax
    }
    ; display:=energy*10
    display=%min%`:%sec%
    GuiControl,sPomo:Text,PomoStatus,%display%
    GuiControl,sPomo:Move,PomoStatus,x18
Return

Relax:
    if(isPomo){
        GuiControl,sPomo:,PomoStatus,(╯°Д° ) ╯
        GuiControl,sPomo:Move,PomoStatus,x5
    }
    else{
        WinHide,sPomodoro
        WinShow, eyeRelax
    }
    tmpTime:=isPomo?relaxTime:eyeCareTime
    SetTimer, StartPomo, %tmpTime%
Return

ClosePomo:
    SetTimer, TickTick, Off
    SetTimer, StartPomo, Off
    Goto, CloseBeep
    ; Gui,sPomo:Destroy
Return

CloseBeep:
    SoundBeep,800, 230 
Return

ToggleApp:
  if not showWin
    WinHide,sPomodoro
  Else
    WinShow, sPomodoro
Return


rMenuHandler:
; Tooltip, You selected %A_ThisMenuItem% from the menu %A_ThisMenu%.
return


WM_MOUSEMOVE( wparam, lparam, msg, hwnd ){
    if wparam = 1 ; LButton
        PostMessage, 0xA1, 2 ; WM_NCLBUTTONDOWN
}

WM_RBUTTONDOWN(){
    Menu,rMenu,Show
}

CheckFullScrn:
    global showWin
    WinGetActiveTitle, title
    if isWindowFullScreen(title){
        if not isHidden{
            WinHide,sPomodoro
            isHidden:=True
        }
    }
    else if isHidden and showWin{
        WinShow, sPomodoro
        isHidden:=False
    }
Return

isWindowFullScreen( winTitle ) {
	;checks if the specified window is full screen
	
	winID := WinExist( winTitle )

	If ( !winID )
		Return false

	WinGet style, Style, ahk_id %WinID%
	WinGetPos ,,,winW,winH, %winTitle%
	; 0x800000 is WS_BORDER.
	; 0x20000000 is WS_MINIMIZE.
	; no border and not minimized
	Return ((style & 0x20800000) or winH < A_ScreenHeight or winW < A_ScreenWidth) ? false : true
}