Repository: jon4hz/fztea
Branch: main
Commit: 5df5fe532c5c
Files: 24
Total size: 53.0 KB
Directory structure:
gitextract_ynrc7a8r/
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── lint.yml
│ └── release.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── LICENSE
├── README.md
├── demo.tape
├── flipperui/
│ ├── flipper.go
│ └── opts.go
├── go.mod
├── go.sum
├── internal/
│ └── version/
│ └── version.go
├── main.go
├── middleware.go
├── model.go
├── recfz/
│ ├── conn.go
│ ├── flipper.go
│ ├── reader.go
│ └── recfz.go
├── scripts/
│ ├── completions.sh
│ └── manpages.sh
└── server.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
================================================
FILE: .github/workflows/lint.yml
================================================
name: lint
on: [push, pull_request, workflow_dispatch]
jobs:
lint:
uses: jon4hz/meta/.github/workflows/lint.yml@master
================================================
FILE: .github/workflows/release.yml
================================================
---
name: goreleaser
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
permissions:
contents: write
id-token: write
packages: write
jobs:
prepare:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
flags: ""
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version: 1.22
cache: true
- shell: bash
run: |
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- uses: actions/cache@v5
if: matrix.os == 'ubuntu-latest'
with:
path: dist/linux
key: linux-${{ env.sha_short }}
- uses: actions/cache@v5
if: matrix.os == 'macos-latest'
with:
path: dist/darwin
key: darwin-${{ env.sha_short }}
- uses: actions/cache@v5
if: matrix.os == 'windows-latest'
with:
path: dist/windows
key: windows-${{ env.sha_short }}
enableCrossOsArchive: true
- if: ${{ github.event_name == 'workflow_dispatch' }}
shell: bash
run: echo "flags=--nightly" >> $GITHUB_ENV
- if: matrix.os == 'windows-latest'
shell: bash
run: echo "flags=--skip=before" >> $GITHUB_ENV # skip before hooks on windows (shell scripts for manpages and completions)
- uses: goreleaser/goreleaser-action@v6
if: steps.cache.outputs.cache-hit != 'true' # do not run if cache hit
with:
distribution: goreleaser-pro
version: latest
args: release --clean --split ${{ env.flags }}
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
release:
runs-on: ubuntu-latest
needs: prepare
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version: 1.22
cache: true
# copy the cashes from prepare
- shell: bash
run: |
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- uses: actions/cache@v5
with:
path: dist/linux
key: linux-${{ env.sha_short }}
- uses: actions/cache@v5
with:
path: dist/darwin
key: darwin-${{ env.sha_short }}
- uses: actions/cache@v5
with:
path: dist/windows
key: windows-${{ env.sha_short }}
enableCrossOsArchive: true
# release
- uses: goreleaser/goreleaser-action@v6
if: steps.cache.outputs.cache-hit != 'true' # do not run if cache hit
with:
version: latest
distribution: goreleaser-pro
args: continue --merge
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
.vscode
.ssh
completions/
manpages/
dist/
flipper_*.png
================================================
FILE: .golangci.yml
================================================
run:
tests: false
issues:
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- bodyclose
- exportloopref
- goimports
- gosec
- nilerr
- predeclared
- revive
- rowserrcheck
- sqlclosecheck
- tparallel
- unconvert
- unparam
- whitespace
================================================
FILE: .goreleaser.yml
================================================
---
version: 2
variables:
main: "."
binary_name: "fztea"
description: "TUI to interact with your flipper zero"
github_url: "https://github.com/jon4hz/fztea"
maintainer: "jonah <me@jon4hz.io>"
license: "MIT"
homepage: "https://jon4hz.io"
aur_package: |-
# bin
install -Dm755 "./fztea" "${pkgdir}/usr/bin/fztea"
# license
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/fztea/LICENSE"
# completions
mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
install -Dm644 "./completions/fztea.bash" "${pkgdir}/usr/share/bash-completion/completions/fztea"
install -Dm644 "./completions/fztea.zsh" "${pkgdir}/usr/share/zsh/site-functions/_fztea"
install -Dm644 "./completions/fztea.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/fztea.fish"
# man pages
install -Dm644 "./manpages/fztea.1.gz" "${pkgdir}/usr/share/man/man1/fztea.1.gz"
before:
hooks:
- go mod tidy
- ./scripts/completions.sh
- ./scripts/manpages.sh
builds:
- id: default
env:
- CGO_ENABLED=0
main: "{{ .Var.main }}"
binary: "{{ .Var.binary_name }}"
ldflags:
- -s
- -w
- -X github.com/jon4hz/{{ .Var.binary_name }}/internal/version.Version={{ .Version }}
- -X github.com/jon4hz/{{ .Var.binary_name }}/internal/version.Commit={{ .Commit }}
- -X github.com/jon4hz/{{ .Var.binary_name }}/internal/version.Date={{ .Date }}
- -X github.com/jon4hz/{{ .Var.binary_name }}/internal/version.BuiltBy=goreleaser
flags:
- -trimpath
goos:
- linux
goarch:
- amd64
- arm64
- "386"
- arm
goarm:
- "7"
- id: windows
env:
- CGO_ENABLED=0
main: "{{ .Var.main }}"
binary: "{{ .Var.binary_name }}"
ldflags:
- -s
- -w
- -X github.com/jon4hz/{{ .Var.binary_name }}/internal/version.Version={{ .Version }}
- -X github.com/jon4hz/{{ .Var.binary_name }}/internal/version.Commit={{ .Commit }}
- -X github.com/jon4hz/{{ .Var.binary_name }}/internal/version.Date={{ .Date }}
- -X github.com/jon4hz/{{ .Var.binary_name }}/internal/version.BuiltBy=goreleaser
flags:
- -trimpath
goos:
- windows
goarch:
- amd64
- arm64
- "386"
- arm
goarm:
- "7"
ignore:
- goos: windows
goarch: arm64
- goos: windows
goarm: "7"
- id: macOS
env:
- CGO_ENABLED=1 # required for the serial lib of fztea
main: "{{ .Var.main }}"
binary: "{{ .Var.binary_name }}"
ldflags:
- -s
- -w
- -X github.com/jon4hz/{{ .Var.binary_name }}/internal/version.Version={{ .Version }}
- -X github.com/jon4hz/{{ .Var.binary_name }}/internal/version.Commit={{ .Commit }}
- -X github.com/jon4hz/{{ .Var.binary_name }}/internal/version.Date={{ .Date }}
- -X github.com/jon4hz/{{ .Var.binary_name }}/internal/version.BuiltBy=goreleaser
flags:
- -trimpath
goos:
- darwin
ignore:
- goos: darwin
goarch: "386"
archives:
- id: default
name_template: "{{ .Var.binary_name }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}-{{ .Mips }}{{ end }}"
builds:
- default
- macOS
files:
- LICENSE*
- README*
- CHANGELOG*
- manpages/
- completions
- id: windows
name_template: "{{ .Var.binary_name }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}-{{ .Mips }}{{ end }}"
builds:
- windows
format_overrides:
- goos: windows
format: zip
files:
- LICENSE*
- README*
- CHANGELOG*
checksum:
name_template: "checksums.txt"
nfpms:
- file_name_template: "{{ .Var.binary_name }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}-{{ .Mips }}{{ end }}"
vendor: jon4hz
homepage: "{{ .Var.homepage }}"
maintainer: "{{ .Var.maintainer }}"
description: "{{ .Var.description }}"
license: "{{ .Var.license }}"
formats:
- apk
- deb
- rpm
contents:
- src: ./completions/fztea.bash
dst: /etc/bash_completion.d/fztea
- src: ./completions/fztea.fish
dst: /usr/share/fish/vendor_completions.d/fztea.fish
- src: ./completions/fztea.zsh
dst: /usr/share/zsh/site-functions/_fztea
- src: ./manpages/fztea.1.gz
dst: /usr/share/man/man1/fztea.1.gz
aurs:
- name: "{{ .Var.binary_name }}-bin"
homepage: "{{ .Var.homepage }}"
description: "{{ .Var.description }}"
maintainers:
- "{{ .Var.maintainer }}"
license: "{{ .Var.license }}"
private_key: "{{ .Env.AUR_KEY }}"
git_url: "ssh://aur@aur.archlinux.org/{{ .Var.binary_name }}-bin.git"
package: "{{ .Var.aur_package }}"
source:
enabled: true
snapshot:
version_template: "{{ incpatch .Version }}-devel"
changelog:
sort: asc
use: github
filters:
exclude:
- "^docs:"
- "^test:"
groups:
- title: "New Features"
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: "Bug fixes"
regexp: "^.*fix[(\\w)]*:+.*$"
order: 10
- title: Others
order: 999
furies:
- account: jon4hz
brews:
- name: "{{ .Var.binary_name }}"
repository:
owner: jon4hz
name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
commit_author:
name: jon4hz
email: me@jon4hz.io
homepage: "{{ .Var.homepage }}"
description: "{{ .Var.description }}"
install: |-
bin.install "{{ .Var.binary_name }}"
bash_completion.install "completions/{{ .Var.binary_name }}.bash" => "{{ .Var.binary_name }}"
zsh_completion.install "completions/{{ .Var.binary_name }}.zsh" => "_{{ .Var.binary_name }}"
fish_completion.install "completions/{{ .Var.binary_name }}.fish"
man1.install "manpages/{{ .Var.binary_name }}.1.gz"
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 Jonah
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# 🐬🧋 Fztea
[](https://github.com/jon4hz/fztea/actions/workflows/lint.yml)
[](https://github.com/jon4hz/fztea/actions/workflows/release.yml)
[](https://goreportcard.com/report/github.com/jon4hz/fztea)
[](https://img.shields.io/badge/Powered%20by-Dolphins-blue)
A [bubbletea](https://github.com/charmbracelet/bubbletea)-bubble and TUI to interact with your [flipper zero](https://flipperzero.one/).
The flipper will be automatically detected, if multiple flippers are connected, the first one will be used.
## 🚀 Installation
```bash
# using go directly
$ go install github.com/jon4hz/fztea@latest
# from aur (btw)
$ yay -S fztea-bin
# local pkg manager
## debian / ubuntu
$ dpkg -i fztea-v0.6.2-linux-amd64.deb
## rhel / fedora / suse
$ rpm -i fztea-v0.6.2-linux-amd64.rpm
## alpine
$ apk add --allow-untrusted fztea-v0.6.2-linux-amd64.apk
# homebrew (macOS & linux)
$ brew install jon4hz/homebrew-tap/fztea
# windows
# -> I'm sure you'll figure something out :)
```
## ✨ Usage
```bash
# trying to autodetect that dolphin
$ fztea
# no flipper found automatically :(
$ fztea -p /dev/ttyACM0
```
## ⚡️ SSH
fztea also allows you to start an ssh server, serving the flipper zero ui over a remote connection.
Why? - Why not!
```bash
# start the ssh server listening on localhost:2222 (default)
$ fztea server -l 127.0.0.1:2222
# connect to the server (from the same machine)
$ ssh localhost -p 2222
```
By default, `fztea` doesn't require any authentication but you can specify an `authorized_keys` file if you want to.
```bash
# use authorized_keys for authentication
$ fztea server -l 127.0.0.1:2222 -k ~/.ssh/authorized_keys
```
## 📸 Screenshots
You can take a screenshot of the flipper using `ctrl+s` at any time. `Fztea` will store the screenshot in the working directoy, by default in a 1024x512px resolution.
The size of the screenshot can be customized using the `--screenshot-resolution` flag.
```
$ fztea --screenshot-resolution=1920x1080
```
## ⌨️ Button Mapping
| Key | Flipper Event | Keypress Type
|-----------------|---------------|--------------|
| w, ↑ | up | short |
| d, → | right | short |
| s, ↓ | down | short |
| a, ← | left | short |
| o, enter, space | ok | short |
| b, back, esc | back | short |
| W, shift + ↑ | up | long |
| D, shift + → | right | long |
| S, shift + ↓ | down | long |
| A, shift + ← | left | long |
| O | ok | long |
| B | back | long |
## 🌈 Custom colors
You can set custom fore- and background colors using the `--bg-color` and `--fg-color` flags.
```
$ fztea --bg-color="#8A0000" --fg-color="#000000"
```
Results in:

## 🎬 Demo
### Local

### SSH
https://user-images.githubusercontent.com/26183582/181772189-13d7aeaa-ac26-4701-8104-a71ed218539c.mp4
================================================
FILE: demo.tape
================================================
Output .github/assets/demo.gif
Require fztea
Set Width 2048
Set Height 1074
Set Margin 20
Set MarginFill "#000000"
Set BorderRadius 10
Set WindowBar Colorful
Type fztea
Sleep 1
Enter
Sleep 3
Enter
Sleep 1
Down@700ms 2
Enter
Sleep 3
Enter
Sleep 3
Backspace 1
Sleep 2
================================================
FILE: flipperui/flipper.go
================================================
package flipperui
import (
"fmt"
"image"
"image/png"
"os"
"strings"
"sync"
"time"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/disintegration/imaging"
"github.com/flipperdevices/go-flipper"
"github.com/jon4hz/fztea/recfz"
)
const (
// building blocks to draw the flipper screen in the terminal.
fullBlock = '█'
upperHalfBlock = '▀'
lowerHalfBlock = '▄'
// screen size of the flipper
flipperScreenHeight = 32
flipperScreenWidth = 128
// fzEventCoolDown is the time that must pass between two events that are sent to the flipper.
// That poor serial connection can handle only so much :(
fzEventCoolDown = time.Millisecond * 10
)
var (
// colors of the flipper screen
colorBg = lipgloss.Color("#FF8C00")
colorFg = lipgloss.Color("#000000")
)
type (
// ScreenMsg is a message that is sent when the flipper sends a screen update.
ScreenMsg struct {
screen string
image image.Image
}
)
// ErrStyle is the style of the error message
var ErrStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000"))
// Model represents the flipper model.
// It also implements the bubbletea.Model interface.
type Model struct {
// viewport is used to handle resizing easily
viewport viewport.Model
// fz is the flipper zero device
fz *recfz.FlipperZero
// err represents the last error that occurred. It will be displayed for a few seconds.
err error
// errTime is the time when the last error occurred
errTime time.Time
// content is the current screen of the flipper as a string
content string
// lastFZEvent is the time of the last event that was sent to the flipper.
lastFZEvent time.Time
// screenUpdate is a channel that receives screen updates from the flipper
screenUpdate <-chan ScreenMsg
// currentScreen is the last screen that was received from the flipper
currentScreen image.Image
// mutex to ensure that only one goroutine can send events to the flipper at a time
mu *sync.Mutex
// resolution of the screenshots
screenshotResolution struct {
width int
height int
}
// Style is the style of the flipper screen
Style lipgloss.Style
// bgColor is the background color of the flipper screen
bgColor string
// fgColor is the foreground color of the flipper screen
fgColor string
}
var _ tea.Model = (*Model)(nil)
// New constructs a new flipper model.
func New(fz *recfz.FlipperZero, screenUpdate <-chan ScreenMsg, opts ...FlipperOpts) tea.Model {
m := Model{
fz: fz,
viewport: viewport.New(flipperScreenWidth, flipperScreenHeight),
lastFZEvent: time.Now().Add(-fzEventCoolDown),
screenUpdate: screenUpdate,
mu: &sync.Mutex{},
screenshotResolution: struct {
width int
height int
}{
width: 1024,
height: 512,
},
bgColor: "#FF8C00",
fgColor: "#000000",
}
m.viewport.MouseWheelEnabled = false
for _, opt := range opts {
opt(&m)
}
colorBg = lipgloss.Color(m.bgColor)
colorFg = lipgloss.Color(m.fgColor)
m.Style = lipgloss.NewStyle().Background(colorBg).Foreground(colorFg)
return &m
}
// Init is the bubbletea init function.
// the initial listenScreenUpdate command is started here.
func (m Model) Init() tea.Cmd {
return listenScreenUpdate(m.screenUpdate)
}
// listenScreenUpdate listens for screen updates from the flipper and returns them as tea.Cmds.
func listenScreenUpdate(u <-chan ScreenMsg) tea.Cmd {
return func() tea.Msg {
return <-u
}
}
// Update is the bubbletea update function and handles all tea.Msgs.
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC:
return nil, tea.Quit
case tea.KeyCtrlS:
m.saveImage()
return m, nil
default:
key, getlong := mapKey(msg)
if key != -1 {
m.sendFlipperEvent(key, getlong)
}
}
case tea.MouseMsg:
event := mapMouse(msg)
if event != -1 {
m.sendFlipperEvent(event, false)
}
case tea.WindowSizeMsg:
m.viewport.Width = min(msg.Width, flipperScreenWidth)
m.viewport.Height = min(msg.Height, flipperScreenHeight)
m.viewport.SetContent(m.Style.Render(m.content))
case ScreenMsg:
m.content = msg.screen
m.currentScreen = msg.image
m.viewport.SetContent(m.Style.Render(m.content))
cmds = append(cmds, listenScreenUpdate(m.screenUpdate))
}
return m, tea.Batch(cmds...)
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// mapKey maps a tea.KeyMsg to a flipper.InputKey
func mapKey(key tea.KeyMsg) (flipper.InputKey, bool) {
switch key.String() {
case "w", "up":
return flipper.InputKeyUp, false
case "a", "left":
return flipper.InputKeyLeft, false
case "s", "down":
return flipper.InputKeyDown, false
case "d", "right":
return flipper.InputKeyRight, false
case "o", "enter", " ":
return flipper.InputKeyOk, false
case "b", "backspace", "esc":
return flipper.InputKeyBack, false
case "W", "shift+up":
return flipper.InputKeyUp, true
case "A", "shift+left":
return flipper.InputKeyLeft, true
case "S", "shift+down":
return flipper.InputKeyDown, true
case "D", "shift+right":
return flipper.InputKeyRight, true
case "O":
return flipper.InputKeyOk, true
case "B":
return flipper.InputKeyBack, true
}
return -1, false
}
// mapMouse maps a tea.MouseMsg to a flipper.InputKey
func mapMouse(event tea.MouseMsg) flipper.InputKey {
switch event.Type {
case tea.MouseWheelUp:
return flipper.InputKeyUp
case tea.MouseWheelDown:
return flipper.InputKeyDown
}
return -1
}
// sendFlipperEvent sends an event to the flipper. It ensures that at most one event is sent every fzEventCoolDown.
func (m *Model) sendFlipperEvent(event flipper.InputKey, isLong bool) {
m.mu.Lock()
defer m.mu.Unlock()
if time.Since(m.lastFZEvent) < fzEventCoolDown {
return
}
if !isLong {
m.fz.SendShortPress(event)
} else {
m.fz.SendLongPress(event)
}
m.lastFZEvent = time.Now()
}
// View renders the flipper screen or an error message if there was an error.
func (m Model) View() string {
if m.err != nil && time.Since(m.errTime) < time.Second*4 {
return ErrStyle.Render(fmt.Sprintf("%d %s", int((time.Second*4 - time.Since(m.errTime)).Seconds()), m.err))
}
return m.viewport.View()
}
// UpdateScreen renders the terminal screen based on the flipper screen.
// It also returns the flipper screen as an image.
// This function is intended to be used as a callback for the flipper.
func UpdateScreen(updates chan<- ScreenMsg) func(frame flipper.ScreenFrame) {
return func(frame flipper.ScreenFrame) {
var s strings.Builder
for y := 0; y < 64; y += 2 {
var l strings.Builder
for x := 0; x < 128; x++ {
r := fullBlock
if !frame.IsPixelSet(x, y) && frame.IsPixelSet(x, y+1) {
r = lowerHalfBlock
}
if frame.IsPixelSet(x, y) && !frame.IsPixelSet(x, y+1) {
r = upperHalfBlock
}
if !frame.IsPixelSet(x, y) && !frame.IsPixelSet(x, y+1) {
r = ' '
}
l.WriteRune(r)
}
s.WriteString(l.String())
// if not last line
if y < 62 {
s.WriteRune('\n')
}
}
// make sure we don't block
go func() {
updates <- ScreenMsg{
screen: s.String(),
image: frame.ToImage(colorFg, colorBg),
}
}()
}
}
// saveImage saves the current screen as a png image.
func (m *Model) saveImage() {
resImg := imaging.Resize(m.currentScreen, m.screenshotResolution.width, m.screenshotResolution.height, imaging.Box)
out, err := os.Create(fmt.Sprintf("flipper_%s.png", time.Now().Format("20060102150405")))
if err != nil {
m.setError(err)
return
}
defer out.Close()
if err := png.Encode(out, resImg); err != nil {
m.setError(err)
}
}
// setError sets the error message and the time when it occurred.
func (m *Model) setError(err error) {
m.err = err
m.errTime = time.Now()
}
================================================
FILE: flipperui/opts.go
================================================
package flipperui
// FlipperOpts represents an optional configuration for the flipper model.
type FlipperOpts func(*Model)
// WithScreenshotResolution sets the resolution of the screenshot.
func WithScreenshotResolution(width, height int) FlipperOpts {
return func(m *Model) {
m.screenshotResolution.width = width
m.screenshotResolution.height = height
}
}
// WithFgColor sets the foreground color of the flipper screen.
func WithFgColor(color string) FlipperOpts {
return func(m *Model) {
m.fgColor = color
}
}
// WithBgColor sets the background color of the flipper screen.
func WithBgColor(color string) FlipperOpts {
return func(m *Model) {
m.bgColor = color
}
}
================================================
FILE: go.mod
================================================
module github.com/jon4hz/fztea
go 1.24.0
toolchain go1.24.1
require (
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/ssh v0.0.0-20250128164007-98fd5ae11894
github.com/charmbracelet/wish v1.4.7
github.com/disintegration/imaging v1.6.2
github.com/flipperdevices/go-flipper v0.6.0
github.com/muesli/coral v1.0.0
github.com/muesli/mango-coral v1.0.1
github.com/muesli/roff v0.1.0
go.bug.st/serial v1.6.4
)
require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/keygen v0.5.3 // indirect
github.com/charmbracelet/log v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/conpty v0.1.0 // indirect
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect
github.com/charmbracelet/x/input v0.3.4 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/charmbracelet/x/termios v0.1.0 // indirect
github.com/charmbracelet/x/windows v0.2.0 // indirect
github.com/creack/goselect v0.1.2 // indirect
github.com/creack/pty v1.1.21 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.1.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
)
================================================
FILE: go.sum
================================================
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/keygen v0.5.3 h1:2MSDC62OUbDy6VmjIE2jM24LuXUvKywLCmaJDmr/Z/4=
github.com/charmbracelet/keygen v0.5.3/go.mod h1:TcpNoMAO5GSmhx3SgcEMqCrtn8BahKhB8AlwnLjRUpk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk=
github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I=
github.com/charmbracelet/ssh v0.0.0-20250128164007-98fd5ae11894 h1:Ffon9TbltLGBsT6XE//YvNuu4OAaThXioqalhH11xEw=
github.com/charmbracelet/ssh v0.0.0-20250128164007-98fd5ae11894/go.mod h1:hg+I6gvlMl16nS9ZzQNgBIrrCasGwEw0QiLsDcP01Ko=
github.com/charmbracelet/wish v1.4.7 h1:O+jdLac3s6GaqkOHHSwezejNK04vl6VjO1A+hl8J8Yc=
github.com/charmbracelet/wish v1.4.7/go.mod h1:OBZ8vC62JC5cvbxJLh+bIWtG7Ctmct+ewziuUWK+G14=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/input v0.3.4 h1:Mujmnv/4DaitU0p+kIsrlfZl/UlmeLKw1wAP3e1fMN0=
github.com/charmbracelet/x/input v0.3.4/go.mod h1:JI8RcvdZWQIhn09VzeK3hdp4lTz7+yhiEdpEQtZN+2c=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/termios v0.1.0 h1:y4rjAHeFksBAfGbkRDmVinMg7x7DELIGAFbdNvxg97k=
github.com/charmbracelet/x/termios v0.1.0/go.mod h1:H/EVv/KRnrYjz+fCYa9bsKdqF3S8ouDK0AZEbG7r+/U=
github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/flipperdevices/go-flipper v0.6.0 h1:e9M8anZc7wCi0BJK37MfevYefdfifslHzTV7CEtYrKI=
github.com/flipperdevices/go-flipper v0.6.0/go.mod h1:rXHKrpiUSl2H3NM4lDj4V9P8xR/xTjlfmCWS1W9XUaw=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/coral v1.0.0 h1:odyqkoEg4aJAINOzvnjN4tUsdp+Zleccs7tRIAkkYzU=
github.com/muesli/coral v1.0.0/go.mod h1:bf91M/dkp7iHQw73HOoR9PekdTJMTD6ihJgWoDitde8=
github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI=
github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/mango-coral v1.0.1 h1:W3nGbUC/q5vLscQ6GPzteHZrJI1Msjw5Hns82o0xRkI=
github.com/muesli/mango-coral v1.0.1/go.mod h1:EPSlYH67AtcxQrxssNw6r/lMFxHTjuDoGfq9Uxxevhg=
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: internal/version/version.go
================================================
package version
var (
// Version is the current version of the application.
Version = "development"
// Commit is the git commit hash of the current version.
Commit = "none"
// Date is the build date of the current version.
Date = "unknown"
// BuiltBy is the user who built the current version.
BuiltBy = "unknown"
)
================================================
FILE: main.go
================================================
package main
import (
"fmt"
"io"
"log"
"os"
tea "github.com/charmbracelet/bubbletea"
"github.com/jon4hz/fztea/flipperui"
"github.com/jon4hz/fztea/internal/version"
"github.com/jon4hz/fztea/recfz"
"github.com/muesli/coral"
mcoral "github.com/muesli/mango-coral"
"github.com/muesli/roff"
)
var rootFlags struct {
port string
screenshotResolution string
fgColor string
bgColor string
}
var rootCmd = &coral.Command{
Use: "fztea",
Short: "TUI to interact with your flipper zero",
Version: version.Version,
Run: root,
}
func init() {
rootCmd.PersistentFlags().StringVarP(&rootFlags.port, "port", "p", "", "serial port to connect to (default: auto-detected)")
rootCmd.PersistentFlags().StringVar(&rootFlags.screenshotResolution, "screenshot-resolution", "1024x512", "screenshot resolution")
rootCmd.PersistentFlags().StringVar(&rootFlags.fgColor, "fg-color", "#000000", "foreground color")
rootCmd.PersistentFlags().StringVar(&rootFlags.bgColor, "bg-color", "#FF8C00", "background color")
rootCmd.AddCommand(serverCmd, versionCmd, manCmd)
}
func root(cmd *coral.Command, _ []string) {
// parse screenshot resolution
screenshotResolution, err := parseScreenshotResolution()
if err != nil {
log.Fatalf("failed to parse screenshot resolution: %s", err)
}
screenUpdates := make(chan flipperui.ScreenMsg)
fz, err := recfz.NewFlipperZero(
recfz.WithContext(cmd.Context()),
recfz.WithPort(rootFlags.port),
recfz.WithStreamScreenCallback(flipperui.UpdateScreen(screenUpdates)),
recfz.WithLogger(log.New(io.Discard, "", 0)),
)
if err != nil {
log.Fatal(err)
}
defer fz.Close()
if err := fz.Connect(); err != nil {
log.Fatal(err)
}
m := model{
flipper: flipperui.New(fz, screenUpdates,
flipperui.WithScreenshotResolution(screenshotResolution.width, screenshotResolution.height),
flipperui.WithFgColor(rootFlags.fgColor),
flipperui.WithBgColor(rootFlags.bgColor),
),
}
if _, err := tea.NewProgram(m, tea.WithMouseCellMotion()).Run(); err != nil {
log.Fatalln(err)
}
}
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
var manCmd = &coral.Command{
Use: "man",
Short: "generates the manpages",
SilenceUsage: true,
DisableFlagsInUseLine: true,
Hidden: true,
Args: coral.NoArgs,
RunE: func(_ *coral.Command, _ []string) error {
manPage, err := mcoral.NewManPage(1, rootCmd)
if err != nil {
return err
}
_, err = fmt.Fprint(os.Stdout, manPage.Build(roff.NewDocument()))
return err
},
}
var versionCmd = &coral.Command{
Use: "version",
Short: "Print the version info",
Run: func(_ *coral.Command, _ []string) {
fmt.Printf("Version: %s\n", version.Version)
fmt.Printf("Commit: %s\n", version.Commit)
fmt.Printf("Date: %s\n", version.Date)
fmt.Printf("Build by: %s\n", version.BuiltBy)
},
}
func parseScreenshotResolution() (struct {
width int
height int
}, error) {
var screenshotResolution struct {
width int
height int
}
_, err := fmt.Sscanf(rootFlags.screenshotResolution, "%dx%d", &screenshotResolution.width, &screenshotResolution.height)
return screenshotResolution, err
}
================================================
FILE: middleware.go
================================================
package main
import (
"errors"
"sync"
"github.com/charmbracelet/ssh"
"github.com/charmbracelet/wish"
)
// connLimiter limits the number of concurrent connections.
type connLimiter struct {
sync.Mutex
conns int
maxConns int
}
// newConnLimiter returns a new connLimiter.
func newConnLimiter(maxConns int) *connLimiter {
return &connLimiter{
maxConns: maxConns,
}
}
// Add adds a connection to the limiter.
func (u *connLimiter) Add() error {
u.Lock()
defer u.Unlock()
if u.conns >= u.maxConns {
return errors.New("max connections reached")
}
u.conns++
return nil
}
// Remove removes a connection from the limiter.
func (u *connLimiter) Remove() {
u.Lock()
defer u.Unlock()
u.conns--
if u.conns < 0 {
u.conns = 0
}
}
// connLimit is a wish middleware that limits the number of concurrent
// connections.
func connLimit(limiter *connLimiter) wish.Middleware {
return func(sh ssh.Handler) ssh.Handler {
return func(s ssh.Session) {
if err := limiter.Add(); err != nil {
wish.Fatalf(s, "max connections reached\n")
return
}
sh(s)
limiter.Remove()
}
}
}
================================================
FILE: model.go
================================================
package main
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type model struct {
flipper tea.Model
width, height int
}
// Init is the bubbletea init function.
func (m model) Init() tea.Cmd {
return m.flipper.Init()
}
// Update is the bubbletea update function and handles all tea.Msgs.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
}
var cmd tea.Cmd
m.flipper, cmd = m.flipper.Update(msg)
return m, cmd
}
// View is the bubbletea view function.
func (m model) View() string {
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, m.flipper.View())
}
================================================
FILE: recfz/conn.go
================================================
package recfz
import (
"bufio"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/flipperdevices/go-flipper"
"go.bug.st/serial"
"go.bug.st/serial/enumerator"
)
// Connect connects to the flipper zero device.
// It will indefinitely try to reconnect if the connection is lost.
func (f *FlipperZero) Connect() error {
if err := f.reconnect(); err != nil {
return err
}
go f.reconnLoop()
return nil
}
// reconnect starts a new connection to the flipper zero device.
func (f *FlipperZero) reconnect() error {
conn, err := f.newConn()
if err != nil {
return fmt.Errorf("could not open serial conn: %w", err)
}
fz, err := flipper.ConnectWithTimeout(conn, 10*time.Second)
if err != nil {
return fmt.Errorf("could not connect to flipper: %w", err)
}
f.logger.Println("successfully connected to flipper")
f.SetFlipper(fz)
f.SetConn(conn)
return f.startScreenStream()
}
// newConn opens a new serial connection to the flipper zero device.
// If the port is not static, it will try to autodetect the flipper zero device.
// If the connection is already open, it will be closed and a new one will be opened.
// If the connection is openend successfully, it will start an rpc session over serial.
func (f *FlipperZero) newConn() (serial.Port, error) {
port := f.port
if !f.staticPort {
var err error
port, err = f.autodetectFlipper()
if err != nil {
return nil, err
}
}
if conn := f.getConn(); conn != nil {
conn.Close()
}
ser, err := serial.Open(port, &serial.Mode{})
if err != nil {
return nil, err
}
br := bufio.NewReader(ser)
_, err = readUntil(br, []byte("\r\n\r\n>: "))
if err != nil {
return nil, err
}
_, err = ser.Write([]byte(startRPCSessionCommand))
if err != nil {
return nil, err
}
token, err := br.ReadString('\r')
if err != nil {
return nil, err
}
if token != startRPCSessionCommand {
return nil, errors.New(strings.TrimSpace(token))
}
go f.checkConnLoop(ser)
f.logger.Println("successfully opened serial connection to flipper")
return ser, nil
}
// autodetectFlipper tries to automatically detect the flipper zero device.
func (f *FlipperZero) autodetectFlipper() (string, error) {
ports, err := enumerator.GetDetailedPortsList()
if err != nil {
return "", err
}
for _, p := range ports {
if p.PID == flipperPid && p.VID == flipperVid {
f.logger.Printf("found flipper on %s", p.Name)
return p.Name, nil
}
}
return "", errors.New("no flipper found")
}
// checkConnLoop checks if the connection is still alive by sending an empty message every 2 seconds.
// If the connection is lost, it will trigger a reconnect.
func (f *FlipperZero) checkConnLoop(r io.Writer) {
ticker := time.NewTicker(time.Second * 2)
defer ticker.Stop()
for {
select {
case <-f.ctx.Done():
return
case <-ticker.C:
if !f.Connected() {
continue
}
_, err := r.Write(nil)
if err != nil {
if f.getClosing() {
return
}
f.logger.Printf("could not read from flipper: %s", err)
f.reconnCh <- struct{}{}
return
}
}
}
}
// reconnLoop tries to reconnect to the flipper zero device if the connection is lost.
// If a reconnect fails, it will indefinitely try again after 1 second.
func (f *FlipperZero) reconnLoop() {
for {
select {
case _, ok := <-f.reconnCh:
if !ok {
return
}
if f.connecting || !f.Connected() {
continue
}
f.connecting = true
f.SetFlipper(nil)
for {
if err := f.reconnect(); err != nil {
f.logger.Printf("could not reconnect: %v", err)
time.Sleep(time.Second)
continue
}
break
}
f.connecting = false
case <-f.ctx.Done():
return
}
}
}
================================================
FILE: recfz/flipper.go
================================================
package recfz
import (
"errors"
"github.com/flipperdevices/go-flipper"
)
// startScreenStream starts a screen stream from the flipper zero device.
// It triggers a callback function for every new screen frame.
func (f *FlipperZero) startScreenStream() error {
f.mu.Lock()
defer f.mu.Unlock()
if f.streamScreenCallback == nil {
return errors.New("no stream screen callback set")
}
if err := f.flipper.Gui.StartScreenStream(f.streamScreenCallback); err != nil {
return err
}
f.logger.Println("started screen streaming...")
return nil
}
// SendShortPress sends a short press event to the flipper zero device.
// If the flipper zero device is not connected, it will do nothing.
func (f *FlipperZero) SendShortPress(event flipper.InputKey) {
f.mu.Lock()
defer f.mu.Unlock()
if f.flipper == nil {
return
}
f.flipper.Gui.SendInputEvent(event, flipper.InputTypePress) //nolint:errcheck
f.flipper.Gui.SendInputEvent(event, flipper.InputTypeShort) //nolint:errcheck
f.flipper.Gui.SendInputEvent(event, flipper.InputTypeRelease) //nolint:errcheck
}
// SendLongPress sends a long press event to the flipper zero device.
// If the flipper zero device is not connected, it will do nothing.
func (f *FlipperZero) SendLongPress(event flipper.InputKey) {
f.mu.Lock()
defer f.mu.Unlock()
if f.flipper == nil {
return
}
f.flipper.Gui.SendInputEvent(event, flipper.InputTypePress) //nolint:errcheck
f.flipper.Gui.SendInputEvent(event, flipper.InputTypeLong) //nolint:errcheck
f.flipper.Gui.SendInputEvent(event, flipper.InputTypeRelease) //nolint:errcheck
}
================================================
FILE: recfz/reader.go
================================================
package recfz
import "bytes"
type reader interface {
ReadString(delim byte) (line string, err error)
}
func readUntil(r reader, delim []byte) (line []byte, err error) {
for {
var s string
s, err = r.ReadString(delim[len(delim)-1])
if err != nil {
return
}
line = append(line, []byte(s)...)
if bytes.HasSuffix([]byte(s), delim) {
return line[:len(line)-len(delim)], nil
}
}
}
================================================
FILE: recfz/recfz.go
================================================
package recfz
import (
"context"
"fmt"
"log"
"sync"
"github.com/flipperdevices/go-flipper"
"go.bug.st/serial"
)
const (
flipperPid = "5740"
flipperVid = "0483"
startRPCSessionCommand = "start_rpc_session\r"
)
// Opts represents an optional configuration for the flipper zero.
type Opts func(f *FlipperZero)
// WithPort sets the port of the flipper zero.
func WithPort(port string) Opts {
return func(f *FlipperZero) {
f.port = port
}
}
// WithContext sets the context for the flipper zero.
func WithContext(ctx context.Context) Opts {
return func(f *FlipperZero) {
f.parentCtx = ctx
}
}
// WithStreamScreenCallback sets the callback for the screen stream.
func WithStreamScreenCallback(cb func(frame flipper.ScreenFrame)) Opts {
return func(f *FlipperZero) {
f.streamScreenCallback = cb
}
}
// WithLogger sets the logger for the flipper zero.
func WithLogger(l *log.Logger) Opts {
return func(f *FlipperZero) {
f.logger = l
}
}
// FlipperZero represents the flipper zero device.
type FlipperZero struct {
parentCtx context.Context
ctx context.Context
cancel context.CancelFunc
port string
conn serial.Port
flipper *flipper.Flipper
reconnCh chan struct{}
connecting bool
mu sync.Mutex
staticPort bool
streamScreenCallback func(frame flipper.ScreenFrame)
logger *log.Logger
isClosing bool
}
// NewFlipperZero creates a new flipper zero device.
// If the port is not static, it will try to autodetect the flipper.
func NewFlipperZero(opts ...Opts) (*FlipperZero, error) {
f := &FlipperZero{
reconnCh: make(chan struct{}),
logger: log.Default(),
parentCtx: context.Background(),
}
for _, opt := range opts {
opt(f)
}
f.ctx, f.cancel = context.WithCancel(f.parentCtx)
if f.port == "" {
p, err := f.autodetectFlipper()
if err != nil {
return nil, fmt.Errorf("could not autodetect flipper: %w", err)
}
f.port = p
} else {
f.staticPort = true
}
return f, nil
}
// Close closes the connection to the flipper zero.
func (f *FlipperZero) Close() {
f.mu.Lock()
defer f.mu.Unlock()
if f.isClosing {
return
}
f.isClosing = true
f.cancel()
close(f.reconnCh)
f.conn.Close()
}
func (f *FlipperZero) getClosing() bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.isClosing
}
// Connected returns true if the flipper zero is connected.
func (f *FlipperZero) Connected() bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.flipper != nil
}
// SetFlipper can be used to set a flipper instance.
func (f *FlipperZero) SetFlipper(fz *flipper.Flipper) {
f.mu.Lock()
defer f.mu.Unlock()
f.flipper = fz
}
// SetConn sets a serial connection to the flipper zero.
func (f *FlipperZero) SetConn(c serial.Port) {
f.mu.Lock()
defer f.mu.Unlock()
f.conn = c
}
func (f *FlipperZero) getConn() serial.Port {
f.mu.Lock()
defer f.mu.Unlock()
return f.conn
}
// GetFlipper returns the flipper instance.
// If the flipper is not connected, it returns an error.
func (f *FlipperZero) GetFlipper() (*flipper.Flipper, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.flipper == nil {
return nil, fmt.Errorf("flipper is not connected")
}
return f.flipper, nil
}
================================================
FILE: scripts/completions.sh
================================================
#!/bin/sh
set -e
rm -rf completions
mkdir completions
for sh in bash zsh fish; do
go run . completion "$sh" >"completions/fztea.$sh"
done
================================================
FILE: scripts/manpages.sh
================================================
#!/bin/sh
set -e
rm -rf manpages
mkdir manpages
go run . man | gzip -c >manpages/fztea.1.gz
================================================
FILE: server.go
================================================
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/ssh"
"github.com/charmbracelet/wish"
bm "github.com/charmbracelet/wish/bubbletea"
lm "github.com/charmbracelet/wish/logging"
"github.com/jon4hz/fztea/flipperui"
"github.com/jon4hz/fztea/recfz"
"github.com/muesli/coral"
)
var serverFlags struct {
listen string
authorizedKeys string
}
var serverCmd = &coral.Command{
Use: "server",
Short: "Start an ssh server serving the flipper zero TUI",
Run: server,
}
func init() {
serverCmd.Flags().StringVarP(&serverFlags.listen, "listen", "l", "127.0.0.1:2222", "address to listen on")
serverCmd.Flags().StringVarP(&serverFlags.authorizedKeys, "authorized-keys", "k", "", "authorized_keys file for public key authentication")
}
func server(cmd *coral.Command, _ []string) {
// parse screenshot resolution
screenshotResolution, err := parseScreenshotResolution()
if err != nil {
log.Fatalf("failed to parse screenshot resolution: %s", err)
}
screenUpdates := make(chan flipperui.ScreenMsg)
fz, err := recfz.NewFlipperZero(
recfz.WithPort(rootFlags.port),
recfz.WithStreamScreenCallback(flipperui.UpdateScreen(screenUpdates)),
recfz.WithContext(cmd.Context()),
)
if err != nil {
log.Fatal(err)
}
defer fz.Close()
if err := fz.Connect(); err != nil {
log.Fatal(err)
}
cl := newConnLimiter(1)
sshOpts := []ssh.Option{
wish.WithAddress(serverFlags.listen),
wish.WithHostKeyPath(".ssh/flipperzero_tea_ed25519"),
wish.WithMiddleware(
bm.Middleware(func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
_, _, active := s.Pty()
if !active {
wish.Fatalln(s, "no active terminal, skipping")
return nil, nil
}
m := model{
flipper: flipperui.New(fz, screenUpdates,
flipperui.WithScreenshotResolution(screenshotResolution.width, screenshotResolution.height),
flipperui.WithFgColor(rootFlags.fgColor),
flipperui.WithBgColor(rootFlags.bgColor)),
}
return m, []tea.ProgramOption{tea.WithAltScreen(), tea.WithMouseCellMotion()}
}),
lm.Middleware(),
connLimit(cl),
),
}
if serverFlags.authorizedKeys != "" {
sshOpts = append(sshOpts, wish.WithAuthorizedKeys(serverFlags.authorizedKeys))
}
s, err := wish.NewServer(
sshOpts...,
)
if err != nil {
log.Fatalln(err)
}
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
log.Printf("Starting SSH server on %s", serverFlags.listen)
go func() {
if err = s.ListenAndServe(); err != nil {
log.Fatalln(err)
}
}()
<-done
log.Println("Stopping SSH server")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer func() { cancel() }()
if err := s.Shutdown(ctx); err != nil {
log.Fatalln(err)
}
}
gitextract_ynrc7a8r/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── lint.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── demo.tape ├── flipperui/ │ ├── flipper.go │ └── opts.go ├── go.mod ├── go.sum ├── internal/ │ └── version/ │ └── version.go ├── main.go ├── middleware.go ├── model.go ├── recfz/ │ ├── conn.go │ ├── flipper.go │ ├── reader.go │ └── recfz.go ├── scripts/ │ ├── completions.sh │ └── manpages.sh └── server.go
SYMBOL INDEX (67 symbols across 10 files)
FILE: flipperui/flipper.go
constant fullBlock (line 22) | fullBlock = '█'
constant upperHalfBlock (line 23) | upperHalfBlock = '▀'
constant lowerHalfBlock (line 24) | lowerHalfBlock = '▄'
constant flipperScreenHeight (line 27) | flipperScreenHeight = 32
constant flipperScreenWidth (line 28) | flipperScreenWidth = 128
constant fzEventCoolDown (line 32) | fzEventCoolDown = time.Millisecond * 10
type ScreenMsg (line 43) | type ScreenMsg struct
type Model (line 54) | type Model struct
method Init (line 122) | func (m Model) Init() tea.Cmd {
method Update (line 134) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method sendFlipperEvent (line 223) | func (m *Model) sendFlipperEvent(event flipper.InputKey, isLong bool) {
method View (line 238) | func (m Model) View() string {
method saveImage (line 284) | func (m *Model) saveImage() {
method setError (line 300) | func (m *Model) setError(err error) {
function New (line 89) | func New(fz *recfz.FlipperZero, screenUpdate <-chan ScreenMsg, opts ...F...
function listenScreenUpdate (line 127) | func listenScreenUpdate(u <-chan ScreenMsg) tea.Cmd {
function min (line 173) | func min(a, b int) int {
function mapKey (line 181) | func mapKey(key tea.KeyMsg) (flipper.InputKey, bool) {
function mapMouse (line 212) | func mapMouse(event tea.MouseMsg) flipper.InputKey {
function UpdateScreen (line 248) | func UpdateScreen(updates chan<- ScreenMsg) func(frame flipper.ScreenFra...
FILE: flipperui/opts.go
type FlipperOpts (line 4) | type FlipperOpts
function WithScreenshotResolution (line 7) | func WithScreenshotResolution(width, height int) FlipperOpts {
function WithFgColor (line 15) | func WithFgColor(color string) FlipperOpts {
function WithBgColor (line 22) | func WithBgColor(color string) FlipperOpts {
FILE: main.go
function init (line 32) | func init() {
function root (line 41) | func root(cmd *coral.Command, _ []string) {
function main (line 75) | func main() {
function parseScreenshotResolution (line 110) | func parseScreenshotResolution() (struct {
FILE: middleware.go
type connLimiter (line 12) | type connLimiter struct
method Add (line 26) | func (u *connLimiter) Add() error {
method Remove (line 37) | func (u *connLimiter) Remove() {
function newConnLimiter (line 19) | func newConnLimiter(maxConns int) *connLimiter {
function connLimit (line 48) | func connLimit(limiter *connLimiter) wish.Middleware {
FILE: model.go
type model (line 8) | type model struct
method Init (line 14) | func (m model) Init() tea.Cmd {
method Update (line 19) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 38) | func (m model) View() string {
FILE: recfz/conn.go
method Connect (line 18) | func (f *FlipperZero) Connect() error {
method reconnect (line 27) | func (f *FlipperZero) reconnect() error {
method newConn (line 48) | func (f *FlipperZero) newConn() (serial.Port, error) {
method autodetectFlipper (line 88) | func (f *FlipperZero) autodetectFlipper() (string, error) {
method checkConnLoop (line 105) | func (f *FlipperZero) checkConnLoop(r io.Writer) {
method reconnLoop (line 131) | func (f *FlipperZero) reconnLoop() {
FILE: recfz/flipper.go
method startScreenStream (line 11) | func (f *FlipperZero) startScreenStream() error {
method SendShortPress (line 26) | func (f *FlipperZero) SendShortPress(event flipper.InputKey) {
method SendLongPress (line 39) | func (f *FlipperZero) SendLongPress(event flipper.InputKey) {
FILE: recfz/reader.go
type reader (line 5) | type reader interface
function readUntil (line 9) | func readUntil(r reader, delim []byte) (line []byte, err error) {
FILE: recfz/recfz.go
constant flipperPid (line 14) | flipperPid = "5740"
constant flipperVid (line 15) | flipperVid = "0483"
constant startRPCSessionCommand (line 16) | startRPCSessionCommand = "start_rpc_session\r"
type Opts (line 20) | type Opts
function WithPort (line 23) | func WithPort(port string) Opts {
function WithContext (line 30) | func WithContext(ctx context.Context) Opts {
function WithStreamScreenCallback (line 37) | func WithStreamScreenCallback(cb func(frame flipper.ScreenFrame)) Opts {
function WithLogger (line 44) | func WithLogger(l *log.Logger) Opts {
type FlipperZero (line 51) | type FlipperZero struct
method Close (line 93) | func (f *FlipperZero) Close() {
method getClosing (line 105) | func (f *FlipperZero) getClosing() bool {
method Connected (line 112) | func (f *FlipperZero) Connected() bool {
method SetFlipper (line 119) | func (f *FlipperZero) SetFlipper(fz *flipper.Flipper) {
method SetConn (line 126) | func (f *FlipperZero) SetConn(c serial.Port) {
method getConn (line 132) | func (f *FlipperZero) getConn() serial.Port {
method GetFlipper (line 140) | func (f *FlipperZero) GetFlipper() (*flipper.Flipper, error) {
function NewFlipperZero (line 69) | func NewFlipperZero(opts ...Opts) (*FlipperZero, error) {
FILE: server.go
function init (line 32) | func init() {
function server (line 37) | func server(cmd *coral.Command, _ []string) {
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (59K chars).
[
{
"path": ".github/dependabot.yml",
"chars": 207,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"gomod\"\n directory: \"/\"\n schedule:\n interval: \"weekly\"\n - package"
},
{
"path": ".github/workflows/lint.yml",
"chars": 126,
"preview": "name: lint\non: [push, pull_request, workflow_dispatch]\n\njobs:\n lint:\n uses: jon4hz/meta/.github/workflows/lint.yml@m"
},
{
"path": ".github/workflows/release.yml",
"chars": 3205,
"preview": "---\nname: goreleaser\n\non:\n push:\n tags:\n - \"v*.*.*\"\n workflow_dispatch:\n\npermissions:\n contents: write\n id-t"
},
{
"path": ".gitignore",
"chars": 55,
"preview": ".vscode\n.ssh\ncompletions/\nmanpages/\ndist/\nflipper_*.png"
},
{
"path": ".golangci.yml",
"chars": 306,
"preview": "run:\n tests: false\n\nissues:\n max-issues-per-linter: 0\n max-same-issues: 0\n\nlinters:\n enable:\n - bodyclose\n - e"
},
{
"path": ".goreleaser.yml",
"chars": 6054,
"preview": "---\nversion: 2\n\nvariables:\n main: \".\"\n binary_name: \"fztea\"\n description: \"TUI to interact with your flipper zero\"\n "
},
{
"path": "LICENSE",
"chars": 1062,
"preview": "MIT License\n\nCopyright (c) 2022 Jonah\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
},
{
"path": "README.md",
"chars": 3434,
"preview": "# 🐬🧋 Fztea\n[](https://github.com/jon4hz/fzt"
},
{
"path": "demo.tape",
"chars": 275,
"preview": "Output .github/assets/demo.gif\n\nRequire fztea\n\nSet Width 2048\nSet Height 1074\n\nSet Margin 20\nSet MarginFill \"#000000\"\nSe"
},
{
"path": "flipperui/flipper.go",
"chars": 7852,
"preview": "package flipperui\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"image/png\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/bu"
},
{
"path": "flipperui/opts.go",
"chars": 685,
"preview": "package flipperui\n\n// FlipperOpts represents an optional configuration for the flipper model.\ntype FlipperOpts func(*Mod"
},
{
"path": "go.mod",
"chars": 2542,
"preview": "module github.com/jon4hz/fztea\n\ngo 1.24.0\n\ntoolchain go1.24.1\n\nrequire (\n\tgithub.com/charmbracelet/bubbles v0.21.0\n\tgith"
},
{
"path": "go.sum",
"chars": 10899,
"preview": "github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.co"
},
{
"path": "internal/version/version.go",
"chars": 325,
"preview": "package version\n\nvar (\n\t// Version is the current version of the application.\n\tVersion = \"development\"\n\t// Commit is the"
},
{
"path": "main.go",
"chars": 3237,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/jon4hz/fztea/fl"
},
{
"path": "middleware.go",
"chars": 1110,
"preview": "package main\n\nimport (\n\t\"errors\"\n\t\"sync\"\n\n\t\"github.com/charmbracelet/ssh\"\n\t\"github.com/charmbracelet/wish\"\n)\n\n// connLim"
},
{
"path": "model.go",
"chars": 830,
"preview": "package main\n\nimport (\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\ntype model stru"
},
{
"path": "recfz/conn.go",
"chars": 3653,
"preview": "package recfz\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/flipperdevices/go-flipper\"\n\t\"go"
},
{
"path": "recfz/flipper.go",
"chars": 1584,
"preview": "package recfz\n\nimport (\n\t\"errors\"\n\n\t\"github.com/flipperdevices/go-flipper\"\n)\n\n// startScreenStream starts a screen strea"
},
{
"path": "recfz/reader.go",
"chars": 402,
"preview": "package recfz\n\nimport \"bytes\"\n\ntype reader interface {\n\tReadString(delim byte) (line string, err error)\n}\n\nfunc readUnti"
},
{
"path": "recfz/recfz.go",
"chars": 3318,
"preview": "package recfz\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"sync\"\n\n\t\"github.com/flipperdevices/go-flipper\"\n\t\"go.bug.st/serial\"\n)\n"
},
{
"path": "scripts/completions.sh",
"chars": 138,
"preview": "#!/bin/sh\nset -e\nrm -rf completions\nmkdir completions\nfor sh in bash zsh fish; do\n\tgo run . completion \"$sh\" >\"completio"
},
{
"path": "scripts/manpages.sh",
"chars": 91,
"preview": "#!/bin/sh\nset -e\nrm -rf manpages\nmkdir manpages\ngo run . man | gzip -c >manpages/fztea.1.gz"
},
{
"path": "server.go",
"chars": 2847,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\ttea \"github.com/charmbracelet/bubbletea"
}
]
About this extraction
This page contains the full source code of the jon4hz/fztea GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (53.0 KB), approximately 18.9k tokens, and a symbol index with 67 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.