Retrofit2でmultipart × multiple images をuploadする

久しぶりにやったらドチャクソハマったのでメモ

interface Service {
    @Multipart
    @POST("upload")
    Call<Response> upload(
            @Part(/* images[] */) MultipartBody.Part[] images,
            @Part("message")
                    RequestBody message);
}
Service service = getService();

List<MultipartBody.Part> parts = new ArrayList<>();

for (String imageUrl: imageUrls) {
    Uri uri = Uri.parse(uriString);
    String mimetype = context.getContentResolver().getType(uri);
    
    if (mimetype == null) continue;

    File tempFile = createTempFile(uri);
    parts.add(MultipartBody.Part.createFormData("images[]", tempFile.getName(), RequestBody.create(MediaType.parse(mimetype), tempFile)));
} 

RequestBody messagePart = RequestBody.create(MultipartBody.FORM, message);

service.upload(parts.toArray(new MultipartBody.Part[0]), messagePart);

DroidKaigi2018を致命傷で終えて

DroidKaigi2018を終えて

スタッフとしては今年で3回目の参加です。

  • 2015年の初回は一般参加
  • 2016年は東工大で開催だったので地の利(当時在学中)を活かして当日スタッフ
  • 2017年は発表者兼通年スタッフ
  • 2018年は通年スタッフ

という感じ。 今回はセッション公募・採択・タイムテーブル周りのリーダー、また部屋担当(司会や補佐の割り振り等)のリーダーをやっていました。

セッションでは連絡係もやっていたので、松田 and/or Jumpei と名乗っていたのは僕です。
とあるスピーカーの人も同じ苗字だったので、松田(jmatsu) という意味不明な名乗り方をしたこともありました。

何か至らない点がありましたらフィードバックをお願いします!!!

セッション公募・採択・タイムテーブル

アサイン方法

DroidKaigi 2018やるぞ〜〜〜ってなってミーティングが開かれ、遅刻してのこのこ到着したときに、ひつじさんから「どう?」って言われました。 今にして思えば、トイレいってからミーティング参加すればよかった。

ニッチというトピック

今回は「ニッチ」というトピックを入れてみました。ひつじさん(代表)が。僕は「いいっすね!」って何も考えずに承認欲求ボタンを押しただけです。

どうなるのかなー・・・と不安になったんですが、結構期待度や評判もよく、 確かに例年よりもセッションの幅が広がっていたような印象を受けました。

もっと褒めてくれても良いんですよ。ひつじさんを。

CfP システム

今回は sessionize.com というサービスを利用しました。このサービスは KotlinConf 2017, 2018 でも使われています。

去年も応募してくださった方の中には覚えている方もいると思うのですが、 Google Form を使って応募を集めていました。
そのシートをmasterとし、スクリプトでメールを送っていたわけです。セッション一覧用のjsonも我らがWebサイト班渾身のスクリプト芸で生成してました。

そういう面もあって、今年は CfP 系サービスを利用しよう!という流れになったわけですね。apiもあるし。

結論からいうと「すごい助かった」し、「致命傷で済んでよかった」という感じです。

バグやら機能要望やら何やらで大量のフィードバックを投げ、リアルタイムで sessionize のメンバーから「直ったよ」と連絡をもらい、直らないものはワークアラウンドで手動修正を行い・・・などなどの手間が発生したことは否めないんですが、メールの一斉送信やタイムテーブルAPIなどは非常に楽でした。

タイムテーブル

賛否両論あるかと思います。このセッションたちが聞きたかったのに同じスロットにある!、自分の発表スロットに聞きたいセッションがある!、といった例が多そうですね。

わかる。

司会や補佐

最初のホールセッションなどでいくつか司会をしていました。イベント途中からちょっと指示出しの必要が多くなって、結局他のスタッフにシフト交代をお願いすることも多かったです。皆さま本当にありがとうございました。

ウェルカムトークの謎の司会

ウェルカムトークの司会って実は決まっていなかったというか、そんな打ち合わせは(僕の知る限り)なかったんですよね。

開始が10分遅れの時点で割と「どうすっかなー」とぐるぐる思考を巡らせていたんですが、オープニング動画を始めてもらうようお願いし、舞台が暗転したところでひつじさんが急に「なんか良い感じによろしく」って言った時は「おっ、今日は泣いて帰ろう」ってちょっと思いました。

しかも日英どっちでもいわないとダメなんじゃないの?って思ったらもうどうでもよくなりました。正直何言ったか覚えてないです。

ホール第一スロットが終わったときのアナウンス

日英両方めっちゃ巻きで話しました。それでも長くてごめんな・・・って気持ちで胸がいっぱいでした。

一応原稿は作っていったんですが、巻くためにガン無視して日英アドリブでアナウンスした結果、汚い英語で大変申し訳ありませんでした。

名札をつけてくださいと言いましたが、実は僕がつけていなかったのは内緒です。

Day1パーティー会場への誘導

大変申し訳ありませんでした。完全に輸送能力を超えるという凡ミスでした。
参加者の方々や業者の方々の多大な理解と協力があってこそ、最終的に移動が叶ったと思っています。本当にありがとうございました。

来年

英語で発表とかできたら、いいな・・・

おまけ

名札とスタッフパーカーかわいい

f:id:jmatsu:20180211152810j:plain

みーちゃんの葛藤 #musani

SHIROBAKO Advent Calendar 4日目 (https://adventar.org/calendars/2092) です。 3Dクリエイターのみーちゃんこと藤堂美沙(とうどう みさ)を取り上げます。 主人公人5人の中で唯一のアホ毛キャラです。ちなみにこのアホ毛は重力に逆らうことのできるタイプのアホ毛です。

f:id:jmatsu:20171204005626j:plain

出典: http://shirobako-anime.com/character-04.html

SHIROBAKO では主人公宮森あおいを含めたアニメーション同好会の5人がそれぞれ夢について悩み、葛藤し、そして乗り越える様が表現されます。

さて、みーちゃんは唯一転職という道を選び、葛藤を乗り越え、チャンスを得るに至ったキャラです。現職では自分のやりたいアニメには関われない・・・さあどうする?という立ち位置の葛藤が描かれます。

5人それぞれの葛藤

アニメーション同好会の5人はそれぞれ違う葛藤を抱えています。

制作 宮森

  • アニメーション会社で制作という立場におり、すでにアニメを作っているが、将来の目標がない

作画 絵麻

  • 著名な原画担当のようになりたいという目標もあり、すでにアニメに携わっているが、自分の実力を卑下して悩む

声優 ずかちゃん

  • 声優の卵になったものの仕事が来ず、この先どうなるか分からない

作家志望 りーちゃん

  • 職人的な立ち位置でもなく、どうアニメの道に進めばいいのか分からない

3Dクリエイター みーちゃん

  • 現職が自分の作りたいアニメに続いているか分からない

アニメーション会社に入った宮森と絵麻、声優プロダクションに所属するずかちゃん、まだ道の拓けている学生のりーちゃん、これら4人と異なり、すでに「ある程度の道筋に乗ってしまっている」ことそのものが悩みの根幹になります。

5人で飲んでるときも一人だけ「このまま仕事を続けていいんだろうか」という悩みをぶちまけ、仕事がとれないというずかちゃんとびっみょーに気まずい空気になる場面も・・・

みーちゃんの葛藤をもうちょい見てみる

最初、みーちゃんは3DCG制作会社に入社(1年目)しているところからスタートします。
業界では高待遇で比較的安定した会社に入社できたようで、武蔵野アニメーションの3DCG監督は「あんないいところやめちゃうの?」と表現するくらいです。

ただ会社が受ける案件の都合上、ホイールとタイヤの3Dモデリングを行なう仕事しかやっていません。アニメを作りたいという思いがありながらも、です。

f:id:jmatsu:20171204020942j:plain

出典 : http://anicobin.ldblog.jp/archives/42240273.html

今の仕事を続けるべきか、他の職場を探すべきか、答えが出せないままタイヤとホイールのモデリングがどんどん上手になっていきます。
もうこれについてはめちゃくちゃ分かりますね。今 Android 開発をやっていますが、Android OS ってこれからどれだけ続いていくんだろう・・・とか思うわけです。それで身につくのは後方互換を維持したワークアラウンドtipsで、Android以外では使えないものばかりなんですよね・・・いやなんでもないです

f:id:jmatsu:20171204005712j:plain

出典 : http://anicobin.ldblog.jp/archives/42240273.html

見てくださいこの顔、タイヤをモデリングしてるとは思えない、まるでゴミを見るときのような顔です。グッときますね!僕もこんな顔して Android 開発したい、というかこんな顔で見られながら開発したい。

まあとりあえず、現職の仕事内容と自分のやりたいこととのギャップに悩むわけです。でも実はやりたいことが具体的でないことを現職の社長に指摘され、ぐぬぬとなるシーンも・・・

f:id:jmatsu:20171204121510j:plain

出典 : http://anicobin.ldblog.jp/archives/42240273.html

転職という選択

これは消去法に見えますが、そのまま続けるという選択肢もあったと思うんですよね。

みーちゃんの最初の職場の社長の言葉に「ディレクターの土橋くんが褒めてたよ。最初は雑だったけど、ポリゴンの流れが綺麗になったって」という褒め言葉があるんですね。これ「ホイールやタイヤのモデリングがうまくなったね」ではなくて、3Dモデリングの基礎技術の向上を褒めてるんですよ。少なくとも「今転職をしなくとも基礎技術を固めることはできて、そのあとからでも転職は遅くないと思うよ」的な社長の隠れた言葉が見えるんですよ(多分)。

とはいっても転職を選ぶわけですが、ちゃんとこのタイヤモデリングが報われる日が来ます。

f:id:jmatsu:20171204120158j:plain 出典: http://anicobin.ldblog.jp/archives/43240658.html

前職でひたすらモデリングしたタイヤとホイールが自分の得意分野として認識され、新しい職場で評価されます。自分の目指すアニメとは違うと思っていた前職のスキルが、夢に近づいた新職場で役に立つ。みーちゃんもここで前の職場の社長の思いを理解したんだと思います(多分)。 Androidはまあアレですけど。

この新職場で第三少女飛行隊に関わり、5人でアニメを作るという目標を達成することができます。かっこいい。

最後に

正直 Android への雑念が溢れて収集がつかなくなってきたので、とりあえず葛藤を乗り越える前のみーちゃんとその後のみーちゃんを見ましょう。

f:id:jmatsu:20171204011847j:plain

出典 : http://anicobin.ldblog.jp/archives/42240273.html

前職で新しくホイールの仕事をもらった時の顔です。かわいい。ではなくて、嫌そうな感じが顔に出ていますね。

f:id:jmatsu:20171204012107j:plain

出典 : http://anicobin.ldblog.jp/archives/43988621.html

新職場で急遽「戦闘機の型を変更してくれ」と言われたときの顔です。どう考えてもブラックすぎてヤバイんですがすごくやる気に満ちた顔です。

まとめ

転職しなかった世界線も見たいなあ。

f:id:jmatsu:20171204125812j:plain

出典: http://anicobin.ldblog.jp/archives/41644190.html

かわいい。

Apple IDの確認コード on macOS が👎という話

複数デバイス間で Apple ID を共有していると、以下の手順を利用してログインする必要がある。

  1. Apple id とパスワードを入力
  2. 他デバイスで承認、確認コードを表示
  3. 確認コードを入力

例えば webログインを行なうと、手順3で新たに確認コード入力画面が表示される。

で、Marvericks (OS X 10.9.5) で App Store に入ろうとしたら先の手順を要求された。

f:id:jmatsu:20170527174121j:plain

↑ が手順1 を終えた段階。ここでもう1回サインインを押せば確認コード入力画面が出るのかなー、と思ったら出ない。 それどころかパスワードが間違ってるとしてロックされる。
正解は パスワード + 確認コードをパスワード欄に入力するという話。

よくよく読めば確かにそういってるんだけれど、確認コード入力画面の存在を知っていると普通にハマる。
結局ドハマリして、5回パスワードリセットをしてツラい。

もっとクソなのは iCould アカウント追加画面で、そっちに至っては確認コード入力画面も出ないし、パスワード入力画面も閉じてしまう。どうすればいいんですかね・・・

ref: https://discussionsjapan.apple.com/thread/10172876?start=0&tstart=0

Android Architecture Components の Lifecycle モジュールの APT を追う

Google I/O 2017 で Android Architecture Components が公開されました。

developer.android.com

簡単に Lifecycle の Observer を作成できます。どうやって実現しているのでしょうか?

試しに簡単な LifecycleObserver を作ってみましょう。

class MyActivity extends LifecycleActivity {
  static class MyLifecycleObserver implements LifecycleObserver {
    private static final String TAG = MyLifecycleObserver.class.getSimpleName();
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    void onResume() {
      Log.d(TAG, "onResume");
    }
  }

  void onCreate() {
    getLifecycle().addObserver(new MyLifecycleObserver());
  }
}

APT で生成された実装は以下です。

// 同一パッケージ
public class MyActivity_MyLifecycleObserver_LifecycleAdapter implements GenericLifecycleObserver {
  final MyActivity.MyLifecycleObserver mReceiver;

  MyActivity_MyLifecycleObserver_LifecycleAdapter(MyActivity.MyLifecycleObserver receiver) {
    this.mReceiver = receiver;
  }

  @Override
  public void onStateChanged(LifecycleOwner owner, Lifecycle.Event event) {
    if (event == Lifecycle.Event.ON_RESUME) {
      mReceiver.onResume();
    }
    if (event == Lifecycle.Event.ON_CREATE) {
      mReceiver.onCreate();
    }
  }

  public Object getReceiver() {
    return mReceiver;
  }
}

上記はとてもシンプルですね。分かりやすいです。では LifecycleRegistry#addObserver を追っていきます。

// in : LifecycleRegistry
@Override
public void addObserver(LifecycleObserver observer) {
  ObserverWithState observerWithState = new ObserverWithState(observer);
  mObserverSet.putIfAbsent(observer, observerWithState);
  observerWithState.sync();
}

なるほどなるほど。では ObserverWithState の中へ。

// in : LifecycleRegistry
class ObserverWithState {
  private State mObserverCurrentState = INITIALIZED;
  private GenericLifecycleObserver mCallback;

  ObserverWithState(LifecycleObserver observer) {
    mCallback = Lifecycling.getCallback(observer);
  }
}

GenericLifecycleObserver は自分では実装していませんね。APT で作成したクラスから取るのでしょう。では Lifecycling#getCallback へ。

// in : LifecycleRegistry
@NonNull
static GenericLifecycleObserver getCallback(Object object) {
  if (object instanceof GenericLifecycleObserver) {
    return (GenericLifecycleObserver) object;
  }
  ...

違いますね。次です。

// in : LifecycleRegistry
//noinspection TryWithIdenticalCatches
try {
  final Class<?> klass = object.getClass();
  Constructor<? extends GenericLifecycleObserver> cachedConstructor = sCallbackCache.get(
  klass);
  if (cachedConstructor != null) {
    return cachedConstructor.newInstance(object);
  }
  cachedConstructor = getGeneratedAdapterConstructor(klass);
  if (cachedConstructor != null) {
    sCallbackCache.put(klass, cachedConstructor);
    if (!cachedConstructor.isAccessible()) {
      cachedConstructor.setAccessible(true);
    }
    return cachedConstructor.newInstance(object);
  } else {
    sCallbackCache.put(klass, sREFLECTIVE);
  }
  return new ReflectiveGenericLifecycleObserver(object);
} catch (IllegalAccessException e) {
  throw new RuntimeException(e);
} catch (InstantiationException e) {
  throw new RuntimeException(e);
} catch (InvocationTargetException e) {
  throw new RuntimeException(e);
}

コンストラクタをキャッシュして早くしてるだけなので、一回キャッシュ周りの処理を消しましょう。

// in : LifecycleRegistry
  final Class<?> klass = object.getClass();
  cachedConstructor = getGeneratedAdapterConstructor(klass);
  if (cachedConstructor != null) {
    if (!cachedConstructor.isAccessible()) {
      cachedConstructor.setAccessible(true);
    }
    return cachedConstructor.newInstance(object);
  }
  return new ReflectiveGenericLifecycleObserver(object);

つまり getGeneratedAdapterConstructor を見ればいいわけですね。

// in : LifecycleRegistry
@Nullable
private static Constructor<? extends GenericLifecycleObserver> getGeneratedAdapterConstructor(
        Class<?> klass) {
    final String fullPackage = klass.getPackage().getName();

    String name = klass.getCanonicalName();
    // anonymous class bug:35073837
    if (name == null) {
        return null;
    }
    final String adapterName = getAdapterName(fullPackage.isEmpty() ? name :
            name.substring(fullPackage.length() + 1));
    try {
        @SuppressWarnings("unchecked")
        final Class<? extends GenericLifecycleObserver> aClass =
                (Class<? extends GenericLifecycleObserver>) Class.forName(
                        fullPackage.isEmpty() ? adapterName : fullPackage + "." + adapterName);
        return aClass.getDeclaredConstructor(klass);
    } catch (ClassNotFoundException e) {
        final Class<?> superclass = klass.getSuperclass();
        if (superclass != null) {
            return getGeneratedAdapterConstructor(superclass);
        }
    } catch (NoSuchMethodException e) {
        // this should not happen
        throw new RuntimeException(e);
    }
    return null;
}

static String getAdapterName(String className) {
    return className.replace(".", "_") + "_LifecycleAdapter";
}

bug とか書いてあって不穏ですが、canonical name は匿名クラスだと null になります。問題ありません。さて canonical name がある場合、パッケージ名等から LifecycleAdapter 名を作成します。最初の APT で生成されたクラスを作り、そのコンストラクタを返します。

// in : LifecycleRegistry
  final Class<?> klass = object.getClass();
  cachedConstructor = getGeneratedAdapterConstructor(klass);
  if (cachedConstructor != null) {
    if (!cachedConstructor.isAccessible()) {
      cachedConstructor.setAccessible(true);
    }
    return cachedConstructor.newInstance(object);
  }
  return new ReflectiveGenericLifecycleObserver(object);

コンストラクタがあれば、最初に渡した Observer を引数として GenericLifecycleObserver を、今回は MyActivity_MyLifecycleObserver_LifecycleAdapter を作成します。非常にシンプルですね。

canonical name がなかった場合は getGeneratedAdapterConstructor が null を返していましたね。つまり ReflectiveGenericLifecycleObserver なるものにたどり着きます。

f:id:jmatsu:20170522141928j:plain

その中身は黒魔術の塊・・・といいたいんですが、Reflection ではクラスを作れないので、本来は APT でクラスにしておく部分を Runtime で処理しています。

ということで、匿名クラスで LifecycleObserver を作成してしまうと初回のみコストが高いことになります。コストが高いといっても感はありますが。 それに一度生成すればオンメモリキャッシュする機構もありますし、問題はなさそうです。というか、匿名クラスで作る必要を感じないんですがそれは・・・

とりあえず定義した LifecycleObserver を wrap するクラスがあり、それを動的に生成しているようです。またその wrapper は Lifecycle State を監視していて、時が来たら LifecycleObserverの該当メソッドを叩くという非常にシンプルな構造でした。とさ。

台タイプのスタンディングデスクを買った

雑記。結論から言うと以下のスタンディングデスクを買って、なかなか良いスタンディングワークライフを送っている。

↓ こんな感じでモニターアームつけて、キーボード台は設置しないで使っている。

f:id:jmatsu:20170515125427j:plain

なんでスタンディングデスクなの?

人間は座りっぱなし・立ちっぱなしのどちらかにならない方が良いとかなんとか。
スタンディングデスクにして定期的に立ち仕事と座り仕事に切り替えると健康に良いらしい。

が、そんな理由じゃなくて、スタンディングワークはポモドーロと相性がいいと思っていることが一番の理由。
立っている間は集中して作業をする。休憩中は座ってのんびりコーヒーでも飲めばいい。
しかもスタンディングワークは(ひとによるけど)連続作動可能時間が存在するので、それを超えたら集中できない。
結果、強制的にメリハリがつく。はず。

スタンディングデスクの種類

スタンディングデスクには机そのものが昇降するタイプ、机の上に置く台タイプが存在していて、それぞれ手動/自動がある。

x 電動 ガス式 手動
机昇降式 Type-A Type-B Type-C
台昇降式 Type-D Type-E Type-F

今回買ったのは Type-E 。ガス式台昇降タイプ。
色々理由はあるけれど、身長が188cmあるせいでちょうどいい高さまで上げられる製品が少なかったので、台式 on the table にして無理矢理頑張っている。

Type-A

本当はこれが欲しかった・・・が、一番高い価格帯になる。

机全体が昇降するので、非常に使い勝手がいい。ロススペースもなく、下降時に物があると自動で止まるセンサーがついているものも多い。
性能を見ても高いのが当然で、全てがダントツトップ。耐荷重、設定可能高さ、対価年数、そして本体重量もダントツ。
一人では絶対設置しない方がいいし、結構な人が単純にできないくらいの重さと大きさがある。
そして腰に良いはずのスタンディングデスクを導入して、腰をヤッてしまったら何の意味もないので、もし導入するなら数人でやるか設置まで頼めるやつにしましょう。

ちなみに安いのもあったにはあったけれど、自分の身長だとスタンディングワークに使える高さまで上がらなかったので・・・

Type-B

これも机全体が昇降するので使い勝手はいいし、ロススペースもない。

価格は安い部類に入る。
ただ設定可能高さがあまり広くないので、その部分では使い勝手が台昇降式にも劣る可能性がある。
そもそもの高さが低いものも多く、作業には向いてないので選定外とした。

Type-C

可変なスタンディングデスクには成り得ないので除外。
しかも上に物を置いて動かすことができない。最初からスタンディングデスクとして使うならギリギリセーフというレベル。

この上に台タイプを置くことも考えたが、元々机はあったのでとりあえずスルー。

Type-D

あるの?

Type-E

今回買ったタイプ。

可動域は30cm~50cmのものが多い。また耐荷重は10kg~20kgに落ち着くので、モニターアーム + ディスプレイ * 2とかにすると厳しいものも多い。
当然なのだけど、載せる机の方が幅広でなくてはならないという制約はあるし、机の耐荷重が台重量(大体10kgはある)+台耐荷重を超えている必要がある。
立ち上げ時のロススペースはほぼない(支柱の形式次第)が、一番下げた状態にすると純粋に台の高さ分がロススペースになる。
今回買ったのはその中でも可動域や耐荷重の大きいもので、まあまあな値段になっている。
ちなみに飲み物を載せたままでも全く問題がない安定した昇降が可能。

Type-F

会社で使っている。

耐荷重が圧倒的に低く、最大の高さにすると不安定になる。そのためスタンディングワークには向いてない。
めっちゃ安いので、単純に高さ調整用の台としては優秀。

まとめ

コストに見合う価値はあったし、十二分に満足している。
あとスタンディングワークをするときは、足元にクッションを買うか、良いスリッパを買った方がいい。
今までスタンディングワークをしているときは靴を履いていたので気付かなかったが、足元がめっちゃ重要。

スタンディングワーク自体やったことがない人はとりあえず一度やってみることをオススメします。

特定のファイルの git addとかがめんどうくさいのでなんとかする

ある程度編集したときに「あー、このファイルだけコミットしておきたい」みたいな状況ってあると思うんですよ。 他にも「このファイルだけHEADの状態に戻したい」とか「unstageしたい」みたいな。

でも例えばJavaだとpackage名が一緒なファイルがずらーっと並んでしまい、補完しようにも先頭が大体一緒で本当に面倒くさい。

残念ながらJavaをやめるという選択肢がないので、gitのサブコマンドを作って対応する。

ファイル一覧から特定のファイルをaddする - git add $file

#!/usr/bin/env bash

: ${GIT_FILTER_COMMAND:=fzf}

ls_modified_files() {
  echo '--- LABEL : tracked'
  git diff --name-only
}

ls_untracked_files() {
  echo '--- LABEL : untracked'
  git ls-files --others --exclude-standard
}

main() {
  local -r selected_file=$(cat <(ls_modified_files) <(ls_untracked_files) | $GIT_FILTER_COMMAND)

  if [[ -f "$selected_file" || -d "$selected_file" ]]; then
    git add "$selected_file"
  else
    echo 'Not found or Canceled' >&2
  fi
}

main

ファイル一覧から特定のファイルをHEADの状態に戻す- git checkout -- $file

#!/usr/bin/env bash

: ${GIT_FILTER_COMMAND:=fzf}

ls_staged_files() {
  echo '--- LABEL : staged'
  git diff --name-only --staged
}

ls_modified_files() {
  echo '--- LABEL : modified'
  git diff --name-only
}

main() {
  local -r selected_file=$(cat <(ls_staged_files) <(ls_modified_files) | $GIT_FILTER_COMMAND)

  if [[ -f "$selected_file" || -d "$selected_file" ]]; then
    git checkout -- "$selected_file"
  else
    echo 'Not found or Canceled' >&2
  fi
}

main

ファイル一覧から特定のファイルをunstageする - git reset --$file

#!/usr/bin/env bash

: ${GIT_FILTER_COMMAND:=fzf}

ls_staged_files() {
  echo '--- LABEL : staged'
  git diff --name-only --staged
}

main() {
  local -r selected_file=$(cat <(ls_staged_files) | $GIT_FILTER_COMMAND)

  if [[ -f "$selected_file" || -d "$selected_file" ]]; then
    git reset -- "$selected_file"
  else
    echo 'Not found or Canceled' >&2
  fi
}

main

まだtrackしてないディレクトリの内部までほじくるとかはキャンセル処理とかが面倒なのでやってないけれど、余裕で可能だと思います。

おまけ

ファイル名を git-hoge としてPATH配下に認識させておくとgit hogeで起動できる。(サブコマンド)
拡張子をつけてエディタのファイル補完を適用したいなら、シンボリックリンクで対応すればいいと思います。