フットスイッチの設定をするGUIを作ってみた話

こんにちは。
この記事は↓の続きです。
smooth-pudding.hatenablog.com

キーの設定が面倒くさい

前回の記事で、キーを割り当ててフットスイッチを使えるようになりました。ただ、設定するには

  • footswitch のバイナリのあるディレクトリに移動して
  • 特定のコマンドを打って
  • 管理者パスワードを打つ

が必要で非常に面倒くさい。なんとかしようと思いました。
そもそもの話、設定したいキーバインドなんて数通りしかありません。せいぜいターミナルを起動するかプログラムを実行するかぐらいでしょう。であれば、設定のリストを表示してそこから選んで設定できたら楽じゃね?と考えました。

Python 便利

で、Python で作りました:
f:id:mat_der_D:20210524214535p:plain
使い方は簡単、起動して設定したいラジオボタンを選択して、管理者権限のパスワードを入力してOK押すだけです。一回入力すればその後はパスワード入力も不要です。ちなみにこのアプリは ctrl+shift+alt+F で起動するようにしました。なんと便利なんでしょう。
ポイントはこんな感じです↓

  1. footswitch を設定する実行ファイルを sudo で見える場所に置く(私は /usr/bin に起きました)
  2. python コードを実行可能にして作る(#!/usr/bin/python3 を一行目に設定して chmod 755 する)
  3. ショートカットキーに↑の python ファイルを割り当てる
  4. 触りやすいところにコードを配置しておき、横にパスの通った場所にコピーするシェルスクリプトをおいておく

せっかくなのでひとつひとつ記録に残しておきます。

footswitch を設定する実行ファイルを sudo で見える場所に置く

これはそのまんまです。前記事で紹介した github のコードをビルドしてできるコードを /usr/bin にコピーするだけです

$ cp footswitch /usr/bin/

python コードを実行可能にして作る

Python コード自体はこの記事の最後に載せますが、一行目にそのスクリプトを実行するインタプリタのパスを書いておいて権限を適当に定めると、単にそのファイルを叩くだけで実行できるようになります。私の環境だと /usr/bin/python3 で実行できるように #!/usr/bin/python3 を一行目に足しました。あと chmod コマンドを使って権限を書き換えました:

$ chmod 755 footswitch_gui  # .py はあってもいいけど気分で除去

こちらもパスが通っている場所におけばいいのですが、こちらの起動は sudo 権限がなくて良いので、/home/pudding/.local/bin においておくことにしました:

$ cp footswitch_gui /home/pudding/.local/bin/

ショートカットキーに↑の python ファイルを割り当てる

Ubuntu だと「設定 > キーボードショートカット」から自分でショートカットキーを割り当てることができます。一番下の + をクリックすると下のような画面が出てきます:
f:id:mat_der_D:20210524220402p:plain
名前は適当に、コマンドは footswitch_gui を設定し、「ショートカットを設定」から適当なキーバインドを割り当てます。私は絶対にかぶらなさそうな ctrl+shift+alt+F にしました。

触りやすいところにコードを配置しておき、横にパスの通った場所にコピーするシェルスクリプトをおいておく

私の PC では大体同じところに python ファイルをおいているので、footswitch の guiスクリプトも同じところにおいておきたいという気持ちがあります。それに対応するために、超単純な↓のスクリプト(install_gui.sh)を書きました:

#!/usr/bin/sh
cp footswitch_gui.py ~/.local/bin/footswitch_gui

これで Python コードをいじったときもターミナルで ./install_gui.sh と打てばリリース完了です。

ソースコード

最後に pythonソースコードをおいておきます。似たツールを作りたい方の参考になれば。

#!/usr/bin/python3
from enum import Enum
import subprocess
import tkinter as tk
from tkinter import messagebox
import tkinter.ttk as ttk


class NoPasswordError(Exception):
    pass


class Arg(Enum):
    # @formatter:off
    DEFAULT          = ("Do Nothing"      , ""                      )
    OPEN_TERMINAL    = ("Open Terminal"   , "-m ctrl -m alt -k t"   )
    PYCHARM_DEBUG    = ("PyCharm Debug"   , "-m shift -m alt -k F9" )
    PYCHARM_EXECUTE  = ("PyCharm Run"     , "-m shift -m alt -k F10")
    VSCODE_BUILD     = ("VSCode Build"    , "-m ctrl -m shift -k b" )
    OVERLEAF_COMPILE = ("Overleaf Compile", "-m ctrl -k s"          )
    # @formatter:on

    def __init__(self, alias, command):
        self.alias = alias
        self.command = command

    @classmethod
    def from_string(cls, string: str) -> "Arg":
        for arg in Arg:
            if arg.alias == string:
                return arg
        raise ValueError(f"no argument found with alias {string}")


class MainWindow(ttk.Frame):
    def __init__(self, master):
        super().__init__(master)
        self.pack(fill=tk.BOTH, expand=True)
        self.create_variable()
        self.config_master()
        self.create_widget()

    def create_variable(self):
        self.strvar_radio = tk.StringVar()
        self.strvar_radio.set(Arg.DEFAULT.alias)
        self.strvar_passwd = tk.StringVar()

    def config_master(self):
        self.master.title("FootSwitchManager")
        self.master.geometry("270x270")
        self.master.resizable(height=False, width=False)
        self.master.bind("<Return>", lambda e: self.click_button_ok())
        self.master.protocol("WM_DELETE_WINDOW", self.click_x)

    def create_widget(self):
        for arg in Arg:
            tk.Radiobutton(
                self,
                text=arg.alias,
                value=arg.alias,
                variable=self.strvar_radio,
                anchor=tk.W,
            ).pack(expand=True, fill=tk.BOTH)

        tk.Label(text="< Password >").pack(expand=True, fill=tk.BOTH)

        self.entry_passwd = tk.Entry(
            show="*",
            textvariable=self.strvar_passwd,
        )
        self.entry_passwd.pack(expand=True, fill=tk.BOTH, padx=5)

        button_ok = tk.Button(text="OK", command=self.click_button_ok)
        button_ok.pack(expand=True, fill=tk.BOTH, padx=20, pady=5)

    def click_x(self):
        ok_to_quit = messagebox.askyesno(
            title="Quit",
            message="Quit?",
        )
        if ok_to_quit:
            self.master.quit()
            self.master.destroy()

    def click_button_ok(self):
        try:
            arg = Arg.from_string(self.strvar_radio.get())
            passwd_str = self.strvar_passwd.get()
            if not passwd_str:
                raise NoPasswordError
            passwd_bytes = (passwd_str + "\n").encode()
            self.execute_command(arg, passwd_bytes)
        except NoPasswordError:
            messagebox.showerror(
                title="Error",
                message="Please input the admin password.",
            )
            return
        except subprocess.CalledProcessError:
            messagebox.showerror(
                title="Error",
                message="The password you typed is wrong."
            )
            return
        except Exception as e:
            messagebox.showerror(
                title="Error",
                message=f"Some unexpected error occurred:\n{e}",
            )
            return
        else:
            messagebox.showinfo(
                title="Information",
                message="Successfully configured the foot switch.",
            )
            self.entry_passwd.configure(state="disabled")

    @staticmethod
    def execute_command(arg: Arg, passwd: bytes):
        command = f"sudo -S footswitch -2 {arg.command}"
        subprocess.run(command.split(), input=passwd, check=True)


if __name__ == "__main__":
    app = MainWindow(tk.Tk())
    app.mainloop()