Support creating a new folder

This commit is contained in:
Trevor Slocum 2020-11-10 12:35:36 -08:00
parent 03675d41ac
commit 5350889e5b
11 changed files with 615 additions and 516 deletions

3
.gitignore vendored
View File

@ -1,5 +1,4 @@
.idea/
dist/
*.sh
cmd/adbfm/adbfm
cmd/adbfm-gtk/adbfm-gtk
adbfm

View File

@ -13,24 +13,14 @@ fmt:
vet:
stage: validate
script:
- apt-get update && apt-get install -y libgtk-3-dev
- go vet -composites=false ./...
test:
stage: validate
script:
- apt-get update && apt-get install -y libgtk-3-dev
- go test -race -v ./...
build-adbfm:
build:
stage: build
script:
- cd cmd/adbfm
- go build
build-adbfm-gtk:
stage: build
script:
- apt-get update && apt-get install -y libgtk-3-dev
- cd cmd/adbfm-gtk
- go build

View File

@ -4,10 +4,33 @@
ADB file manager
## Dependencies
**Note:** adbfm is in early development. Additional features are planned.
* [zach-klippenstein/goadb](https://github.com/zach-klippenstein/goadb)
## Features
- Download
- Upload
- Rename
- New folder
## Download
```bash
go get gitlab.com/tslocum/adbfm/cmd/adbfm
```
## Usage
adbfm may be used with a keyboard or mouse.
- Navigation: Arrow keys or VIM-keys (H/J/K/L), Tab, Shift+Tab
- Enter directory: Enter
- Select file or option: Enter
## Support
Please share issues and suggestions [here](https://gitlab.com/tslocum/adbfm/issues).
## Dependencies
* [zach-klippenstein/goadb](https://github.com/zach-klippenstein/goadb)

View File

@ -1,4 +1,4 @@
package android
package main
import (
"errors"
@ -18,8 +18,8 @@ import (
// DefaultPort is the default port the ADB server listens on.
const DefaultPort = 5037
// ADB represents a connection with an ADB server.
type Bridge struct {
// adbConn represents a connection with an ADB server.
type adbConn struct {
bridge *adb.Adb
device *adb.Device
dir string
@ -27,9 +27,9 @@ type Bridge struct {
sync.Mutex
}
// NewBridge establishes a connection with an ADB server. When host and port
// connect establishes a connection with an ADB server. When host and port
// are unspecified they are replaced with localhost and the default port.
func NewBridge(host string, port int, pathToADB string) (bridge *Bridge, err error) {
func connect(host string, port int, pathToADB string) (bridge *adbConn, err error) {
adbBridge, err := adb.NewWithConfig(adb.ServerConfig{
PathToAdb: pathToADB,
Host: host,
@ -46,10 +46,10 @@ func NewBridge(host string, port int, pathToADB string) (bridge *Bridge, err err
return nil, fmt.Errorf("failed to get server version: %s", err)
}
return &Bridge{bridge: adbBridge}, nil
return &adbConn{bridge: adbBridge}, nil
}
func (a *Bridge) ListDevices() (devices []*adb.DeviceInfo, err error) {
func (a *adbConn) listDevices() (devices []*adb.DeviceInfo, err error) {
a.Lock()
defer a.Unlock()
@ -65,14 +65,10 @@ func (a *Bridge) ListDevices() (devices []*adb.DeviceInfo, err error) {
return devices, nil
}
func (a *Bridge) SetDevice(deviceInfo *adb.DeviceInfo) (err error) {
func (a *adbConn) setDevice(deviceInfo *adb.DeviceInfo) (err error) {
a.Lock()
defer a.Unlock()
return a.setDevice(deviceInfo)
}
func (a *Bridge) setDevice(deviceInfo *adb.DeviceInfo) (err error) {
device := a.bridge.Device(adb.DeviceWithSerial(deviceInfo.Serial))
if device == nil {
return fmt.Errorf("failed to get device with serial %s", deviceInfo.Serial)
@ -82,7 +78,7 @@ func (a *Bridge) setDevice(deviceInfo *adb.DeviceInfo) (err error) {
return nil
}
func (a *Bridge) Upload(fileName string, data []byte, mtime time.Time) (err error) {
func (a *adbConn) upload(fileName string, data []byte, mtime time.Time) (err error) {
a.Lock()
defer a.Unlock()
@ -102,7 +98,7 @@ func (a *Bridge) Upload(fileName string, data []byte, mtime time.Time) (err erro
return nil
}
func (a *Bridge) Download(fileName string) (data []byte, err error) {
func (a *adbConn) download(fileName string) (data []byte, err error) {
a.Lock()
defer a.Unlock()
@ -120,7 +116,7 @@ func (a *Bridge) Download(fileName string) (data []byte, err error) {
return data, nil
}
func (a *Bridge) DirectoryEntries(dir string) (entries []*adb.DirEntry, err error) {
func (a *adbConn) directoryEntries(dir string) (entries []*adb.DirEntry, err error) {
a.Lock()
defer a.Unlock()
@ -160,7 +156,7 @@ func (a *Bridge) DirectoryEntries(dir string) (entries []*adb.DirEntry, err erro
return entries, nil
}
func (a *Bridge) Move(old string, new string) (err error) {
func (a *adbConn) move(old string, new string) (err error) {
a.Lock()
defer a.Unlock()
@ -175,3 +171,19 @@ func (a *Bridge) Move(old string, new string) (err error) {
return nil
}
func (a *adbConn) newDirectory(path string) (err error) {
a.Lock()
defer a.Unlock()
if a.device == nil {
return errors.New("no device")
}
_, err = a.device.RunCommand("mkdir", path)
if err != nil {
return fmt.Errorf("failed to create directory: %s", err)
}
return nil
}

View File

@ -1,5 +0,0 @@
package main
func main() {
// TODO
}

View File

@ -1,458 +0,0 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
adb "github.com/zach-klippenstein/goadb"
"gitlab.com/tslocum/cbind"
"gitlab.com/tslocum/cview"
)
var (
app *cview.Application
inputConfig = cbind.NewConfiguration()
appGrid *cview.Grid
appContainer *cview.Pages
modalPopup *cview.Modal
devicesDropDown *cview.DropDown
localBuffer *cview.List
remoteBuffer *cview.List
renameField *cview.InputField
renameItem int
currentFocus = 0
currentMode = modeNormal
modeLock sync.RWMutex
)
const contextMenuTitleWidth = 16
const (
modeNormal = 0
modeRename = 1
)
func getMode() int {
modeLock.RLock()
defer modeLock.RUnlock()
return currentMode
}
func setMode(mode int) {
modeLock.Lock()
defer modeLock.Unlock()
currentMode = mode
}
func acceptRename(buttonIndex int, buttonLabel string) {
if buttonIndex >= 0 {
bridge.Move(path.Join(remotePath, remoteEntriesShown[renameItem].Name), path.Join(remotePath, renameField.GetText()))
browseRemote(remotePath)
}
setMode(modeNormal)
focusUpdated()
app.Draw()
}
func initTUI() error {
devices, err := bridge.ListDevices()
if err != nil {
log.Fatalf("failed to list devices: %s", err)
}
if len(devices) == 0 {
log.Fatal("failed to start adbfm: please connect to a device via ADB before launching")
}
/*cview.Styles.TitleColor = tcell.ColorDefault
cview.Styles.BorderColor = tcell.ColorDefault
cview.Styles.PrimaryTextColor = tcell.ColorDefault
cview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault*/
app = cview.NewApplication().
EnableMouse(true).
SetInputCapture(inputConfig.Capture)
// Devices
devicesDropDown = cview.NewDropDown().SetLabel(" Device: ").SetLabelColor(tcell.ColorDefault)
for _, device := range devices {
devicesDropDown.AddOptions(cview.NewDropDownOption(device.Serial).SetSelectedFunc(setDeviceFunc(device)))
}
// Local
localBuffer = cview.NewList().
ShowSecondaryText(false).
SetScrollBarVisibility(cview.ScrollBarAlways)
localBuffer.SetTitleAlign(cview.AlignLeft).SetBorder(true)
localBuffer.
AddContextItem("Upload", 'k', func(index int) {
// TODO Exists confirmation dialog
modalPopup.SetText(fmt.Sprintf("Uploading %s", localEntriesShown[index].Name))
focusUpdated()
app.ForceDraw()
f, err := os.Open(path.Join(localPath, localEntriesShown[index].Name))
if err != nil {
log.Fatalf("failed to open local file: %s", err)
}
data, err := ioutil.ReadAll(f)
if err != nil {
log.Fatalf("failed to read local file: %s", err)
}
stat, err := f.Stat()
if err != nil {
log.Fatalf("failed to fetch info of local file: %s", err)
}
err = bridge.Upload(localEntriesShown[index].Name, data, stat.ModTime())
if err != nil {
log.Fatalf("failed to upload file: %s", err)
}
browseRemote(remotePath)
focusUpdated()
app.Draw()
}).
AddContextItem("", 0, nil).
AddContextItem("Cut", 'x', func(index int) {
}).
AddContextItem("Copy", 'c', func(index int) {
}).
AddContextItem("Paste", 'v', func(index int) {
}).
AddContextItem("", 0, nil).
AddContextItem("Delete", 'd', func(index int) {
}).
AddContextItem("", 0, nil).
AddContextItem("Rename", 'r', func(index int) {
}).
AddContextItem("", 0, nil).
AddContextItem("New folder", 'f', func(index int) {
})
localBuffer.SetSelectedFunc(func(i int, item *cview.ListItem) {
localLock.Lock()
if i < 0 || i > len(localEntriesShown) {
localLock.Unlock()
return
}
if localEntriesShown[i].Mode&os.ModeDir != 0 {
newPath := localEntriesShown[i].Name
localLock.Unlock()
go browseLocal(path.Join(localPath, newPath))
return
}
align := cview.AlignCenter
if runewidth.StringWidth(localEntriesShown[i].Name) > contextMenuTitleWidth {
align = cview.AlignLeft
}
localBuffer.ContextMenuList().SetTitle(fmt.Sprintf("%s", localEntriesShown[i].Name)).SetTitleAlign(align)
localBuffer.ShowContextMenu(localBuffer.GetCurrentItemIndex(), -1, -1, func(primitive cview.Primitive) {
app.SetFocus(primitive)
})
localLock.Unlock()
})
// Remote
renameField = cview.NewInputField().SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEnter {
acceptRename(0, "")
} else {
acceptRename(-1, "")
}
})
remoteBuffer = cview.NewList().
ShowSecondaryText(false).
SetScrollBarVisibility(cview.ScrollBarAlways)
remoteBuffer.SetTitleAlign(cview.AlignLeft).SetBorder(true)
remoteBuffer.
AddContextItem("Download", 'j', func(index int) {
// TODO Exists confirmation dialog
modalPopup.SetText(fmt.Sprintf("Downloading %s", remoteEntriesShown[index].Name)).SetDoneFunc(acceptRename)
focusUpdated()
app.ForceDraw()
data, err := bridge.Download(remoteEntriesShown[index].Name)
if err != nil {
log.Fatalf("failed to download file: %s", err)
}
file, err := os.Create(path.Join(localPath, remoteEntriesShown[index].Name))
if err != nil {
log.Fatalf("failed to download file: failed to create local file: %s", err)
}
file.Write(data)
file.Close()
focusUpdated()
app.Draw()
}).
AddContextItem("", 0, nil).
AddContextItem("Cut", 'x', func(index int) {
}).
AddContextItem("Copy", 'c', func(index int) {
}).
AddContextItem("Paste", 'v', func(index int) {
}).
AddContextItem("", 0, nil).
AddContextItem("Delete", 'd', func(index int) {
}).
AddContextItem("", 0, nil).
AddContextItem("Rename", 'r', func(index int) {
renameItem = index
setMode(modeRename)
renameField.SetText(remoteEntriesShown[index].Name)
modalPopup.SetDoneFunc(acceptRename)
modalPopup.SetText(fmt.Sprintf("%s", remoteEntriesShown[index].Name)).GetForm().
Clear(true).
AddFormItem(renameField).
AddButton("Rename", func() {
acceptRename(0, "")
})
focusUpdated()
app.Draw()
}).
AddContextItem("", 0, nil).
AddContextItem("New folder", 'f', func(index int) {
})
remoteBuffer.SetSelectedFunc(func(i int, item *cview.ListItem) {
remoteLock.Lock()
if i < 0 || i > len(remoteEntriesShown) {
remoteLock.Unlock()
return
}
if remoteEntriesShown[i].Mode&os.ModeDir != 0 {
newPath := remoteEntriesShown[i].Name
remoteLock.Unlock()
go browseRemote(path.Join(remotePath, newPath))
return
}
align := cview.AlignCenter
if runewidth.StringWidth(remoteEntriesShown[i].Name) > contextMenuTitleWidth {
align = cview.AlignLeft
}
remoteBuffer.ContextMenuList().SetTitle(fmt.Sprintf("%s", remoteEntriesShown[i].Name)).SetTitleAlign(align)
remoteBuffer.ShowContextMenu(remoteBuffer.GetCurrentItemIndex(), -1, -1, func(primitive cview.Primitive) {
app.SetFocus(primitive)
})
remoteLock.Unlock()
})
// Select first device
if len(devices) > 0 {
devicesDropDown.SetCurrentOption(0)
}
pad := cview.NewTextView()
appGrid = cview.NewGrid().
SetRows(1, 1, -1).
SetColumns(-1, -1).
AddItem(devicesDropDown, 0, 0, 1, 2, 0, 0, true).
AddItem(pad, 1, 0, 1, 2, 0, 0, true).
AddItem(localBuffer, 2, 0, 1, 1, 0, 0, false).
AddItem(remoteBuffer, 2, 1, 1, 1, 0, 0, false)
appContainer = cview.NewPages()
modalPopup = cview.NewModal() // TODO center
modalPopup.SetBackgroundColor(tcell.ColorBlack)
appContainer.AddPage("main", appGrid, true, true)
appContainer.AddPage("modal", modalPopup, true, true)
app.SetRoot(appContainer, true).SetFocus(devicesDropDown)
focusUpdated()
// TODO No errors possible?
return nil
}
func focusUpdated() {
if getMode() == modeRename {
appContainer.ShowPage("modal")
app.SetFocus(renameField)
return
}
appContainer.HidePage("modal")
switch currentFocus {
case 0:
app.SetFocus(devicesDropDown)
case 1:
app.SetFocus(localBuffer)
case 2:
app.SetFocus(remoteBuffer)
}
}
func browseLocal(p string) {
localLock.Lock()
defer localLock.Unlock()
localEntries = nil
localPath = p
var skippedFirst bool
err := filepath.Walk(p, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
} else if !skippedFirst {
skippedFirst = true
return nil
} else if path == "." || path == ".." {
return nil
}
// TODO Store entries with more info
localEntries = append(localEntries, &adb.DirEntry{Name: info.Name(), Mode: info.Mode(), Size: int32(info.Size()), ModifiedAt: info.ModTime()})
// Do not search recursively
if info.IsDir() {
return filepath.SkipDir
}
return nil
})
if err != nil {
log.Fatal(err)
}
localEntries = append(localEntries, &adb.DirEntry{Name: "..", Mode: os.ModeDir})
sort.Slice(localEntries, func(i, j int) bool {
if localEntries[i].Name == ".." {
return true
} else if localEntries[i].Mode&os.ModeDir != localEntries[j].Mode&os.ModeDir {
return localEntries[i].Mode&os.ModeDir != 0
}
return strings.ToLower(localEntries[i].Name) < strings.ToLower(localEntries[j].Name)
})
app.QueueUpdateDraw(func() {
localLock.Lock()
defer localLock.Unlock()
localBuffer.SetTitle(" " + p + " ")
localBuffer.Clear()
localEntriesShown = nil
for _, entry := range localEntries {
if len(entry.Name) > 0 && entry.Name[0] == '.' && entry.Name != ".." && !showHidden {
continue
}
label := entry.Name
if entry.Mode&os.ModeDir != 0 {
label += "/"
}
localBuffer.AddItem(cview.NewListItem(cview.Escape(label)))
localEntriesShown = append(localEntriesShown, entry)
}
})
}
func browseRemote(p string) {
remoteLock.Lock()
defer remoteLock.Unlock()
previousSelection := -1
previousOffset := -1
if remotePath == p {
previousSelection = remoteBuffer.GetCurrentItemIndex()
previousOffset = remoteBuffer.GetOffset()
}
oldPath := remotePath
app.QueueUpdateDraw(func() {
remoteBuffer.SetTitle(" " + oldPath + "... ")
})
remotePath = p
var err error
remoteEntries, err = bridge.DirectoryEntries(p)
if err != nil {
log.Fatalf("failed to list files: %s", err)
}
go app.QueueUpdateDraw(func() {
remoteLock.Lock()
defer remoteLock.Unlock()
remoteBuffer.SetTitle(" " + p + " ")
remoteBuffer.Clear()
remoteEntriesShown = nil
for _, entry := range remoteEntries {
if len(entry.Name) > 0 && entry.Name[0] == '.' && entry.Name != ".." && !showHidden {
continue
}
label := entry.Name
if entry.Mode&os.ModeDir != 0 {
label += "/"
}
remoteBuffer.AddItem(cview.NewListItem(cview.Escape(label)))
remoteEntriesShown = append(remoteEntriesShown, entry)
}
if previousSelection >= 0 {
remoteBuffer.SetCurrentItem(previousSelection)
remoteBuffer.SetOffset(previousOffset)
}
})
}

9
go.mod
View File

@ -1,12 +1,13 @@
module gitlab.com/tslocum/adbfm
go 1.14
go 1.15
require (
github.com/gdamore/tcell/v2 v2.0.0-dev.0.20200926152101-0fb77ddaa5b4
github.com/gdamore/tcell/v2 v2.0.1-0.20201019142633-1057d5591ed1
github.com/mattn/go-runewidth v0.0.9
github.com/stretchr/testify v1.6.1 // indirect
github.com/zach-klippenstein/goadb v0.0.0-20170530005145-029cc6bee481
gitlab.com/tslocum/cbind v0.1.2
gitlab.com/tslocum/cview v1.4.10-0.20201003000057-5a3409bfd6c0
gitlab.com/tslocum/cbind v0.1.3
gitlab.com/tslocum/cview v1.5.2-0.20201107170141-79a35fe4de6c
golang.org/x/sys v0.0.0-20201109165425-215b40eba54c // indirect
)

25
go.sum
View File

@ -3,8 +3,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.0.0-dev/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gdamore/tcell/v2 v2.0.0-dev.0.20200926152101-0fb77ddaa5b4 h1:9WLVV5c2UI2qvgROlgzLgCuK5gi7igcU5LNsPXCSFB8=
github.com/gdamore/tcell/v2 v2.0.0-dev.0.20200926152101-0fb77ddaa5b4/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gdamore/tcell/v2 v2.0.1-0.20201019142633-1057d5591ed1 h1:gp9ujdOQmQf1gMvqOYYgxdMS5tRpRGE3HAgRH4Hgzd4=
github.com/gdamore/tcell/v2 v2.0.1-0.20201019142633-1057d5591ed1/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
@ -14,24 +14,25 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/zach-klippenstein/goadb v0.0.0-20170530005145-029cc6bee481 h1:yVrbGOZZHihWLaa3xpaTKoO5FbYg/vG5hUufn429HAo=
github.com/zach-klippenstein/goadb v0.0.0-20170530005145-029cc6bee481/go.mod h1:Drd+klC4FSDx0vKNEQDsSpWX5so04NA7l0vzHqkH8AQ=
gitlab.com/tslocum/cbind v0.1.2 h1:ptDjO7WeOl1HglprsK18L8I9JeRkmtuBoBBaYw/6/Ow=
gitlab.com/tslocum/cbind v0.1.2/go.mod h1:HfB7qAhHSZbn1rFK8M9SvSN5NG6ScAg/3h3iE6xdeeI=
gitlab.com/tslocum/cview v1.4.10-0.20201003000057-5a3409bfd6c0 h1:zS1fXLRZN44JIt21KOjtN6d9lDj5JJwtSn3bbCEKdLs=
gitlab.com/tslocum/cview v1.4.10-0.20201003000057-5a3409bfd6c0/go.mod h1:i9NyxtwBtkiVFrwmsh3Bv3dunvipjZrKX0TTdPHbzcw=
gitlab.com/tslocum/cbind v0.1.3 h1:FT/fTQ4Yj3eo5021lB3IbkIt8eVtYGhrw/xur+cjvUU=
gitlab.com/tslocum/cbind v0.1.3/go.mod h1:RvwYE3auSjBNlCmWeGspzn+jdLUVQ8C2QGC+0nP9ChI=
gitlab.com/tslocum/cview v1.5.2-0.20201107170141-79a35fe4de6c h1:umKPjQ2bSmLWf0rpYWhjeZsl5IdhNGEq3041e+yq//U=
gitlab.com/tslocum/cview v1.5.2-0.20201107170141-79a35fe4de6c/go.mod h1:BRtUi0zXzVXufhqFm/1GD7GL+iznKh5m9pEGN19SnKA=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck=
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201109165425-215b40eba54c h1:+B+zPA6081G5cEb2triOIJpcvSW4AYzmIyWAqMn2JAc=
golang.org/x/sys v0.0.0-20201109165425-215b40eba54c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

535
gui.go Normal file
View File

@ -0,0 +1,535 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"sort"
"strings"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
adb "github.com/zach-klippenstein/goadb"
"gitlab.com/tslocum/cbind"
"gitlab.com/tslocum/cview"
)
var (
app *cview.Application
inputConfig = cbind.NewConfiguration()
appGrid *cview.Grid
appContainer *cview.Panels
modalPopup *cview.Modal
devicesDropDown *cview.DropDown
localBuffer *cview.List
remoteBuffer *cview.List
renameField *cview.InputField
renameItem int
newFolderField *cview.InputField
newFolderItem int
currentFocus = 0
currentMode = modeNormal
)
const contextMenuTitleWidth = 16
const (
modeNormal = 0
modeRename = 1
modeNewFolder = 2
)
func getMode() int {
return currentMode
}
func setMode(mode int) {
currentMode = mode
switch currentMode {
case modeNormal:
appContainer.HidePanel("modal")
case modeRename, modeNewFolder:
appContainer.ShowPanel("modal")
}
focusUpdated()
}
func acceptRename(buttonIndex int, buttonLabel string) {
if buttonIndex >= 0 {
err := bridge.move(path.Join(remotePath, remoteEntriesShown[renameItem].Name), path.Join(remotePath, renameField.GetText()))
if err != nil {
log.Fatal(err)
}
}
setMode(modeNormal)
focusUpdated()
if buttonIndex >= 0 {
go func() {
browseRemote(remotePath)
app.Draw()
}()
return
}
app.Draw()
}
func acceptNewFolder(buttonIndex int, buttonLabel string) {
if buttonIndex >= 0 {
err := bridge.newDirectory(path.Join(remotePath, newFolderField.GetText()))
if err != nil {
log.Fatal(err)
}
}
setMode(modeNormal)
focusUpdated()
if buttonIndex >= 0 {
go func() {
browseRemote(remotePath)
app.Draw()
}()
}
app.Draw()
}
func initTUI() error {
devices, err := bridge.listDevices()
if err != nil {
log.Fatalf("failed to list devices: %s", err)
}
if len(devices) == 0 {
log.Fatal("failed to start adbfm: please connect to a device via ADB before launching")
}
/*cview.Styles.TitleColor = tcell.ColorDefault
cview.Styles.BorderColor = tcell.ColorDefault
cview.Styles.PrimaryTextColor = tcell.ColorDefault
cview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault*/
app = cview.NewApplication()
app.EnableMouse(true)
app.SetInputCapture(inputConfig.Capture)
// Devices
devicesDropDown = cview.NewDropDown()
devicesDropDown.SetLabel(" Device: ")
devicesDropDown.SetLabelColor(tcell.ColorDefault)
devicesDropDown.SetFieldTextColor(tcell.ColorWhite.TrueColor())
devicesDropDown.SetFieldTextColorFocused(tcell.ColorBlack.TrueColor())
devicesDropDown.SetFieldBackgroundColor(tcell.ColorBlack.TrueColor())
devicesDropDown.SetFieldBackgroundColorFocused(tcell.ColorWhite.TrueColor())
for _, device := range devices {
option := cview.NewDropDownOption(device.Serial)
option.SetSelectedFunc(setDeviceFunc(device))
devicesDropDown.AddOptions(option)
}
// Local
localBuffer = cview.NewList()
localBuffer.ShowSecondaryText(false)
localBuffer.SetScrollBarVisibility(cview.ScrollBarAlways)
localBuffer.SetTitleAlign(cview.AlignLeft)
localBuffer.SetBorder(true)
localBuffer.AddContextItem("Upload", 'k', func(index int) {
// TODO Exists confirmation dialog
modalPopup.SetText(fmt.Sprintf("Uploading %s", localEntriesShown[index].Name))
focusUpdated()
app.Draw()
f, err := os.Open(path.Join(localPath, localEntriesShown[index].Name))
if err != nil {
log.Fatalf("failed to open local file: %s", err)
}
data, err := ioutil.ReadAll(f)
if err != nil {
log.Fatalf("failed to read local file: %s", err)
}
stat, err := f.Stat()
if err != nil {
log.Fatalf("failed to fetch info of local file: %s", err)
}
err = bridge.upload(localEntriesShown[index].Name, data, stat.ModTime())
if err != nil {
log.Fatalf("failed to upload file: %s", err)
}
browseRemote(remotePath)
focusUpdated()
app.Draw()
})
localBuffer.AddContextItem("", 0, nil)
localBuffer.AddContextItem("Cut", 'x', func(index int) {
})
localBuffer.AddContextItem("Copy", 'c', func(index int) {
})
localBuffer.AddContextItem("Paste", 'v', func(index int) {
})
localBuffer.AddContextItem("", 0, nil)
localBuffer.AddContextItem("Delete", 'd', func(index int) {
})
localBuffer.AddContextItem("", 0, nil)
localBuffer.AddContextItem("Rename", 'r', func(index int) {
})
localBuffer.AddContextItem("", 0, nil)
localBuffer.AddContextItem("New folder", 'f', func(index int) {
})
localBuffer.SetSelectedFunc(func(i int, item *cview.ListItem) {
localLock.Lock()
if i < 0 || i > len(localEntriesShown) {
localLock.Unlock()
return
}
if localEntriesShown[i].Mode&os.ModeDir != 0 {
newPath := localEntriesShown[i].Name
localLock.Unlock()
go browseLocal(path.Join(localPath, newPath))
return
}
align := cview.AlignCenter
if runewidth.StringWidth(localEntriesShown[i].Name) > contextMenuTitleWidth {
align = cview.AlignLeft
}
localBuffer.ContextMenuList().SetTitle(fmt.Sprintf("%s", localEntriesShown[i].Name))
localBuffer.ContextMenuList().SetTitleAlign(align)
localBuffer.ShowContextMenu(localBuffer.GetCurrentItemIndex(), -1, -1, func(primitive cview.Primitive) {
app.SetFocus(primitive)
})
localLock.Unlock()
})
// Remote
renameField = cview.NewInputField()
renameField.SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEnter {
acceptRename(0, "")
} else {
acceptRename(-1, "")
}
})
newFolderField = cview.NewInputField()
newFolderField.SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEnter {
acceptNewFolder(0, "")
} else {
acceptNewFolder(-1, "")
}
})
remoteBuffer = cview.NewList()
remoteBuffer.ShowSecondaryText(false)
remoteBuffer.SetScrollBarVisibility(cview.ScrollBarAlways)
remoteBuffer.SetTitleAlign(cview.AlignLeft)
remoteBuffer.SetBorder(true)
remoteBuffer.AddContextItem("Download", 'j', func(index int) {
// TODO Exists confirmation dialog
modalPopup.SetText(fmt.Sprintf("Downloading %s", remoteEntriesShown[index].Name))
modalPopup.SetDoneFunc(acceptRename)
focusUpdated()
app.Draw()
data, err := bridge.download(remoteEntriesShown[index].Name)
if err != nil {
log.Fatalf("failed to download file: %s", err)
}
file, err := os.Create(path.Join(localPath, remoteEntriesShown[index].Name))
if err != nil {
log.Fatalf("failed to download file: failed to create local file: %s", err)
}
file.Write(data)
file.Close()
focusUpdated()
app.Draw()
})
remoteBuffer.AddContextItem("", 0, nil)
remoteBuffer.AddContextItem("Cut", 'x', func(index int) {
})
remoteBuffer.AddContextItem("Copy", 'c', func(index int) {
})
remoteBuffer.AddContextItem("Paste", 'v', func(index int) {
})
remoteBuffer.AddContextItem("", 0, nil)
remoteBuffer.AddContextItem("Delete", 'd', func(index int) {
})
remoteBuffer.AddContextItem("", 0, nil)
remoteBuffer.AddContextItem("Rename", 'r', func(index int) {
renameItem = index
setMode(modeRename)
renameField.SetText(remoteEntriesShown[index].Name)
modalPopup.SetDoneFunc(acceptRename)
modalPopup.SetText(fmt.Sprintf("%s", remoteEntriesShown[index].Name))
modalPopup.GetForm().Clear(true)
modalPopup.GetForm().AddFormItem(renameField)
modalPopup.GetForm().AddButton("Rename", func() {
acceptRename(0, "")
})
focusUpdated()
app.Draw()
})
remoteBuffer.AddContextItem("", 0, nil)
remoteBuffer.AddContextItem("New folder", 'f', func(index int) {
newFolderItem = index
setMode(modeNewFolder)
newFolderField.SetLabel("Folder name")
modalPopup.SetDoneFunc(acceptNewFolder)
modalPopup.SetText(fmt.Sprintf("%s", remoteEntriesShown[index].Name))
modalPopup.GetForm().Clear(true)
modalPopup.GetForm().AddFormItem(newFolderField)
modalPopup.GetForm().AddButton("Create", func() {
acceptNewFolder(0, "")
})
focusUpdated()
app.Draw()
})
remoteBuffer.SetSelectedFunc(func(i int, item *cview.ListItem) {
remoteLock.Lock()
if i < 0 || i > len(remoteEntriesShown) {
remoteLock.Unlock()
return
}
if remoteEntriesShown[i].Mode&os.ModeDir != 0 {
newPath := remoteEntriesShown[i].Name
remoteLock.Unlock()
go browseRemote(path.Join(remotePath, newPath))
return
}
align := cview.AlignCenter
if runewidth.StringWidth(remoteEntriesShown[i].Name) > contextMenuTitleWidth {
align = cview.AlignLeft
}
remoteBuffer.ContextMenuList().SetTitle(fmt.Sprintf("%s", remoteEntriesShown[i].Name))
remoteBuffer.ContextMenuList().SetTitleAlign(align)
remoteBuffer.ShowContextMenu(remoteBuffer.GetCurrentItemIndex(), -1, -1, func(primitive cview.Primitive) {
app.SetFocus(primitive)
})
remoteLock.Unlock()
})
// Select first device
if len(devices) > 0 {
devicesDropDown.SetCurrentOption(0)
}
pad := cview.NewTextView()
appGrid = cview.NewGrid()
appGrid.SetRows(1, 1, -1)
appGrid.SetColumns(-1, -1)
appGrid.AddItem(devicesDropDown, 0, 0, 1, 2, 0, 0, true)
appGrid.AddItem(pad, 1, 0, 1, 2, 0, 0, true)
appGrid.AddItem(localBuffer, 2, 0, 1, 1, 0, 0, false)
appGrid.AddItem(remoteBuffer, 2, 1, 1, 1, 0, 0, false)
appContainer = cview.NewPanels()
modalPopup = cview.NewModal() // TODO center
modalPopup.SetBackgroundColor(tcell.ColorBlack)
appContainer.AddPanel("main", appGrid, true, true)
appContainer.AddPanel("modal", modalPopup, true, true)
app.SetRoot(appContainer, true)
app.SetFocus(devicesDropDown)
currentMode = -1
setMode(modeNormal)
focusUpdated()
// TODO No errors possible?
return nil
}
func focusUpdated() {
switch getMode() {
case modeRename:
app.SetFocus(renameField)
return
case modeNewFolder:
app.SetFocus(newFolderField)
return
}
switch currentFocus {
case 0:
app.SetFocus(devicesDropDown)
case 1:
app.SetFocus(localBuffer)
case 2:
app.SetFocus(remoteBuffer)
}
}
func browseLocal(p string) {
localLock.Lock()
defer localLock.Unlock()
localEntries = nil
localPath = p
var skippedFirst bool
err := filepath.Walk(p, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
} else if !skippedFirst {
skippedFirst = true
return nil
} else if path == "." || path == ".." {
return nil
}
// TODO Store entries with more info
localEntries = append(localEntries, &adb.DirEntry{Name: info.Name(), Mode: info.Mode(), Size: int32(info.Size()), ModifiedAt: info.ModTime()})
// Do not search recursively
if info.IsDir() {
return filepath.SkipDir
}
return nil
})
if err != nil {
log.Fatal(err)
}
localEntries = append(localEntries, &adb.DirEntry{Name: "..", Mode: os.ModeDir})
sort.Slice(localEntries, func(i, j int) bool {
if localEntries[i].Name == ".." {
return true
} else if localEntries[i].Mode&os.ModeDir != localEntries[j].Mode&os.ModeDir {
return localEntries[i].Mode&os.ModeDir != 0
}
return strings.ToLower(localEntries[i].Name) < strings.ToLower(localEntries[j].Name)
})
app.QueueUpdateDraw(func() {
localLock.Lock()
defer localLock.Unlock()
localBuffer.SetTitle(" " + p + " ")
localBuffer.Clear()
localEntriesShown = nil
for _, entry := range localEntries {
if len(entry.Name) > 0 && entry.Name[0] == '.' && entry.Name != ".." && !showHidden {
continue
}
label := entry.Name
if entry.Mode&os.ModeDir != 0 {
label += "/"
}
localBuffer.AddItem(cview.NewListItem(cview.Escape(label)))
localEntriesShown = append(localEntriesShown, entry)
}
})
}
func browseRemote(p string) {
remoteLock.Lock()
defer remoteLock.Unlock()
previousSelection := -1
previousOffset := -1
if remotePath == p {
previousSelection = remoteBuffer.GetCurrentItemIndex()
previousOffset, _ = remoteBuffer.GetOffset()
}
oldPath := remotePath
app.QueueUpdateDraw(func() {
remoteBuffer.SetTitle(" " + oldPath + "... ")
})
remotePath = p
var err error
remoteEntries, err = bridge.directoryEntries(p)
if err != nil {
log.Fatalf("failed to list files: %s", err)
}
go app.QueueUpdateDraw(func() {
remoteLock.Lock()
defer remoteLock.Unlock()
remoteBuffer.SetTitle(" " + p + " ")
remoteBuffer.Clear()
remoteEntriesShown = nil
for _, entry := range remoteEntries {
if len(entry.Name) > 0 && entry.Name[0] == '.' && entry.Name != ".." && !showHidden {
continue
}
label := entry.Name
if entry.Mode&os.ModeDir != 0 {
label += "/"
}
remoteBuffer.AddItem(cview.NewListItem(cview.Escape(label)))
remoteEntriesShown = append(remoteEntriesShown, entry)
}
if previousSelection >= 0 {
remoteBuffer.SetCurrentItem(previousSelection)
remoteBuffer.SetOffset(previousOffset, 0)
}
})
}

View File

@ -29,7 +29,8 @@ func selectItem(ev *tcell.EventKey) *tcell.EventKey {
if runewidth.StringWidth(localEntriesShown[currentItem].Name) > contextMenuTitleWidth {
align = cview.AlignLeft
}
localBuffer.ContextMenuList().SetTitleAlign(align).SetTitle(localEntriesShown[currentItem].Name)
localBuffer.ContextMenuList().SetTitleAlign(align)
localBuffer.ContextMenuList().SetTitle(localEntriesShown[currentItem].Name)
localBuffer.ShowContextMenu(currentItem, -1, -1, func(primitive cview.Primitive) {
app.SetFocus(primitive)
@ -55,7 +56,8 @@ func selectItem(ev *tcell.EventKey) *tcell.EventKey {
if runewidth.StringWidth(remoteEntriesShown[currentItem].Name) > contextMenuTitleWidth {
align = cview.AlignLeft
}
remoteBuffer.ContextMenuList().SetTitleAlign(align).SetTitle(remoteEntriesShown[currentItem].Name)
remoteBuffer.ContextMenuList().SetTitleAlign(align)
remoteBuffer.ContextMenuList().SetTitle(remoteEntriesShown[currentItem].Name)
remoteBuffer.ShowContextMenu(currentItem, -1, -1, func(primitive cview.Primitive) {
app.SetFocus(primitive)

View File

@ -12,7 +12,6 @@ import (
"github.com/gdamore/tcell/v2"
adb "github.com/zach-klippenstein/goadb"
goadb "github.com/zach-klippenstein/goadb"
"gitlab.com/tslocum/adbfm/pkg/android"
"gitlab.com/tslocum/cbind"
"gitlab.com/tslocum/cview"
)
@ -39,7 +38,7 @@ var (
localLock sync.Mutex
remoteLock sync.Mutex
bridge *android.Bridge
bridge *adbConn
done = make(chan bool)
)
@ -100,7 +99,7 @@ func setDeviceFunc(device *goadb.DeviceInfo) func(index int, option *cview.DropD
localLock.Lock()
remoteLock.Lock()
err := bridge.SetDevice(device)
err := bridge.setDevice(device)
if err != nil {
log.Fatal(err)
}
@ -144,7 +143,7 @@ func run() error {
}
var err error
bridge, err = android.NewBridge("", 0, "")
bridge, err = connect("", 0, "")
if err != nil {
return fmt.Errorf("failed to connect to ADB server: %s", err)
}