Compare commits

..
5 Commits
Author SHA1 Message Date
Illusion 2951ea391f todo 2024-12-27 02:26:20 +02:00
Illusion 95bedb8ee7 feat: allow selection for files to be encoded in a directory 2024-12-27 00:07:05 +02:00
Illusion 85ebb57885 put buttons next to each other 2024-12-26 21:12:20 +02:00
Illusion 1d456b9863 clearer(?) config name for when output file name conflicts with another 2024-12-26 20:52:12 +02:00
Illusion 2e1d175e98 misc: small clean ups and fixes
fix: dry run command truncation by printing the command after bubbletea quits
Remove unnecessary startEncoding tea.Cmd
Capitalize model struct to Model
2024-12-26 18:34:33 +02:00
8 changed files with 321 additions and 157 deletions
+3 -2
View File
@@ -1,5 +1,6 @@
- [x] query ffmpeg binary for available encoders and list the ones we know
- [ ] when passing in a directory, list all video files in that directory and have the user choose which ones they want encoded
- This will of course have a select all checkbox for quickly encoding all the files in a dir.
- [-] when passing in a directory, list all video files in that directory and have the user choose which ones they want encoded
- [ ] Scrollable viewport for the files. They currently get cut if there's too many.
- [ ] CLI Option/Flag to choose the output directory for the encoded video(s)
- [ ] calculate total progress based on file sizes instead of counts of files for a more accurate progress percentage.
might not do this.
+162 -81
View File
@@ -21,22 +21,22 @@ var X = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF2233")).Render("✖️
const (
None Screen = iota
Files
Cfg
Files
Main
)
type File struct {
Name string
Encode bool
Path string
Selected bool
}
type model struct {
type Model struct {
IsDirectory bool
Path string
CurrentFileName string
FileCount int
Files []string
Files []File
Spinner spinner.Model
SingleFileProgressBar progress.Model
SingleFileProgress float64
@@ -51,13 +51,16 @@ type model struct {
Config []Config
VisibleConfig []Config
FocusIndex int
ChoiceIndex int
ParsedConfig ParsedConfig
DryRun bool
ErrQuit bool
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 = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
@@ -83,12 +86,13 @@ func initialModel(fileInfo os.FileInfo, absolutePath string) *model {
}
}
return &model{
return &Model{
IsDirectory: fileInfo.IsDir(),
CurrentFileName: fileInfo.Name(),
Path: absolutePath,
FileCount: 0,
Files: make([]string, 0),
Files: make([]File, 0),
Screen: Cfg,
Spinner: s,
SingleFileProgressBar: progress.New(progress.WithGradient("#1010ff", "#00ff00")),
SingleFileProgress: 0.0,
@@ -102,7 +106,7 @@ func initialModel(fileInfo os.FileInfo, absolutePath string) *model {
}
}
func (m model) updateConfigFocusedOptions() {
func (m Model) updateConfigFocusedOptions() {
for _, visCfg := range m.VisibleConfig {
for i := range m.Config {
if m.Config[i].Name == visCfg.Name {
@@ -112,11 +116,11 @@ func (m model) updateConfigFocusedOptions() {
}
}
func (m model) Init() 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) {
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
@@ -128,32 +132,61 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.FileCount = msg.fileCount
m.Files = msg.files
if len(m.Files) == 1 {
m.Screen = Cfg
} else if len(m.Files) > 1 {
m.Screen = Files
}
return m, nil
}
switch m.Screen {
case Files:
// TODO: navigate files and select them and stuff
switch msg := msg.(type) {
case tea.KeyMsg:
key := msg.String()
switch key {
case "ctrl+c", "esc":
return m, tea.Quit
case "enter", "space":
// TODO: change the file state to Encode = true
// Also change the files to be of File type
// t := m.Files[m.FocusIndex]
case "enter", " ":
if m.FocusIndex == 0 && 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.FocusIndex != 0 {
m.Files[m.FocusIndex-1].Selected = !m.Files[m.FocusIndex-1].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
}
}
case "g":
m.FocusIndex = 0
case "G":
m.FocusIndex = len(m.Files) - 1
m.FocusIndex = len(m.Files)
case "left", "right", "h", "l":
if m.FocusIndex == 0 && 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", "up", "down", "j", "k":
if key == "up" || key == "shift+tab" || key == "k" {
m.FocusIndex--
@@ -161,10 +194,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.FocusIndex++
}
if m.FocusIndex >= len(m.Files) {
if m.FocusIndex >= len(m.Files)+1 {
m.FocusIndex = 0
} else if m.FocusIndex < 0 {
m.FocusIndex = len(m.Files) - 1
m.FocusIndex = len(m.Files)
}
}
}
@@ -178,16 +211,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "enter", " ":
// parse config and switch to main screen if we're focused on the start button
if m.FocusIndex == len(m.VisibleConfig) {
if m.ChoiceIndex == 0 {
return m, m.parseConfig(false)
}
// parse config, print it and exit
if m.FocusIndex == len(m.VisibleConfig)+1 {
} else if m.ChoiceIndex == 1 {
return m, m.parseConfig(true)
}
}
case "g":
m.FocusIndex = 0
case "G":
m.FocusIndex = len(m.VisibleConfig) + 1
m.FocusIndex = len(m.VisibleConfig)
case "tab", "shift+tab", "up", "down", "j", "k":
if key == "up" || key == "shift+tab" || key == "k" {
m.FocusIndex--
@@ -195,10 +228,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.FocusIndex++
}
if m.FocusIndex >= len(m.VisibleConfig)+2 {
if m.FocusIndex > len(m.VisibleConfig) {
m.FocusIndex = 0
} else if m.FocusIndex < 0 {
m.FocusIndex = len(m.VisibleConfig) + 1
m.FocusIndex = len(m.VisibleConfig)
}
case "left", "right", "h", "l":
// If we're not hovering a button
@@ -218,15 +251,39 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
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:
m.Quitting = true
return m, tea.Quit
case parsedCfgMsg:
m.Screen = Main
m.ParsedConfig = msg.parsedConfig
return m, tea.Batch(m.Spinner.Tick, startEncoding)
if m.DryRun {
return m, tea.Batch(tea.ExitAltScreen, tea.Quit)
}
if m.IsDirectory {
m.Screen = Files
m.FocusIndex = 0
m.ChoiceIndex = 0
} else {
m.Screen = Main
return m, tea.Batch(m.Spinner.Tick, encodeVideo)
}
}
case Main:
switch msg := msg.(type) {
@@ -242,18 +299,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, tea.Sequence(tea.ExitAltScreen, tea.Quit)
}
case encodingStartedMsg:
if m.DryRun {
return m, tea.Sequence(tea.ExitAltScreen, gracefullyQuit)
}
return m, encodeVideo
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() {
fullFilePath := m.Files[len(m.Files)-1]
encode(fullFilePath, filepath.Base(fullFilePath), m.Program, m.ParsedConfig)
file := m.Files[len(m.Files)-1]
encode(file, filepath.Base(file.Path), m.Program, m.ParsedConfig)
}()
return m, nil
@@ -263,8 +314,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
case finishedEncodingVideo:
if m.ParsedConfig.DeleteOldVideo {
m.CurrentFileName = fmt.Sprintf("Deleting: %s", filepath.Base(m.Files[len(m.Files)-1]))
os.Remove(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].Path)
}
m.Files = m.Files[:len(m.Files)-1]
@@ -311,7 +362,7 @@ func parseConfig(cfg []Config) ParsedConfig {
return ParsedConfig{
DeleteOldVideo: find(cfg, "Delete old video(s)?").FocusedOption != 0,
SkipEncodedVid: find(cfg, "What should we do about encoded videos?").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],
@@ -319,25 +370,58 @@ func parseConfig(cfg []Config) ParsedConfig {
}
}
func FilesScreenView(m model) string {
view := ""
func FilesScreenView(m Model) string {
view := lipgloss.NewStyle().Margin(1, 0).Render("Select the files you wish to encode.")
var files string
var buttons string
var selectAllBtnText string
// TODO: Create a nice view where we can select different videos and then select the encoding options and start
// encoding.
for i, file := range m.Files {
if m.FocusIndex == i {
view += fmt.Sprintf(FocusedConfig.Render("[%s] %s"), " ", filepath.Base(file))
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 {
view += fmt.Sprintf(BlurredConfig.Render("[%s] %s"), " ", filepath.Base(file))
selectAllBtnText = "Select All"
}
blurredStartButton := BlurredStartButton
focusedStartButton := FocusedStartButton
if noneSelected {
blurredStartButton = DisabledStartButton
focusedStartButton = DisabledStartButton
}
if m.FocusIndex == 0 {
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)
}
// TODO: files should be a scrollable grid
for i, file := range m.Files {
selection := " "
if file.Selected {
selection = "x"
}
if m.FocusIndex == i+1 {
files += fmt.Sprintf(FocusedConfig.Render("[%s] %s"), selection, filepath.Base(file.Path))
} else {
files += fmt.Sprintf(BlurredConfig.Render("[%s] %s"), selection, filepath.Base(file.Path))
}
}
view += "\n"
view += lipgloss.JoinVertical(0, buttons, files)
return view
}
func CfgScreenView(m model) string {
func CfgScreenView(m Model) string {
view := ""
for i, cfg := range m.VisibleConfig {
@@ -362,27 +446,39 @@ func CfgScreenView(m model) string {
}
}
if m.FocusIndex == len(m.VisibleConfig) {
view += FocusedStartButton
var startButton string
var dryRunButton string
if m.FocusIndex == len(m.VisibleConfig) && m.ChoiceIndex == 0 {
if m.IsDirectory {
startButton = FocusedNextButton
} else {
view += BlurredStartButton
startButton = FocusedStartButton
}
} else {
if m.IsDirectory {
startButton = BlurredNextButton
} else {
startButton = BlurredStartButton
}
}
if m.FocusIndex == len(m.VisibleConfig)+1 {
view += FocusedDryRunButton
if m.FocusIndex == len(m.VisibleConfig) && m.ChoiceIndex == 1 {
dryRunButton = FocusedDryRunButton
} else {
view += BlurredDryRunButton
dryRunButton = BlurredDryRunButton
}
view += lipgloss.JoinHorizontal(0, startButton, dryRunButton)
view += "\n"
return view
}
func MainScreenView(m model) string {
func MainScreenView(m Model) string {
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",
m.Spinner.View(),
m.FileCount-len(m.Files),
@@ -425,28 +521,13 @@ func formatEstimate(estimate int) string {
return fmt.Sprintf("%ds", estimate)
}
func (m model) View() string {
if m.Quitting {
func (m Model) View() string {
if m.DryRun {
sb := strings.Builder{}
for _, file := range m.Files {
parentDir := filepath.Dir(file)
fileName := filepath.Base(file)
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(file, outFileFullPath, m.ParsedConfig)...)
sb.WriteString(cmd.String())
sb.WriteByte('\n')
return ""
}
return fmt.Sprintf("%s %s\n", Checkmark, sb.String())
} else {
if m.Quitting {
return fmt.Sprintf("%s %d/%d files encoded\n", Checkmark, m.FileCount-len(m.Files), m.FileCount)
}
} else if m.Cancelled {
// TODO: should we clean up the file ourselves?
return fmt.Sprintf("%s Encoding cancelled. Stopped ffmpeg process.\n Make sure to clean up the created file\n", X)
+10 -16
View File
@@ -27,10 +27,10 @@ type ffmpegProcessStart struct {
type filesStatMsg struct {
fileCount int
files []string
files []File
}
func (m *model) statFiles() tea.Msg {
func (m *Model) statFiles() tea.Msg {
if m.IsDirectory {
entries, _ := os.ReadDir(m.Path)
@@ -53,7 +53,7 @@ func (m *model) statFiles() tea.Msg {
}
m.FileCount++
m.Files = append(m.Files, fullFilePath)
m.Files = append(m.Files, File{Path: fullFilePath})
}
if len(m.Files) == 0 {
@@ -61,7 +61,7 @@ func (m *model) statFiles() tea.Msg {
}
} else {
m.FileCount = 1
m.Files = append(m.Files, m.Path)
m.Files = append(m.Files, File{Path: m.Path, Selected: true})
}
return filesStatMsg{
@@ -70,12 +70,6 @@ func (m *model) statFiles() tea.Msg {
}
}
type encodingStartedMsg struct{}
func startEncoding() tea.Msg {
return encodingStartedMsg{}
}
type encodeVideoMsg struct{}
func encodeVideo() tea.Msg {
@@ -93,7 +87,7 @@ type parsedCfgMsg struct {
dryRun bool
}
func (m *model) parseConfig(dryRun bool) tea.Cmd {
func (m *Model) parseConfig(dryRun bool) tea.Cmd {
m.DryRun = dryRun
return func() tea.Msg {
@@ -105,7 +99,7 @@ func (m *model) parseConfig(dryRun bool) tea.Cmd {
return parsedCfgMsg{
parsedConfig: ParsedConfig{
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],
AudioEncoder: aEncoder.Opts[aEncoder.FocusedOption],
Preset: preset.Opts[preset.FocusedOption],
@@ -120,15 +114,15 @@ type errQuitMsg struct {
msg string
}
func (m *model) cleanUp() tea.Msg {
func (m *Model) cleanUp() tea.Msg {
if len(m.Files) == 0 {
return tea.Quit()
}
fullFilePath := m.Files[len(m.Files)-1]
fileName := filepath.Base(fullFilePath)
file := m.Files[len(m.Files)-1]
fileName := filepath.Base(file.Path)
parentDir := filepath.Dir(fullFilePath)
parentDir := filepath.Dir(file.Path)
extensionIndex := strings.LastIndex(fileName, ".")
newFileName := fileName[:extensionIndex]
extension := fileName[extensionIndex:]
+8 -31
View File
@@ -18,7 +18,7 @@ var SupportedAudioEncoders = []string{
var Configs = []Config{
{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"}},
{Name: "Audio Encoder", Opts: []string{"None", "copy"}, FocusedOption: 1},
{Name: "Preset", Opts: []string{"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"}, FocusedOption: 4},
@@ -33,7 +33,7 @@ type Config struct {
type ParsedConfig struct {
DeleteOldVideo bool
SkipEncodedVid bool
IgnoreConflictingName bool
VideoEncoder string
AudioEncoder string
Preset string
@@ -51,41 +51,18 @@ func find(cfgs []Config, name string) Config {
panic(err)
}
func filter(cfgs []Config, names ...string) []Config {
filtered := make([]Config, 0)
cfgloop:
for _, cfg := range cfgs {
for _, name := range names {
if cfg.Name == name {
continue cfgloop
}
}
filtered = append(filtered, cfg)
}
return filtered
}
func contains[T comparable](slice []T, elem T) bool {
for _, e := range slice {
if e == elem {
return true
}
}
return false
}
func getVisibleConfigs(cfgs []Config) []Config {
parsed := parseConfig(cfgs)
switch parsed.VideoEncoder {
case "copy", "librav1e":
return filter(cfgs, "Preset", "Constant Rate Factor (CRF)")
return filter(cfgs, func(c Config) bool {
return c.Name != "Preset" && c.Name != "Constant Rate Factor (CRF)"
})
case "libvpx-vp9", "libsvtav1":
return filter(cfgs, "Preset")
return filter(cfgs, func(c Config) bool {
return c.Name != "Preset"
})
}
return cfgs
+3 -1
View File
@@ -3,7 +3,9 @@ package main
import "testing"
func TestFilterConfigs(t *testing.T) {
cfgs := filter(Configs, "Preset", "Constant Rate Factor (CRF)")
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" {
+24 -6
View File
@@ -46,11 +46,29 @@ func main() {
log.Fatal(err)
}
_, err = p.Run()
final, err := p.Run()
if err != nil {
fmt.Println(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 {
@@ -100,8 +118,8 @@ func buildFFmpegCmdArgs(fullFilePath string, outFullFilePath string, cfg ParsedC
return args
}
func encode(fullFilePath string, fileName string, teaP *tea.Program, cfg ParsedConfig) {
mType, err := mimetype.DetectFile(fullFilePath)
func encode(file File, fileName string, teaP *tea.Program, cfg ParsedConfig) {
mType, err := mimetype.DetectFile(file.Path)
if err != nil {
log.Fatal(err)
}
@@ -110,14 +128,14 @@ func encode(fullFilePath string, fileName string, teaP *tea.Program, cfg ParsedC
log.Fatalf("%s is not a valid video file\n", fileName)
}
parentDir := filepath.Dir(fullFilePath)
parentDir := filepath.Dir(file.Path)
extensionIndex := strings.LastIndex(fileName, ".")
newFileName := fileName[:extensionIndex]
extension := fileName[extensionIndex:]
newFileFullPath := filepath.Join(parentDir, newFileName+fmt.Sprintf("_[%s]_[%s]", cfg.VideoEncoder, cfg.AudioEncoder)+extension)
if _, err := os.Stat(newFileFullPath); err == nil {
if cfg.SkipEncodedVid {
if cfg.IgnoreConflictingName {
log.Printf("Skipping \"%s\" because it already exists with the exact same encodings (crf and preset might be different though)", newFileFullPath)
teaP.Send(finishedEncodingVideo{})
return
@@ -126,7 +144,7 @@ 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...)
teaP.Send(ffmpegProcessStart{cmd})
err = cmd.Run()
+48
View File
@@ -18,6 +18,11 @@ var (
Light: "#1C6DD0",
}
DisabledColor = lipgloss.AdaptiveColor{
Dark: "#444",
Light: "#AAA",
}
BlurredConfig = lipgloss.NewStyle().
Foreground(PrimaryColor).
MarginTop(1)
@@ -49,6 +54,32 @@ var (
Padding(0, 2).
Align(lipgloss.Center).
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().
Border(lipgloss.NormalBorder()).
@@ -67,4 +98,21 @@ var (
Padding(0, 2).
Align(lipgloss.Center).
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)
)
+43
View File
@@ -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
}