mirror of
https://github.com/IllusionMan1212/ffui.git
synced 2026-10-24 18:12:35 +00:00
Compare commits
18
Commits
24924e0bd1
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
067f2155b8
|
||
|
|
424319bf95
|
||
|
|
3ef9017031
|
||
|
|
0fb852e034
|
||
|
|
36f648fd86
|
||
|
|
2951ea391f
|
||
|
|
95bedb8ee7
|
||
|
|
85ebb57885
|
||
|
|
1d456b9863
|
||
|
|
2e1d175e98
|
||
|
|
fb390bbcbe
|
||
|
|
4f22b5252c
|
||
|
|
5e74d10409
|
||
|
|
206a62ee76
|
||
|
|
96389610ed
|
||
|
|
7b03fa0d86
|
||
|
|
039e30bacc
|
||
|
|
76ef469443
|
@@ -1,4 +1,9 @@
|
|||||||
## TODO: Don't show incompatible encoding options for encoders
|
- [x] query ffmpeg binary for available encoders and list the ones we know
|
||||||
## TODO: send a SIGINT to the ffmpeg process on ctrl+c
|
- [-] when passing in a directory, list all video files in that directory and have the user choose which ones they want encoded
|
||||||
## TODO: calculate total progress based on file sizes instead of counts of files for a more accurate progress percentage.
|
- [x] Scrollable viewport for the files. They currently get cut if there's too many.
|
||||||
## TODO: when passing in a directory, list all video files in that directory and have the user choose which ones they want encoded
|
- [ ] CLI Option/Flag to choose the output directory for the encoded video(s)
|
||||||
|
- [ ] The 'Encoding "XXXXX" ETA: XHXXMXXS' text gets cut instead of wrapping when the text is too long/terminal width too small. Fix
|
||||||
|
- [ ] Show FPS, bitrate, and current file size when encoding.
|
||||||
|
- Has to be parsed from the ffmpeg output
|
||||||
|
- [ ] calculate total progress based on file sizes instead of counts of files for a more accurate progress percentage.
|
||||||
|
might not do this.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -9,26 +10,36 @@ import (
|
|||||||
|
|
||||||
"github.com/charmbracelet/bubbles/progress"
|
"github.com/charmbracelet/bubbles/progress"
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Screen int
|
type Screen int
|
||||||
|
|
||||||
var checkmark = lipgloss.NewStyle().Foreground(lipgloss.Color("#22FF33")).Render("✔")
|
var Checkmark = lipgloss.NewStyle().Foreground(lipgloss.Color("#22FF33")).Render("✔")
|
||||||
var x = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF2233")).Render("✖️")
|
var X = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF2233")).Render("✖️")
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Cfg Screen = iota
|
None Screen = iota
|
||||||
|
Cfg
|
||||||
|
Files
|
||||||
Main
|
Main
|
||||||
)
|
)
|
||||||
|
|
||||||
type model struct {
|
type File struct {
|
||||||
|
Path string
|
||||||
|
Selected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
IsDirectory bool
|
IsDirectory bool
|
||||||
Path string
|
Path string
|
||||||
CurrentFileName string
|
CurrentFileName string
|
||||||
|
ViewportFocused bool
|
||||||
FileCount int
|
FileCount int
|
||||||
Files []string
|
Files []File
|
||||||
|
Viewport viewport.Model
|
||||||
Spinner spinner.Model
|
Spinner spinner.Model
|
||||||
SingleFileProgressBar progress.Model
|
SingleFileProgressBar progress.Model
|
||||||
SingleFileProgress float64
|
SingleFileProgress float64
|
||||||
@@ -36,95 +47,231 @@ type model struct {
|
|||||||
TotalProgress float64
|
TotalProgress float64
|
||||||
Estimate int
|
Estimate int
|
||||||
Program *tea.Program
|
Program *tea.Program
|
||||||
|
Command *exec.Cmd
|
||||||
Quitting bool
|
Quitting bool
|
||||||
|
Cancelled bool
|
||||||
Screen Screen
|
Screen Screen
|
||||||
Config map[int]Config
|
Config []Config
|
||||||
|
VisibleConfig []Config
|
||||||
FocusIndex int
|
FocusIndex int
|
||||||
|
ChoiceIndex int
|
||||||
ParsedConfig ParsedConfig
|
ParsedConfig ParsedConfig
|
||||||
DryRun bool
|
DryRun bool
|
||||||
ErrQuit bool
|
ErrQuit bool
|
||||||
ErrQuitMessage string
|
ErrQuitMessage string
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialModel(fileInfo os.FileInfo, absolutePath string) *model {
|
// We're returning a pointer here so we can embed the tea.Program on the original model
|
||||||
|
// instead of a copy.
|
||||||
|
func initialModel(fileInfo os.FileInfo, absolutePath string) *Model {
|
||||||
s := spinner.New()
|
s := spinner.New()
|
||||||
s.Spinner = spinner.Dot
|
s.Spinner = spinner.Dot
|
||||||
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
|
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
|
||||||
|
|
||||||
cfg := make(map[int]Config)
|
codecsStr, err := exec.Command("ffmpeg", "-encoders").Output()
|
||||||
|
if err != nil {
|
||||||
for i := 0; i < len(Configs); i++ {
|
log.Println(err)
|
||||||
cfg[i] = Configs[i]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &model{
|
codecs := strings.Split(strings.Split(string(codecsStr), " ------\n")[1], "\n")
|
||||||
|
|
||||||
|
for i, codec := range codecs {
|
||||||
|
if codec == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
codecs[i] = strings.Fields(codec)[1]
|
||||||
|
|
||||||
|
// hardcoded indexes are never bad YEP :)))
|
||||||
|
if contains(SupportedVideoEncoders, codecs[i]) {
|
||||||
|
Configs[2].Opts = append(Configs[2].Opts, codecs[i])
|
||||||
|
} else if contains(SupportedAudioEncoders, codecs[i]) {
|
||||||
|
Configs[3].Opts = append(Configs[3].Opts, codecs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Model{
|
||||||
IsDirectory: fileInfo.IsDir(),
|
IsDirectory: fileInfo.IsDir(),
|
||||||
CurrentFileName: fileInfo.Name(),
|
CurrentFileName: fileInfo.Name(),
|
||||||
Path: absolutePath,
|
Path: absolutePath,
|
||||||
FileCount: 0,
|
FileCount: 0,
|
||||||
Files: make([]string, 0),
|
Files: make([]File, 0),
|
||||||
|
Viewport: viewport.New(0, 0),
|
||||||
|
Screen: Cfg,
|
||||||
Spinner: s,
|
Spinner: s,
|
||||||
SingleFileProgressBar: progress.New(progress.WithGradient("#1010ff", "#00ff00")),
|
SingleFileProgressBar: progress.New(progress.WithGradient("#1010ff", "#00ff00")),
|
||||||
SingleFileProgress: 0.0,
|
SingleFileProgress: 0.0,
|
||||||
TotalProgressBar: progress.New(progress.WithDefaultGradient()),
|
TotalProgressBar: progress.New(progress.WithDefaultGradient()),
|
||||||
TotalProgress: 0.0,
|
TotalProgress: 0.0,
|
||||||
Quitting: false,
|
Quitting: false,
|
||||||
Config: cfg,
|
Config: Configs,
|
||||||
|
VisibleConfig: getVisibleConfigs(Configs),
|
||||||
ErrQuit: false,
|
ErrQuit: false,
|
||||||
ErrQuitMessage: "",
|
ErrQuitMessage: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd {
|
func (m Model) updateConfigFocusedOptions() {
|
||||||
return tea.Batch(m.Spinner.Tick, initCfg)
|
for _, visCfg := range m.VisibleConfig {
|
||||||
|
for i := range m.Config {
|
||||||
|
if m.Config[i].Name == visCfg.Name {
|
||||||
|
m.Config[i].FocusedOption = visCfg.FocusedOption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m Model) Init() tea.Cmd {
|
||||||
|
return tea.Batch(m.Spinner.Tick, m.statFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "esc":
|
|
||||||
// TODO: cancel the ffmpeg command context to terminate the process
|
|
||||||
return m, tea.Quit
|
|
||||||
}
|
|
||||||
case errQuitMsg:
|
case errQuitMsg:
|
||||||
m.ErrQuitMessage = msg.msg
|
m.ErrQuitMessage = msg.msg
|
||||||
m.ErrQuit = true
|
m.ErrQuit = true
|
||||||
return m, m.cleanUp
|
return m, tea.Sequence(tea.ExitAltScreen, m.cleanUp)
|
||||||
|
case filesStatMsg:
|
||||||
|
m.FileCount = msg.fileCount
|
||||||
|
m.Files = msg.files
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.Viewport.Width = msg.Width
|
||||||
|
m.Viewport.Height = msg.Height - lipgloss.Height(FilesScreenViewHeader(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
switch m.Screen {
|
switch m.Screen {
|
||||||
|
case Files:
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
key := msg.String()
|
||||||
|
switch key {
|
||||||
|
case "ctrl+c", "esc":
|
||||||
|
return m, tea.Quit
|
||||||
|
case "enter", " ":
|
||||||
|
if !m.ViewportFocused && key == "enter" {
|
||||||
|
selectAll := !every(m.Files, func(e File) bool { return e.Selected })
|
||||||
|
|
||||||
|
if m.ChoiceIndex == 0 {
|
||||||
|
for i := range m.Files {
|
||||||
|
m.Files[i].Selected = selectAll
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.Files = filter(m.Files, func(f File) bool {
|
||||||
|
return f.Selected
|
||||||
|
})
|
||||||
|
|
||||||
|
m.Screen = Main
|
||||||
|
m.FileCount = len(m.Files)
|
||||||
|
|
||||||
|
return m, tea.Batch(m.Spinner.Tick, encodeVideo)
|
||||||
|
}
|
||||||
|
} else if m.ViewportFocused {
|
||||||
|
m.Files[m.FocusIndex].Selected = !m.Files[m.FocusIndex].Selected
|
||||||
|
|
||||||
|
// Reset the choice to "Select All" button when no files are selected
|
||||||
|
if !anyOf(m.Files, func(f File) bool { return f.Selected }) {
|
||||||
|
m.ChoiceIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SetViewportContent()
|
||||||
|
case "g":
|
||||||
|
if m.ViewportFocused {
|
||||||
|
m.FocusIndex = 0
|
||||||
|
m.SetViewportContent()
|
||||||
|
m.Viewport.GotoTop()
|
||||||
|
}
|
||||||
|
case "G":
|
||||||
|
if m.ViewportFocused {
|
||||||
|
m.FocusIndex = len(m.Files) - 1
|
||||||
|
m.SetViewportContent()
|
||||||
|
m.Viewport.GotoBottom()
|
||||||
|
}
|
||||||
|
case "left", "right", "h", "l":
|
||||||
|
if !m.ViewportFocused && anyOf(m.Files, func(f File) bool { return f.Selected }) {
|
||||||
|
if key == "right" || key == "l" {
|
||||||
|
m.ChoiceIndex++
|
||||||
|
} else {
|
||||||
|
m.ChoiceIndex--
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.ChoiceIndex > 1 {
|
||||||
|
m.ChoiceIndex = 0
|
||||||
|
} else if m.ChoiceIndex < 0 {
|
||||||
|
m.ChoiceIndex = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "tab", "shift+tab":
|
||||||
|
m.ViewportFocused = !m.ViewportFocused
|
||||||
|
m.SetViewportContent()
|
||||||
|
case "up", "down", "j", "k":
|
||||||
|
if m.ViewportFocused {
|
||||||
|
if key == "up" || key == "k" {
|
||||||
|
m.FocusIndex--
|
||||||
|
} else {
|
||||||
|
m.FocusIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == "down" || key == "j") && m.FocusIndex >= m.Viewport.Height+m.Viewport.YOffset {
|
||||||
|
m.Viewport.LineDown(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == "up" || key == "k") && m.FocusIndex < m.Viewport.YOffset {
|
||||||
|
m.Viewport.LineUp(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.FocusIndex >= len(m.Files) {
|
||||||
|
m.Viewport.GotoTop()
|
||||||
|
m.FocusIndex = 0
|
||||||
|
} else if m.FocusIndex < 0 {
|
||||||
|
m.Viewport.GotoBottom()
|
||||||
|
m.FocusIndex = len(m.Files) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SetViewportContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
case Cfg:
|
case Cfg:
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
key := msg.String()
|
key := msg.String()
|
||||||
switch key {
|
switch key {
|
||||||
|
case "ctrl+c", "esc":
|
||||||
|
return m, tea.Quit
|
||||||
case "enter", " ":
|
case "enter", " ":
|
||||||
// parse config and switch to main screen if we're focused on the start button
|
// parse config and switch to main screen if we're focused on the start button
|
||||||
if m.FocusIndex == len(Configs) {
|
if m.FocusIndex == len(m.VisibleConfig) {
|
||||||
return m, m.parseConfig(false)
|
if m.ChoiceIndex == 0 {
|
||||||
|
return m, m.parseConfig(false)
|
||||||
|
} else if m.ChoiceIndex == 1 {
|
||||||
|
return m, m.parseConfig(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// parse config, print it and exit
|
case "g":
|
||||||
if m.FocusIndex == len(Configs)+1 {
|
m.FocusIndex = 0
|
||||||
return m, m.parseConfig(true)
|
case "G":
|
||||||
}
|
m.FocusIndex = len(m.VisibleConfig)
|
||||||
case "tab", "shift+tab", "up", "down":
|
case "tab", "shift+tab", "up", "down", "j", "k":
|
||||||
if key == "up" || key == "shift+tab" {
|
if key == "up" || key == "shift+tab" || key == "k" {
|
||||||
m.FocusIndex--
|
m.FocusIndex--
|
||||||
} else {
|
} else {
|
||||||
m.FocusIndex++
|
m.FocusIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.FocusIndex >= len(Configs)+2 {
|
if m.FocusIndex > len(m.VisibleConfig) {
|
||||||
m.FocusIndex = 0
|
m.FocusIndex = 0
|
||||||
} else if m.FocusIndex < 0 {
|
} else if m.FocusIndex < 0 {
|
||||||
m.FocusIndex = len(Configs) + 1
|
m.FocusIndex = len(m.VisibleConfig)
|
||||||
}
|
}
|
||||||
case "left", "right":
|
case "left", "right", "h", "l":
|
||||||
if cfg, ok := m.Config[m.FocusIndex]; ok {
|
// If we're not hovering a button
|
||||||
if key == "right" {
|
if m.FocusIndex < len(m.VisibleConfig) {
|
||||||
|
cfg := &m.VisibleConfig[m.FocusIndex]
|
||||||
|
if key == "right" || key == "l" {
|
||||||
cfg.FocusedOption++
|
cfg.FocusedOption++
|
||||||
} else {
|
} else {
|
||||||
cfg.FocusedOption--
|
cfg.FocusedOption--
|
||||||
@@ -136,47 +283,81 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
cfg.FocusedOption = len(cfg.Opts) - 1
|
cfg.FocusedOption = len(cfg.Opts) - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Config[m.FocusIndex] = cfg
|
m.updateConfigFocusedOptions()
|
||||||
|
m.VisibleConfig = getVisibleConfigs(m.Config)
|
||||||
|
} else {
|
||||||
|
if key == "right" || key == "l" {
|
||||||
|
m.ChoiceIndex++
|
||||||
|
} else {
|
||||||
|
m.ChoiceIndex--
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.ChoiceIndex > 1 {
|
||||||
|
m.ChoiceIndex = 0
|
||||||
|
} else if m.ChoiceIndex < 0 {
|
||||||
|
m.ChoiceIndex = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case quitMsg:
|
case quitMsg:
|
||||||
m.Quitting = true
|
m.Quitting = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case parsedCfgMsg:
|
case parsedCfgMsg:
|
||||||
m.Screen = Main
|
|
||||||
m.ParsedConfig = msg.parsedConfig
|
m.ParsedConfig = msg.parsedConfig
|
||||||
return m, tea.Batch(m.Spinner.Tick, m.statFile)
|
|
||||||
|
if m.DryRun {
|
||||||
|
return m, tea.Batch(tea.ExitAltScreen, tea.Quit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.IsDirectory {
|
||||||
|
m.Screen = Files
|
||||||
|
m.FocusIndex = 0
|
||||||
|
m.ChoiceIndex = 0
|
||||||
|
|
||||||
|
m.SetViewportContent()
|
||||||
|
} else {
|
||||||
|
m.Screen = Main
|
||||||
|
|
||||||
|
return m, tea.Batch(m.Spinner.Tick, encodeVideo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case Main:
|
case Main:
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case initUi:
|
case tea.KeyMsg:
|
||||||
m.FileCount = msg.fileCount
|
key := msg.String()
|
||||||
m.Files = msg.files
|
switch key {
|
||||||
|
case "ctrl+c":
|
||||||
if m.DryRun {
|
m.Cancelled = true
|
||||||
return m, gracefullyQuit
|
err := m.Command.Process.Signal(os.Interrupt)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("An error occurred when sending SIGINT to the ffmpeg process:")
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
return m, tea.Sequence(tea.ExitAltScreen, tea.Quit)
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, encodeVideo
|
|
||||||
case encodeVideoMsg:
|
case encodeVideoMsg:
|
||||||
m.CurrentFileName = filepath.Base(m.Files[len(m.Files)-1])
|
m.CurrentFileName = filepath.Base(m.Files[len(m.Files)-1].Path)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
fullFilePath := m.Files[len(m.Files)-1]
|
file := m.Files[len(m.Files)-1]
|
||||||
encode(fullFilePath, filepath.Base(fullFilePath), m.Program, m.ParsedConfig)
|
encode(file, filepath.Base(file.Path), m.Program, m.ParsedConfig)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
case ffmpegProcessStart:
|
||||||
|
log.Printf("Running command: %s\n", msg.cmd.String())
|
||||||
|
m.Command = msg.cmd
|
||||||
return m, nil
|
return m, nil
|
||||||
case finishedEncodingVideo:
|
case finishedEncodingVideo:
|
||||||
if m.ParsedConfig.DeleteOldVideo {
|
if m.ParsedConfig.DeleteOldVideo {
|
||||||
m.CurrentFileName = fmt.Sprintf("Deleting: %s", filepath.Base(m.Files[len(m.Files)-1]))
|
m.CurrentFileName = fmt.Sprintf("Deleting: %s", filepath.Base(m.Files[len(m.Files)-1].Path))
|
||||||
os.Remove(m.Files[len(m.Files)-1])
|
os.Remove(m.Files[len(m.Files)-1].Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Files = m.Files[:len(m.Files)-1]
|
m.Files = m.Files[:len(m.Files)-1]
|
||||||
|
|
||||||
if len(m.Files) == 0 {
|
if len(m.Files) == 0 {
|
||||||
return m, gracefullyQuit
|
return m, tea.Sequence(tea.ExitAltScreen, gracefullyQuit)
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, encodeVideo
|
return m, encodeVideo
|
||||||
@@ -209,14 +390,93 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func CfgScreenView(m model) string {
|
func (m *Model) SetViewportContent() {
|
||||||
|
var files string
|
||||||
|
|
||||||
|
for i, file := range m.Files {
|
||||||
|
selection := " "
|
||||||
|
if file.Selected {
|
||||||
|
selection = "x"
|
||||||
|
}
|
||||||
|
if m.ViewportFocused && m.FocusIndex == i {
|
||||||
|
files += fmt.Sprintf(FocusedConfig.UnsetMarginTop().Render("[%s] %s"), selection, filepath.Base(file.Path))
|
||||||
|
} else {
|
||||||
|
files += fmt.Sprintf(BlurredConfig.UnsetMarginTop().Render("[%s] %s"), selection, filepath.Base(file.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
files += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Viewport.SetContent(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfig(cfg []Config) ParsedConfig {
|
||||||
|
vEncoder := find(cfg, "Video Encoder")
|
||||||
|
aEncoder := find(cfg, "Audio Encoder")
|
||||||
|
preset := find(cfg, "Preset")
|
||||||
|
crf := find(cfg, "Constant Rate Factor (CRF)")
|
||||||
|
|
||||||
|
return ParsedConfig{
|
||||||
|
DeleteOldVideo: find(cfg, "Delete old video(s)?").FocusedOption != 0,
|
||||||
|
IgnoreConflictingName: find(cfg, "On name conflict?").FocusedOption == 0,
|
||||||
|
VideoEncoder: vEncoder.Opts[vEncoder.FocusedOption],
|
||||||
|
AudioEncoder: aEncoder.Opts[aEncoder.FocusedOption],
|
||||||
|
Preset: preset.Opts[preset.FocusedOption],
|
||||||
|
CRF: crf.Opts[crf.FocusedOption],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FilesScreenViewHeader(m Model) string {
|
||||||
|
view := lipgloss.NewStyle().Margin(1, 0).Render("Select the files you wish to encode.")
|
||||||
|
var buttons string
|
||||||
|
var selectAllBtnText string
|
||||||
|
|
||||||
|
allSelected := every(m.Files, func(e File) bool { return e.Selected })
|
||||||
|
noneSelected := !anyOf(m.Files, func(e File) bool { return e.Selected })
|
||||||
|
|
||||||
|
if allSelected {
|
||||||
|
selectAllBtnText = "Deselect All"
|
||||||
|
} else {
|
||||||
|
selectAllBtnText = "Select All"
|
||||||
|
}
|
||||||
|
|
||||||
|
blurredStartButton := BlurredStartButton
|
||||||
|
focusedStartButton := FocusedStartButton
|
||||||
|
|
||||||
|
if noneSelected {
|
||||||
|
blurredStartButton = DisabledStartButton
|
||||||
|
focusedStartButton = DisabledStartButton
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.ViewportFocused {
|
||||||
|
if m.ChoiceIndex == 0 {
|
||||||
|
buttons = lipgloss.JoinHorizontal(0, FocusedSelectAllButton.Render(selectAllBtnText), blurredStartButton)
|
||||||
|
} else {
|
||||||
|
buttons = lipgloss.JoinHorizontal(0, BlurredSelectAllButton.Render(selectAllBtnText), focusedStartButton)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buttons = lipgloss.JoinHorizontal(0, BlurredSelectAllButton.Render(selectAllBtnText), blurredStartButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
view += buttons
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func FilesScreenView(m Model) string {
|
||||||
|
// TODO: files should be a grid (?)
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(0, FilesScreenViewHeader(m), m.Viewport.View())
|
||||||
|
}
|
||||||
|
|
||||||
|
func CfgScreenView(m Model) string {
|
||||||
view := ""
|
view := ""
|
||||||
|
|
||||||
for i, cfg := range Configs {
|
for i, cfg := range m.VisibleConfig {
|
||||||
opts := ""
|
opts := ""
|
||||||
|
|
||||||
for j, opt := range cfg.Opts {
|
for j, opt := range cfg.Opts {
|
||||||
if m.Config[i].FocusedOption == j {
|
if m.VisibleConfig[i].FocusedOption == j {
|
||||||
opts += FocusedOption.Render(opt)
|
opts += FocusedOption.Render(opt)
|
||||||
} else {
|
} else {
|
||||||
opts += BlurredOption.Render(opt)
|
opts += BlurredOption.Render(opt)
|
||||||
@@ -234,27 +494,39 @@ func CfgScreenView(m model) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.FocusIndex == len(Configs) {
|
var startButton string
|
||||||
view += FocusedStartButton
|
var dryRunButton string
|
||||||
|
|
||||||
|
if m.FocusIndex == len(m.VisibleConfig) && m.ChoiceIndex == 0 {
|
||||||
|
if m.IsDirectory {
|
||||||
|
startButton = FocusedNextButton
|
||||||
|
} else {
|
||||||
|
startButton = FocusedStartButton
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
view += BlurredStartButton
|
if m.IsDirectory {
|
||||||
|
startButton = BlurredNextButton
|
||||||
|
} else {
|
||||||
|
startButton = BlurredStartButton
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.FocusIndex == len(Configs)+1 {
|
if m.FocusIndex == len(m.VisibleConfig) && m.ChoiceIndex == 1 {
|
||||||
view += FocusedDryRunButton
|
dryRunButton = FocusedDryRunButton
|
||||||
} else {
|
} else {
|
||||||
view += BlurredDryRunButton
|
dryRunButton = BlurredDryRunButton
|
||||||
}
|
}
|
||||||
|
|
||||||
|
view += lipgloss.JoinHorizontal(0, startButton, dryRunButton)
|
||||||
view += "\n"
|
view += "\n"
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func MainScreenView(m model) string {
|
func MainScreenView(m Model) string {
|
||||||
progress := ""
|
progress := ""
|
||||||
|
|
||||||
if m.IsDirectory {
|
if m.IsDirectory && m.FileCount > 1 {
|
||||||
progress = fmt.Sprintf("%s %d/%d files encoded\nFile Progress: %s\n\nTotal Progress: %s",
|
progress = fmt.Sprintf("%s %d/%d files encoded\nFile Progress: %s\n\nTotal Progress: %s",
|
||||||
m.Spinner.View(),
|
m.Spinner.View(),
|
||||||
m.FileCount-len(m.Files),
|
m.FileCount-len(m.Files),
|
||||||
@@ -297,30 +569,25 @@ func formatEstimate(estimate int) string {
|
|||||||
return fmt.Sprintf("%ds", estimate)
|
return fmt.Sprintf("%ds", estimate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) View() string {
|
func (m Model) View() string {
|
||||||
|
if m.DryRun {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
if m.Quitting {
|
if m.Quitting {
|
||||||
if m.DryRun {
|
return fmt.Sprintf("%s %d/%d files encoded\n", Checkmark, m.FileCount-len(m.Files), m.FileCount)
|
||||||
fullFilePath := m.Files[len(m.Files)-1]
|
} else if m.Cancelled {
|
||||||
parentDir := filepath.Dir(fullFilePath)
|
// TODO: should we clean up the file ourselves?
|
||||||
fileName := filepath.Base(fullFilePath)
|
return fmt.Sprintf("%s Encoding cancelled. Stopped ffmpeg process.\n Make sure to clean up the created file\n", X)
|
||||||
extensionIndex := strings.LastIndex(fileName, ".")
|
|
||||||
newFileName := fileName[:extensionIndex]
|
|
||||||
extension := fileName[extensionIndex:]
|
|
||||||
outFileFullPath := filepath.Join(parentDir, newFileName+fmt.Sprintf("_[%s]_[%s]", m.ParsedConfig.VideoEncoder, m.ParsedConfig.AudioEncoder)+extension)
|
|
||||||
|
|
||||||
cmd := exec.Command("ffmpeg", buildFFmpegCmdArgs(fullFilePath, outFileFullPath, m.ParsedConfig)...)
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s %s\n", checkmark, cmd.String())
|
|
||||||
} else {
|
|
||||||
return fmt.Sprintf("%s %d/%d files encoded\n", checkmark, m.FileCount-len(m.Files), m.FileCount)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.ErrQuit {
|
if m.ErrQuit {
|
||||||
return fmt.Sprintf("%s %s\n", x, m.ErrQuitMessage)
|
return fmt.Sprintf("%s %s\n", X, m.ErrQuitMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch m.Screen {
|
switch m.Screen {
|
||||||
|
case Files:
|
||||||
|
return FilesScreenView(m)
|
||||||
case Cfg:
|
case Cfg:
|
||||||
return CfgScreenView(m)
|
return CfgScreenView(m)
|
||||||
case Main:
|
case Main:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -20,24 +21,28 @@ type updateEstimate struct {
|
|||||||
estimate int
|
estimate int
|
||||||
}
|
}
|
||||||
|
|
||||||
type initUi struct {
|
type ffmpegProcessStart struct {
|
||||||
fileCount int
|
cmd *exec.Cmd
|
||||||
files []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) statFile() tea.Msg {
|
type filesStatMsg struct {
|
||||||
|
fileCount int
|
||||||
|
files []File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) statFiles() tea.Msg {
|
||||||
if m.IsDirectory {
|
if m.IsDirectory {
|
||||||
entries, _ := os.ReadDir(m.Path)
|
entries, _ := os.ReadDir(m.Path)
|
||||||
|
|
||||||
sort.Sort(DirEntrySlice(entries))
|
sort.Sort(DirEntrySlice(entries))
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
fullFilePath := filepath.Join(m.Path, entry.Name())
|
if entry.IsDir() || !entry.Type().IsRegular() {
|
||||||
|
|
||||||
if entry.IsDir() {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fullFilePath := filepath.Join(m.Path, entry.Name())
|
||||||
|
|
||||||
mType, err := mimetype.DetectFile(fullFilePath)
|
mType, err := mimetype.DetectFile(fullFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@@ -48,14 +53,18 @@ func (m *model) statFile() tea.Msg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.FileCount++
|
m.FileCount++
|
||||||
m.Files = append(m.Files, fullFilePath)
|
m.Files = append(m.Files, File{Path: fullFilePath})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.Files) == 0 {
|
||||||
|
return errQuitMsg{"Chosen directory has no video files"}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m.FileCount = 1
|
m.FileCount = 1
|
||||||
m.Files = append(m.Files, m.Path)
|
m.Files = append(m.Files, File{Path: m.Path, Selected: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
return initUi{
|
return filesStatMsg{
|
||||||
fileCount: m.FileCount,
|
fileCount: m.FileCount,
|
||||||
files: m.Files,
|
files: m.Files,
|
||||||
}
|
}
|
||||||
@@ -73,34 +82,28 @@ func gracefullyQuit() tea.Msg {
|
|||||||
return quitMsg{}
|
return quitMsg{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type initCfgMsg struct{}
|
|
||||||
|
|
||||||
func initCfg() tea.Msg {
|
|
||||||
return initCfgMsg{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type parsedCfgMsg struct {
|
type parsedCfgMsg struct {
|
||||||
parsedConfig ParsedConfig
|
parsedConfig ParsedConfig
|
||||||
dryRun bool
|
dryRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) parseConfig(dryRun bool) tea.Cmd {
|
func (m *Model) parseConfig(dryRun bool) tea.Cmd {
|
||||||
m.DryRun = dryRun
|
m.DryRun = dryRun
|
||||||
|
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
vEncoder := find(m.Config, "Video Encoder")
|
vEncoder := find(m.Config, "Video Encoder")
|
||||||
aEncoder := find(m.Config, "Audio Encoder")
|
aEncoder := find(m.Config, "Audio Encoder")
|
||||||
preset := find(m.Config, "Preset (libx264 & libx265 only)")
|
preset := find(m.Config, "Preset")
|
||||||
crf := find(m.Config, "Constant Rate Factor (CRF)")
|
crf := find(m.Config, "Constant Rate Factor (CRF)")
|
||||||
|
|
||||||
return parsedCfgMsg{
|
return parsedCfgMsg{
|
||||||
parsedConfig: ParsedConfig{
|
parsedConfig: ParsedConfig{
|
||||||
DeleteOldVideo: find(m.Config, "Delete old video(s)?").FocusedOption != 0,
|
DeleteOldVideo: find(m.Config, "Delete old video(s)?").FocusedOption != 0,
|
||||||
SkipEncodedVid: find(m.Config, "What should we do about encoded videos?").FocusedOption == 0,
|
IgnoreConflictingName: find(m.Config, "On name conflict?").FocusedOption == 0,
|
||||||
VideoEncoder: vEncoder.Opts[vEncoder.FocusedOption],
|
VideoEncoder: vEncoder.Opts[vEncoder.FocusedOption],
|
||||||
AudioEncoder: aEncoder.Opts[aEncoder.FocusedOption],
|
AudioEncoder: aEncoder.Opts[aEncoder.FocusedOption],
|
||||||
Preset: preset.Opts[preset.FocusedOption],
|
Preset: preset.Opts[preset.FocusedOption],
|
||||||
CRF: crf.Opts[crf.FocusedOption],
|
CRF: crf.Opts[crf.FocusedOption],
|
||||||
},
|
},
|
||||||
dryRun: dryRun,
|
dryRun: dryRun,
|
||||||
}
|
}
|
||||||
@@ -111,15 +114,19 @@ type errQuitMsg struct {
|
|||||||
msg string
|
msg string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) cleanUp() tea.Msg {
|
func (m *Model) cleanUp() tea.Msg {
|
||||||
fullFilePath := m.Files[len(m.Files)-1]
|
if len(m.Files) == 0 {
|
||||||
fileName := filepath.Base(fullFilePath)
|
return tea.Quit()
|
||||||
|
}
|
||||||
|
|
||||||
parentDir := filepath.Dir(fullFilePath)
|
file := m.Files[len(m.Files)-1]
|
||||||
|
fileName := filepath.Base(file.Path)
|
||||||
|
|
||||||
|
parentDir := filepath.Dir(file.Path)
|
||||||
extensionIndex := strings.LastIndex(fileName, ".")
|
extensionIndex := strings.LastIndex(fileName, ".")
|
||||||
newFileName := fileName[:extensionIndex]
|
newFileName := fileName[:extensionIndex]
|
||||||
extension := fileName[extensionIndex:]
|
extension := fileName[extensionIndex:]
|
||||||
newFileFullPath := filepath.Join(parentDir, newFileName+fmt.Sprintf(" [%s] [%s]", m.ParsedConfig.VideoEncoder, m.ParsedConfig.AudioEncoder)+extension)
|
newFileFullPath := filepath.Join(parentDir, newFileName+fmt.Sprintf("_[%s]_[%s]", m.ParsedConfig.VideoEncoder, m.ParsedConfig.AudioEncoder)+extension)
|
||||||
|
|
||||||
os.Remove(newFileFullPath)
|
os.Remove(newFileFullPath)
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,26 @@ package main
|
|||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
|
var SupportedVideoEncoders = []string{
|
||||||
|
"libx264",
|
||||||
|
"libx265",
|
||||||
|
"libvpx-vp9",
|
||||||
|
"librav1e",
|
||||||
|
"libsvtav1",
|
||||||
|
}
|
||||||
|
|
||||||
|
var SupportedAudioEncoders = []string{
|
||||||
|
"aac",
|
||||||
|
"libopus",
|
||||||
|
"libvorbis",
|
||||||
|
}
|
||||||
|
|
||||||
var Configs = []Config{
|
var Configs = []Config{
|
||||||
{Name: "Delete old video(s)?", Opts: []string{"No", "Yes"}, FocusedOption: 1},
|
{Name: "Delete old video(s)?", Opts: []string{"No", "Yes"}, FocusedOption: 1},
|
||||||
{Name: "What should we do about encoded videos?", Opts: []string{"Skip", "Delete and encode again"}},
|
{Name: "On name conflict?", Opts: []string{"Ignore", "Overwrite"}},
|
||||||
{Name: "Video Encoder", Opts: []string{"copy", "libx264", "libx265", "libvpx-vp9", "librav1e", "libsvtav1"}},
|
{Name: "Video Encoder", Opts: []string{"copy"}},
|
||||||
{Name: "Audio Encoder", Opts: []string{"None", "copy", "aac", "libopus"}, FocusedOption: 1},
|
{Name: "Audio Encoder", Opts: []string{"None", "copy"}, FocusedOption: 1},
|
||||||
{Name: "Preset (libx264 & libx265 only)", Opts: []string{"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"}, FocusedOption: 4},
|
{Name: "Preset", Opts: []string{"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"}, FocusedOption: 4},
|
||||||
{Name: "Constant Rate Factor (CRF)", Opts: []string{"10", "15", "20", "25", "30", "35", "40", "45", "50"}, FocusedOption: 4},
|
{Name: "Constant Rate Factor (CRF)", Opts: []string{"10", "15", "20", "25", "30", "35", "40", "45", "50"}, FocusedOption: 4},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,15 +32,15 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ParsedConfig struct {
|
type ParsedConfig struct {
|
||||||
DeleteOldVideo bool
|
DeleteOldVideo bool
|
||||||
SkipEncodedVid bool
|
IgnoreConflictingName bool
|
||||||
VideoEncoder string
|
VideoEncoder string
|
||||||
AudioEncoder string
|
AudioEncoder string
|
||||||
Preset string
|
Preset string
|
||||||
CRF string
|
CRF string
|
||||||
}
|
}
|
||||||
|
|
||||||
func find(cfgs map[int]Config, name string) Config {
|
func find(cfgs []Config, name string) Config {
|
||||||
for _, cfg := range cfgs {
|
for _, cfg := range cfgs {
|
||||||
if cfg.Name == name {
|
if cfg.Name == name {
|
||||||
return cfg
|
return cfg
|
||||||
@@ -36,3 +50,20 @@ func find(cfgs map[int]Config, name string) Config {
|
|||||||
err := fmt.Sprintf("Couldn't find requested cfg: %s", name)
|
err := fmt.Sprintf("Couldn't find requested cfg: %s", name)
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getVisibleConfigs(cfgs []Config) []Config {
|
||||||
|
parsed := parseConfig(cfgs)
|
||||||
|
|
||||||
|
switch parsed.VideoEncoder {
|
||||||
|
case "copy", "librav1e":
|
||||||
|
return filter(cfgs, func(c Config) bool {
|
||||||
|
return c.Name != "Preset" && c.Name != "Constant Rate Factor (CRF)"
|
||||||
|
})
|
||||||
|
case "libvpx-vp9", "libsvtav1":
|
||||||
|
return filter(cfgs, func(c Config) bool {
|
||||||
|
return c.Name != "Preset"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfgs
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestFilterConfigs(t *testing.T) {
|
||||||
|
cfgs := filter(Configs, func(c Config) bool {
|
||||||
|
return c.Name != "Preset" && c.Name != "Constant Rate Factor (CRF)"
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, cfg := range cfgs {
|
||||||
|
if cfg.Name == "Preset" {
|
||||||
|
t.Fatalf("Failed to filter out config \"Preset\"")
|
||||||
|
} else if cfg.Name == "Constant Rate Factor (CRF)" {
|
||||||
|
t.Fatalf("Failed to filter out config \"Constant Rate Factor (CRF)\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetVisibleConfigs(t *testing.T) {
|
||||||
|
cfgs := getVisibleConfigs(Configs)
|
||||||
|
|
||||||
|
if len(cfgs) == len(Configs) {
|
||||||
|
t.Fatalf("Failed to get the correct visible configs. Expected VisibleConfigs len: %v. Got %v", 4, len(cfgs))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,22 +4,27 @@ go 1.23
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charmbracelet/bubbles v0.10.3
|
github.com/charmbracelet/bubbles v0.10.3
|
||||||
github.com/charmbracelet/bubbletea v0.20.0
|
github.com/charmbracelet/bubbletea v0.23.2
|
||||||
github.com/charmbracelet/lipgloss v0.5.0
|
github.com/charmbracelet/lipgloss v0.5.0
|
||||||
github.com/gabriel-vasile/mimetype v1.4.0
|
github.com/gabriel-vasile/mimetype v1.4.8
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/aymanbagabas/go-osc52 v1.2.1 // indirect
|
||||||
github.com/charmbracelet/harmonica v0.1.0 // indirect
|
github.com/charmbracelet/harmonica v0.1.0 // indirect
|
||||||
github.com/containerd/console v1.0.3 // indirect
|
github.com/containerd/console v1.0.3 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/reflow v0.3.0 // indirect
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
|
github.com/muesli/termenv v0.14.0 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 // indirect
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed // indirect
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
|
golang.org/x/term v0.27.0 // indirect
|
||||||
|
golang.org/x/text v0.21.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E=
|
||||||
|
github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
||||||
github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho=
|
github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho=
|
||||||
github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
|
github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
|
||||||
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
|
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
|
||||||
github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc=
|
github.com/charmbracelet/bubbletea v0.23.2 h1:vuUJ9HJ7b/COy4I30e8xDVQ+VRDUEFykIjryPfgsdps=
|
||||||
github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM=
|
github.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM=
|
||||||
github.com/charmbracelet/harmonica v0.1.0 h1:lFKeSd6OAckQ/CEzPVd2mqj+YMEubQ/3FM2IYY3xNm0=
|
github.com/charmbracelet/harmonica v0.1.0 h1:lFKeSd6OAckQ/CEzPVd2mqj+YMEubQ/3FM2IYY3xNm0=
|
||||||
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||||
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
|
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
|
||||||
@@ -12,43 +14,58 @@ github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0
|
|||||||
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
|
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
|
||||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
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/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||||
|
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
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.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
|
||||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
|
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
|
||||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||||
|
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/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||||
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
|
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
|
||||||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
|
github.com/muesli/termenv v0.14.0 h1:8x9NFfOe8lmIWK4pgy3IfVEy47f+ppe3tUqdPZG2Uy0=
|
||||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 h1:Ugb8sMTWuWRC3+sz5WeN/4kejDx9BvIwnPUiJBjJE+8=
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||||
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func main() {
|
|||||||
|
|
||||||
ffui := initialModel(fileInfo, absolutePath)
|
ffui := initialModel(fileInfo, absolutePath)
|
||||||
|
|
||||||
p := tea.NewProgram(ffui)
|
p := tea.NewProgram(ffui, tea.WithAltScreen())
|
||||||
|
|
||||||
ffui.Program = p
|
ffui.Program = p
|
||||||
|
|
||||||
@@ -46,11 +46,29 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.Start()
|
final, err := p.Run()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finalModel, _ := final.(Model)
|
||||||
|
|
||||||
|
if finalModel.DryRun {
|
||||||
|
for _, file := range finalModel.Files {
|
||||||
|
parentDir := filepath.Dir(file.Path)
|
||||||
|
fileName := filepath.Base(file.Path)
|
||||||
|
extensionIndex := strings.LastIndex(fileName, ".")
|
||||||
|
newFileName := fileName[:extensionIndex]
|
||||||
|
extension := fileName[extensionIndex:]
|
||||||
|
outFileFullPath := filepath.Join(parentDir, newFileName+fmt.Sprintf("_[%s]_[%s]", finalModel.ParsedConfig.VideoEncoder, finalModel.ParsedConfig.AudioEncoder)+extension)
|
||||||
|
|
||||||
|
cmd := exec.Command("ffmpeg", buildFFmpegCmdArgs(file.Path, outFileFullPath, finalModel.ParsedConfig)...)
|
||||||
|
|
||||||
|
fmt.Println(fmt.Sprintf("%s %s", Checkmark, cmd.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildFFmpegCmdArgs(fullFilePath string, outFullFilePath string, cfg ParsedConfig, additionalArgs ...string) []string {
|
func buildFFmpegCmdArgs(fullFilePath string, outFullFilePath string, cfg ParsedConfig, additionalArgs ...string) []string {
|
||||||
@@ -92,9 +110,7 @@ func buildFFmpegCmdArgs(fullFilePath string, outFullFilePath string, cfg ParsedC
|
|||||||
args = append(args, cfg.AudioEncoder)
|
args = append(args, cfg.AudioEncoder)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, arg := range additionalArgs {
|
args = append(args, additionalArgs...)
|
||||||
args = append(args, arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output file
|
// Output file
|
||||||
args = append(args, outFullFilePath)
|
args = append(args, outFullFilePath)
|
||||||
@@ -102,25 +118,25 @@ func buildFFmpegCmdArgs(fullFilePath string, outFullFilePath string, cfg ParsedC
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(fullFilePath string, fileName string, teaP *tea.Program, cfg ParsedConfig) {
|
func encode(file File, fileName string, teaP *tea.Program, cfg ParsedConfig) {
|
||||||
mType, err := mimetype.DetectFile(fullFilePath)
|
mType, err := mimetype.DetectFile(file.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(mType.String(), "video/") {
|
if !strings.HasPrefix(mType.String(), "video/") {
|
||||||
log.Fatalf("%s is not a valid video file\n", fileName)
|
log.Fatalf("%s is not a valid video file. It is %v\n", fileName, mType)
|
||||||
}
|
}
|
||||||
|
|
||||||
parentDir := filepath.Dir(fullFilePath)
|
parentDir := filepath.Dir(file.Path)
|
||||||
extensionIndex := strings.LastIndex(fileName, ".")
|
extensionIndex := strings.LastIndex(fileName, ".")
|
||||||
newFileName := fileName[:extensionIndex]
|
newFileName := fileName[:extensionIndex]
|
||||||
extension := fileName[extensionIndex:]
|
extension := fileName[extensionIndex:]
|
||||||
newFileFullPath := filepath.Join(parentDir, newFileName+fmt.Sprintf("_[%s]_[%s]", cfg.VideoEncoder, cfg.AudioEncoder)+extension)
|
newFileFullPath := filepath.Join(parentDir, newFileName+fmt.Sprintf("_[%s]_[%s]", cfg.VideoEncoder, cfg.AudioEncoder)+extension)
|
||||||
|
|
||||||
if _, err := os.Stat(newFileFullPath); err == nil {
|
if _, err := os.Stat(newFileFullPath); err == nil {
|
||||||
if cfg.SkipEncodedVid {
|
if cfg.IgnoreConflictingName {
|
||||||
log.Printf("%s already exists with the exact same encodings (crf and preset might be different though), skipping.", newFileFullPath)
|
log.Printf("Skipping \"%s\" because it already exists with the exact same encodings (crf and preset might be different though)", newFileFullPath)
|
||||||
teaP.Send(finishedEncodingVideo{})
|
teaP.Send(finishedEncodingVideo{})
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
@@ -128,10 +144,9 @@ func encode(fullFilePath string, fileName string, teaP *tea.Program, cfg ParsedC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs := buildFFmpegCmdArgs(fullFilePath, newFileFullPath, cfg, "-progress", "unix://"+getProgressSocket(fullFilePath, teaP))
|
cmdArgs := buildFFmpegCmdArgs(file.Path, newFileFullPath, cfg, "-progress", "unix://"+getProgressSocket(file.Path, teaP))
|
||||||
cmd := exec.Command("ffmpeg", cmdArgs...)
|
cmd := exec.Command("ffmpeg", cmdArgs...)
|
||||||
// TODO: send the cmd obj as a tea msg so we can send interrupt signals to the process when ctrl+c is pressed
|
teaP.Send(ffmpegProcessStart{cmd})
|
||||||
//teaP.Send()
|
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ func TempSock(totalDuration float64, teaP *tea.Program) string {
|
|||||||
go func() {
|
go func() {
|
||||||
re := regexp.MustCompile(`out_time_ms=(\d+)`)
|
re := regexp.MustCompile(`out_time_ms=(\d+)`)
|
||||||
speedRe := regexp.MustCompile(`speed=(\d+\.\d+x)`)
|
speedRe := regexp.MustCompile(`speed=(\d+\.\d+x)`)
|
||||||
|
totalSizeRe := regexp.MustCompile(`total_size=(\d+)`)
|
||||||
|
|
||||||
fd, err := l.Accept()
|
fd, err := l.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -65,6 +66,12 @@ func TempSock(totalDuration float64, teaP *tea.Program) string {
|
|||||||
cp = fmt.Sprintf("%.2f", float64(c)/totalDuration/1000000)
|
cp = fmt.Sprintf("%.2f", float64(c)/totalDuration/1000000)
|
||||||
}
|
}
|
||||||
if strings.Contains(data, "progress=end") {
|
if strings.Contains(data, "progress=end") {
|
||||||
|
totalSize := totalSizeRe.FindString(data)
|
||||||
|
if len(totalSize) > 0 && strings.Split(totalSize, "=")[1] == "0" {
|
||||||
|
// Command failed. Don't send a finishedEncodingVideo message.
|
||||||
|
// encode() will take care of reporting the error.
|
||||||
|
continue
|
||||||
|
}
|
||||||
cp = "done"
|
cp = "done"
|
||||||
teaP.Send(finishedEncodingVideo{})
|
teaP.Send(finishedEncodingVideo{})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ var (
|
|||||||
Light: "#1C6DD0",
|
Light: "#1C6DD0",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisabledColor = lipgloss.AdaptiveColor{
|
||||||
|
Dark: "#444",
|
||||||
|
Light: "#AAA",
|
||||||
|
}
|
||||||
|
|
||||||
BlurredConfig = lipgloss.NewStyle().
|
BlurredConfig = lipgloss.NewStyle().
|
||||||
Foreground(PrimaryColor).
|
Foreground(PrimaryColor).
|
||||||
MarginTop(1)
|
MarginTop(1)
|
||||||
@@ -49,6 +54,32 @@ var (
|
|||||||
Padding(0, 2).
|
Padding(0, 2).
|
||||||
Align(lipgloss.Center).
|
Align(lipgloss.Center).
|
||||||
Render("Start Encoding!")
|
Render("Start Encoding!")
|
||||||
|
DisabledStartButton = lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(DisabledColor).
|
||||||
|
Foreground(DisabledColor).
|
||||||
|
MarginTop(1).
|
||||||
|
Padding(0, 2).
|
||||||
|
Align(lipgloss.Center).
|
||||||
|
Render("Start Encoding!")
|
||||||
|
|
||||||
|
FocusedNextButton = lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(AccentColor).
|
||||||
|
Foreground(AccentColor).
|
||||||
|
MarginTop(1).
|
||||||
|
Padding(0, 2).
|
||||||
|
Align(lipgloss.Center).
|
||||||
|
Bold(true).
|
||||||
|
Render("Next")
|
||||||
|
BlurredNextButton = lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(PrimaryColor).
|
||||||
|
Foreground(PrimaryColor).
|
||||||
|
MarginTop(1).
|
||||||
|
Padding(0, 2).
|
||||||
|
Align(lipgloss.Center).
|
||||||
|
Render("Next")
|
||||||
|
|
||||||
FocusedDryRunButton = lipgloss.NewStyle().
|
FocusedDryRunButton = lipgloss.NewStyle().
|
||||||
Border(lipgloss.NormalBorder()).
|
Border(lipgloss.NormalBorder()).
|
||||||
@@ -67,4 +98,21 @@ var (
|
|||||||
Padding(0, 2).
|
Padding(0, 2).
|
||||||
Align(lipgloss.Center).
|
Align(lipgloss.Center).
|
||||||
Render("Print FFmpeg command and exit")
|
Render("Print FFmpeg command and exit")
|
||||||
|
|
||||||
|
FocusedSelectAllButton = lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(AccentColor).
|
||||||
|
Foreground(AccentColor).
|
||||||
|
MarginTop(1).
|
||||||
|
Padding(0, 2).
|
||||||
|
Align(lipgloss.Center).
|
||||||
|
Bold(true)
|
||||||
|
BlurredSelectAllButton = lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(PrimaryColor).
|
||||||
|
Foreground(PrimaryColor).
|
||||||
|
MarginTop(1).
|
||||||
|
Padding(0, 2).
|
||||||
|
Align(lipgloss.Center).
|
||||||
|
Bold(true)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
func contains[T comparable](slice []T, elem T) bool {
|
||||||
|
for _, e := range slice {
|
||||||
|
if e == elem {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func every[T any](slice []T, pred func(elem T) bool) bool {
|
||||||
|
for _, e := range slice {
|
||||||
|
if !pred(e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func anyOf[T any](slice []T, pred func(elem T) bool) bool {
|
||||||
|
for _, e := range slice {
|
||||||
|
if pred(e) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter[T any](slice []T, pred func(elem T) bool) []T {
|
||||||
|
filtered := make([]T, 0)
|
||||||
|
|
||||||
|
for _, e := range slice {
|
||||||
|
if pred(e) {
|
||||||
|
filtered = append(filtered, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user