Pythonista3 のKeyboard で、Pythonista3 のコーディングを楽にする。人生を豊かにする。

この記事は、[Pythonista3 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pythonista3) の04日目の記事です。

https://qiita.com/advent-calendar/2022/pythonista3

一方的な偏った目線で、Pythonista3 を紹介していきます。

ほぼ毎日iPhone（Pythonista3）で、コーディングをしている者です。よろしくお願いします。

以下、私の2022年12月時点の環境です。

```
--- SYSTEM INFORMATION ---
**System Information**

* Pythonista 3.3 (330025), Default interpreter 3.6.1
* iOS 16.0.2, model iPhone12,1, resolution (portrait) 828.0 x 1792.0 @ 2.0
```

他の環境(iPad や端末の種類、iOS のバージョン違い)では、意図としない挙動(エラーになる)なる場合もあります。ご了承ください。

ちなみに、`model iPhone12,1` は、iPhone11 です。

## 先にこっちを紹介しろ😡

![img221116_193630.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/9f882fe0-301f-6180-22e8-70147ad2c781.gif)


![img221116_193950.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/abb605df-50fa-0227-32f9-da6a2a5d4640.png)


![img221116_193958.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/1c052abe-df54-63c2-f7ad-8f998adebe36.png)



人生が豊かになるキーボードぉー☺️


![img221116_194037.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/2e84da49-7cfe-b2ff-7576-22e1e0d76bda.png)


今回は私がPythonista3 にて、高頻度使用しているスクリプトを紹介します。

初回で紹介せず、本当に申し訳ない気持ちでいっぱいです。本当にそれくらい便利になるので、紹介するのが楽しみです。

みんなで人生豊かになりましょう！

## PyKeys でカスタムして、人生豊かに


![img221116_200131.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/0655b0bf-9dbf-1ca7-92aa-bd0a02560256.gif)



GIF では、何を押しているか分かりづらいですね。。。

自分で設定した要素を押し文字を入力し、コピーしたものを「📝」を押して、ペーストしています。


![img221116_200502.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/85f6b197-c9fd-f433-7396-f08f4fc51087.png)



設定する要素は、自分で選べます。ペーストも、わざわざ画面長押ししてpopup の「Paste」を選択せずに、ワンアクションで貼り付けができます。

### `keyboard` モジュール

[keyboard — Utilities for the Pythonista Keyboard — Python 3.6.1 documentation](https://omz-software.com/pythonista/docs/ios/keyboard.html)

https://omz-software.com/pythonista/docs/ios/keyboard.html


> keyboardモジュールは、Pythonistaのカスタムキーボード("PyKeys")を拡張するための様々な関数を提供します。

Pythonista3 にカスタムできるキーボードが用意されており、`keyboard` モジュールを使って色々とできるわけですね。

Documentation には`ui.View` と書かれていて、難しそうな印象がありますね。

大丈夫です。安心してください☺️

もう、Pythonista3 に最低限のコードは用意されています。

### サンプルコード`Examples/Keyboard/Special Characters.py`

以前2日目の記事にて、`Examples` フォルダを紹介しました。`Keyboard` 内にたくさんのサンプルコードがあります。

`Special Characters.py` を借用 & 一部変更をし実装します。

### コード全貌

基本的に変更点は

- `characters`
  - 使いたい文字列
- `def layout`
  - キーのサイズを気持ち小さめに
- `clipboard`
  - `(?)` ヘルプの部分に、「📝」ペースト

程度で、残りはほぼサンプルコードを使っています。

私の環境では、Reformat Code を使うと、最終行に`)` が実現してしまいます。細かく追ってないのですが。。。

```py
import keyboard
import ui
import clipboard

# You can modify or extend this list to change the characters that are shown in the keyboard extension:
characters = [
  '📝', '#', '-', '*', '/', '%', '`', '<', '>', '[', ']', '1', '2', '3', '4',
  '5', '6', '7', '8', '9', '0'
]


class CharsView(ui.View):
  def __init__(self, *args, **kwargs):
    super().__init__(self, *args, **kwargs)
    self.background_color = '#333'
    self.scroll_view = ui.ScrollView(frame=self.bounds, flex='WH')
    self.scroll_view.paging_enabled = True
    self.scroll_view.shows_horizontal_scroll_indicator = False
    self.add_subview(self.scroll_view)
    self.buttons = []
    for c in characters:
      button = ui.Button(title=c)
      button.font = ('<System>', 16)
      button.background_color = (1, 1, 1, 0.1)
      button.tint_color = 'white'
      button.corner_radius = 4
      button.action = self.button_action
      self.scroll_view.add_subview(button)
      self.buttons.append(button)

  def layout(self):
    rows = max(1, int(self.bounds.h / 36))
    bw = 32  # 44
    h = (self.bounds.h / rows) - 4
    x, y = 2.5, 2
    for button in self.buttons:
      button.frame = (x, y, bw, h)
      y += h + 4
      if y + h > self.bounds.h:
        y = 2
        x += bw + 4
    self.scroll_view.content_size = (
      (len(self.buttons) / rows + 1) * (bw + 4) + 40, 0)

  def button_action(self, sender):
    if sender.title == '📝':
      text = clipboard.get()
    else:
      text = sender.title

    if keyboard.is_keyboard():
      keyboard.play_input_click()
      keyboard.insert_text(text)
    else:
      print('Keyboard input:', text)


def main():
  v = CharsView(frame=(0, 0, 320, 40))
  if keyboard.is_keyboard():
    keyboard.set_view(v, 'current')
  else:
    # For debugging in the main app:
    v.name = 'Keyboard Preview'
    v.present('sheet')


if __name__ == '__main__':
  main()

```

### おこだわりを聞いておくれー

Extended Keyboard で恩恵を受けつつ、Python とMarkdown 双方がいい感じに書けるような配置にしてみています。

Extended Keyboard 上で、長押しすれば出てくるけども、使用頻度が高いキーに簡単にアクセスできるように。

いま、こうしてAdvent Calendar をPythonista3 で書けているのも、PyKeys あってこそですね☺️

| Key | Python | Markdown |
| ---- | ---- | ----|
| `#` | コメント | ヘッダー|
| `-` | 減算・マイナス | 箇条書きリスト |
| `*` | 乗算 | 箇条書きリスト・強調 |
| `/` | 除算・ファイルパス区切り | - |
| バッククォート | - | code |
| `>` | - | 引用 |
| `[`・`]`| 配列 | リンク |

これを派生させGLSL やJavaScript 用のキーボード設定なども作っています。

### 設定で「フルアクセスを許可」

ペースト機能をPyKeys で使用には、許可設定が必要です。

1. 設定App（Pythonista3 App 内のsetting ではありません）を開く
2. （下にずっーとスクロールしていった）Pythonista3 アイコンをタップ
3. 「キーボード」 をタップ
4. 「PyKeys」と「フルアクセスを許可」をアクティブ


![img221117_131257.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/1de01d69-0685-e2e1-e6c2-fca063b4e054.png)


![img221117_131305.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/155c86ad-04ff-b6a9-c02c-0851198407e5.png)


「PyKeysに設定しても、ペーストができない😭」って方は、設定を確認してみてください。


## ファイル新規作成で人生豊かに

ファイル名はどうでもいいから「とりあえず実行して、挙動や結果を知りたい」って場面ありませんか？

`Untitled.py` でもいいけど（Pythonista3 のデフォルトファイル名）`_1.py`, `_2.py`... と続いていくのも気持ちがいいものではありません。また、見返してみると、`Untitled` 祭りで何がなんだかわからなくなりますし（矛盾）。

![img221117_162650.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/674366ad-7f9f-6f56-d98b-32c7ba768198.gif)



（またもや登場の）Editor Action を使って、いい感じに新規ファイル作成するスクリプトです。

1. 新規ファイル作成Editor Action をタップ
2. コンソール画面上に、作成案内のログが出現
3. キーボードで、整数値入力
4. return
5. `YYMMDD_HHmm.py` として新規作成
6. 新規タブで開かれる

![img221117_163435.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/cfe480cc-ea85-94e6-22c1-7b91562b3875.png)



新規ファイルは、Editor Action を実行時にアクティブなファイル階層に作成されます（ファイルパス取得できない場合、`Documents` 直下）。

### コードの全貌

コード量の多さに面食らってしまうかもですが、大半がテンプレ用に貼り付ける文字列たちなので、実際はさほど多くありません。

```py
from pathlib import Path

import arrow

from objc_util import ObjCClass
import editor
import console

# todo: set code templates
blank_value = ''

copy_value = str(editor.get_text() if editor.get_text() else blank_value)

ui_value = '''\
import ui


class View(ui.View):
  def __init__(self):
    self.bg_color = 1

  def did_load(self):
    pass

  def will_close(self):
    pass

  def draw(self):
    pass

  def layout(self):
    pass

  def touch_began(self, touch):
    pass

  def touch_moved(self, touch):
    pass

  def touch_ended(self, touch):
    pass

  def keyboard_frame_will_change(self, frame):
    pass

  def keyboard_frame_did_change(self, frame):
    pass


if __name__ == '__main__':
  view = View()
  view.present()
  #view.present(hide_title_bar=True)
  #view.present(style='fullscreen', orientations=['portrait'])

'''

shader_value = '''\
precision highp float;

uniform float u_time;
uniform vec2 u_sprite_size;
uniform float u_scale;
uniform sampler2D u_texture;
uniform vec4 u_tint_color;
uniform vec4 u_fill_color;
varying vec2 v_tex_coord;


void main(){
  float t = u_time;
  vec2 uv = v_tex_coord;
  vec2 p = (uv- vec2(0.5)) *2.0;
  
  
  uv = uv / 2.0 + vec2(0.5);
  if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)discard;
  gl_FragColor = vec4(uv.x, uv.y, 0.0, 1.0);
  //gl_FragColor = texture2D(u_texture,uv.xy);
  
}

'''


def get_feedback_generator():
  """
  call feedback ex:
  `UIImpactFeedbackGenerator.impactOccurred()`
  """
  style = 4  # 0-4 
  UIImpactFeedbackGenerator = ObjCClass('UIImpactFeedbackGenerator').new()
  UIImpactFeedbackGenerator.prepare()
  UIImpactFeedbackGenerator.initWithStyle_(style)
  return UIImpactFeedbackGenerator


class TemplateItem:
  def __init__(self, prompt, code, extension='.py'):
    self.prompt = prompt
    self.code = code
    self.extension = extension


def get_prompt(templates):
  prompt = ''
  for n, template in enumerate(templates):
    prompt += f'{n} => {template.prompt},\n'
  prompt += 'select a num, create a template file.\n'
  return prompt


def get_dirpath():
  fliepath = editor.get_path()
  if fliepath:
    dir_path = Path(f'{fliepath}').parent
  else:
    _home = Path('/').home()
    dir_path = _home / 'Documents'
  return dir_path


def get_nowtime():
  utc = arrow.utcnow().to('JST')
  now_str = utc.format('YYMMDD_HHmm')
  return now_str


def create_flie(templates):
  prompt = get_prompt(templates)
  feedback = get_feedback_generator()
  try:
    choose_num = int(input(prompt))
  except:
    print('Select the number(int) displayed. ')
    feedback.impactOccurred()
    return
  if choose_num == None:
    print('I can\'t find the number I chose.')
    feedback.impactOccurred()
    return

  choose_template = templates[choose_num]
  now_time = get_nowtime()

  root_dir = get_dirpath()
  new_file = Path(root_dir, f'{now_time}{choose_template.extension}')
  new_file.write_text(choose_template.code, encoding='utf-8')

  console.clear()
  editor.open_file(str(new_file), new_tab=True)
  console.hide_output()
  feedback.impactOccurred()


def main():
  blank = TemplateItem('blank', blank_value)
  copy = TemplateItem('copy', copy_value)
  ui = TemplateItem('ui', ui_value)
  shader = TemplateItem('shader', shader_value, '.js')

  template_list = [blank, copy, ui, shader]
  create_flie(template_list)


if __name__ == '__main__':
  main()

```

### コードのないよう

上から下に順を追って説明していきます。文法よりも、何をしてるのかベースですので、書き換える際の参考になればと思います。

エラーハンドリングがかなりガバです。気軽に気になる所は教えてください🙇

#### `copy_value`

`editor` モジュールを使い、編集中ファイルのコードをコピーしています。

文字として取得できない場合や、ファイルパス失敗でも空のファイル生成するように`if` で処理させています。

#### `ui_value` `shader_value`

`ui` モジュール（次回の記事予定）やシェーダー（GLSL）のテンプレートとして、準備してます。

使いいいテンプレートを作りたい場合は、ここを書き換えるか、参考にして追加する事も可能です。

#### `class TemplateItem`

不適切な書き方かもしれませんが、オブジェクトとして`.` 呼び出しをしたかったので、class で作っています。

辞書の`dir['key']` がしっくりこなかったのが理由です。

#### `def get_prompt(templates):`

console画面に操作指示として出す役割です。`enumerate` でindex としてカウントさせて、テンプレート一覧の配列呼び出しと同期できるようにしてます。

#### `def get_dirpath():`

もしもファイルパス取得が失敗した場合、Script Library 直下にファイルを作成できるようにしてます。

#### `def get_nowtime():`

[arrow — Python 3.6.1 documentation](https://omz-software.com/pythonista/docs/ios/undocumented/arrow.html)

https://omz-software.com/pythonista/docs/ios/undocumented/arrow.htm



`arrow` モジュールがPythonista3 には、標準で入っているので`time` モジュールがNG な理由はありません（雰囲気）。

#### `def create_flie(templates):`

このスクリプトの心臓部です。

Pythonista3 的な言及ですが、`editor` モジュールにも`editor.make_new_file([name, content])` として、ファイル作成関数は用意されています。

しかし、`.py` ファイルしか生成してくれないご様子なのです（自動保存してくれるので便利なのですが）。

今回の処理では`pathlib.Path.write_text` で、事前に設定している`pathlib` のオブジェクトに`write_text` してあげるかたちをとっています（なんとなく`pathlib` が好き）。

また、

[console — Utilities for Console Output and Various System Services — Python 3.6.1 documentation](https://omz-software.com/pythonista/docs/ios/console.html)

https://omz-software.com/pythonista/docs/ios/console.html


`console` モジュールを使い、console 画面の文字を消したり引っ込めたりしています。

#### `def main():`

テンプレートの選択ができるように、`TemplateItem` のインスタンスを配列に格納しています。

`TemplateItem` の引数として

- `prompt`
  - console に表示させたい文字列
- `code`
  - テンプレートとして、新規ファイルに書き込みたい文字列
- `extension`
  - 新規ファイルの拡張子
  - 指定しない場合は`.py`

また、`ui` と変数名を指定しまっていますが、`ui.View` 使わないし、`main` 関数内スコープだからヨシ！としてしまってます（よくない）

## 次回は

このスクリプトたちで、私の人生は豊かになっています☺️

そんな、豊かになる秘訣をみなさまに共有ができて嬉しいです。

iPhone でコードを書くという行為が、苦行のように見える方もいらしゃるかもしれませんが、こうしたスクリプトに支えられ楽しくやってます😇

次回は、`ui` モジュールについての記事です。PyKeys カスタマイズの処理や、テンプレートの中でも登場してきた`ui` を知るとPythonista3 の楽しさがより一層広がると思います。


ここまで、読んでいただきありがとうございました。

## せんでん

### Discord

Pythonista3 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。

https://t.co/0IOW2hn1VC

### 書籍

iPhone/iPad でプログラミングする最強の本。

- [Python や Jupyter で iPhone/iPad 先端機能を簡単･自由にプログラミング！「活用篇」：hirax](https://techbookfest.org/product/1WiyV4LEev3f4YrzesRKWV?productVariantID=axX32srsSgtLtZWUtjkj99)

https://techbookfest.org/product/1WiyV4LEev3f4YrzesRKWV?productVariantID=axX32srsSgtLtZWUtjkj99

- [Python や Jupyter で iPhone/iPad 先端機能を簡単･自由にプログラミング！「土台篇」：hirax](https://techbookfest.org/product/wTZTyeibm5GQ5XgdfMrEBV?productVariantID=kRDmN1udbEYZUWbETdwL8r)

https://techbookfest.org/product/wTZTyeibm5GQ5XgdfMrEBV?productVariantID=kRDmN1udbEYZUWbETdwL8r

### その他

- サンプルコード

[Pythonista3 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pythonista3) でのコードをまとめているリポジトリがあります。

コードのエラーや変なところや改善点など。ご指摘やPR お待ちしておりますー

https://github.com/pome-ta/Pythonista3AdventCalendar2022sampleCode

- Twitter

なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー

https://twitter.com/pome_ta93

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">やれるか、やれないか。ではなく、やるんだけども、紹介説明することは尽きないと思うけど、締め切り守れるか？って話よ！（クズ）<br><br>Pythonista3 Advent Calendar 2022 <a href="https://t.co/JKUxA525Pt">https://t.co/JKUxA525Pt</a> <a href="https://twitter.com/hashtag/Qiita?src=hash&amp;ref_src=twsrc%5Etfw">#Qiita</a></p>&mdash; pome-ta (@pome_ta93) <a href="https://twitter.com/pome_ta93/status/1588570697777152006?ref_src=twsrc%5Etfw">November 4, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

- GitHub

基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。

https://github.com/pome-ta