diff options
Diffstat (limited to 'src/screens')
| -rw-r--r-- | src/screens/available_packages.rs | 38 | ||||
| -rw-r--r-- | src/screens/installed_packages.rs | 37 |
2 files changed, 43 insertions, 32 deletions
diff --git a/src/screens/available_packages.rs b/src/screens/available_packages.rs index a96437b..b16eac1 100644 --- a/src/screens/available_packages.rs +++ b/src/screens/available_packages.rs @@ -7,8 +7,8 @@ use ratatui::{ Frame, crossterm::event::KeyCode, layout::{Alignment, Constraint, Layout, Rect}, - style::{Color, Style}, - text::{Line, Span, Text}, + style::Color, + text::{Line, Span}, widgets::{Block, Borders, List, ListState, Paragraph}, }; @@ -136,16 +136,16 @@ impl AvailablePackagesScreen { List::new(pkg_versions.iter().map(|v| version_to_variant(v))) .style(Color::White) .highlight_style(Theme::selected()) - .highlight_symbol("> "), // , // .block( - // Block::default() - // .borders(Borders::ALL) - // .border_style(Theme::block()) - // .title("Variants"), - // ), + .highlight_symbol("> "), ); self.selected_version = Some(pkg_versions[0].clone()); } + + fn select_package_index(&mut self, index: usize, repo: &Portage) { + self.pkg_list_state.select(Some(index)); + self.build_pkg_version_list(repo); + } } impl Default for AvailablePackagesScreen { @@ -258,6 +258,10 @@ impl Screen for AvailablePackagesScreen { let text = if self.search_popup.visible { "(esc) to quit search | (enter) to search".to_string() + } else if let Some((current, total)) = self.search_popup.match_status() { + format!( + "(q) to quit | (j) down | (k) up | (space) switch panel | (/) to search | Search {current}/{total} | (n) next | (N) previous" + ) } else { "(q) to quit | (j) down | (k) up | (space) switch panel | (/) to search".to_string() }; @@ -312,6 +316,18 @@ impl Screen for AvailablePackagesScreen { KeyCode::Char(' ') => self.cycle_list(), KeyCode::Char('/') => self.search_popup.toggle(), + KeyCode::Char('n') => { + if let Some(index) = self.search_popup.next_match() { + self.select_package_index(index, repo) + } + } + + KeyCode::Char('N') => { + if let Some(index) = self.search_popup.previous_match() { + self.select_package_index(index, repo) + } + } + _ => match self.chosen_list { CurrentList::PackageList => match key.code { KeyCode::Char('j') => { @@ -369,12 +385,6 @@ impl Screen for AvailablePackagesScreen { .style(Color::White) .highlight_style(Theme::selected()) .highlight_symbol("> "); - // .block( - // Block::default() - // .borders(Borders::ALL) - // .border_style(Theme::block()) - // .title("Available packages"), - // ); self.pkg_list = Some(list); self.loading_state = LoadingState::Complete; diff --git a/src/screens/installed_packages.rs b/src/screens/installed_packages.rs index b373cb4..24fab50 100644 --- a/src/screens/installed_packages.rs +++ b/src/screens/installed_packages.rs @@ -4,17 +4,17 @@ use crate::{ signal::{Event, Signal}, theme::Theme, widgets::{ - helpers::search_popup_rect, package_metadata::render_package_metadata, - search_popup::SearchPopup, use_flags::render_use_flags, + package_metadata::render_package_metadata, search_popup::SearchPopup, + use_flags::render_use_flags, }, }; use ratatui::{ Frame, crossterm::event::{KeyCode, KeyModifiers}, layout::{Constraint, Layout, Rect}, - style::{Color, Modifier, Style}, + style::Color, text::{Line, Span}, - widgets::{Block, Borders, Clear, List, ListState, Paragraph}, + widgets::{Block, Borders, List, ListState, Paragraph}, }; #[derive(Debug)] @@ -114,7 +114,6 @@ impl Screen for InstalledPackagesScreen { frame, split_vert[0], pkg.iuse.iter().collect(), - // TODO: Dont use clone here if possible &pkg.enabled_use_flags, ); render_package_metadata( @@ -129,19 +128,13 @@ impl Screen for InstalledPackagesScreen { let text = if self.search_popup.visible { "(esc) to quit search | (enter) to search".to_string() pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #49483e }
.highlight .c { color: #959077 } /* Comment */
.highlight .err { color: #ed007e; background-color: #1e0010 } /* Error */
.highlight .esc { color: #f8f8f2 } /* Escape */
.highlight .g { color: #f8f8f2 } /* Generic */
.highlight .k { color: #66d9ef } /* Keyword */
.highlight .l { color: #ae81ff } /* Literal */
.highlight .n { color: #f8f8f2 } /* Name */
.highlight .o { color: #ff4689 } /* Operator */
.highlight .x { color: #f8f8f2 } /* Other */
.highlight .p { color: #f8f8f2 } /* Punctuation */
.highlight .ch { color: #959077 } /* Comment.Hashbang */
.highlight .cm { color: #959077 } /* Comment.Multiline */
.highlight .cp { color: #959077 } /* Comment.Preproc */
.highlight .cpf { color: #959077 } /* Comment.PreprocFile */
.highlight .c1 { color: #959077 } /* Comment.Single */
.highlight .cs { color: #959077 } /* Comment.Special */
.highlight .gd { color: #ff4689 } /* Generic.Deleted */
.highlight .ge { color: #f8f8f2; font-style: italic } /* Generic.Emph */
.highlight .ges { color: #f8f8f2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #f8f8f2 } /* Generic.Error */
.highlight .gh { color: #f8f8f2 } /* Generic.Heading */
.highlight .gi { color: #a6e22e } /* Generic.Inserted */
.highlight .go { color: #66d9ef } /* Generic.Output */
.highlight .gp { color: #ff4689; font-weight: bold } /* Generic.Prompt */
.highlight .gs { color: #f8f8f2; font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #959077 } /* Generic.Subheading */
.highlight .gt { color: #f8f8f2 } /* Generic.Traceback */
.highlight .kc { color: #66d9ef } /* Keyword.Constant */
.highlight .kd { color: #66d9ef } /* Keyword.Declaration */
.highlight .kn { color: #ff4689 } /* Keyword.Namespace */
.highlight .kp { color: #66d9ef } /* Keyword.Pseudo */
.highlight .kr { color: #66d9ef } /* Keyword.Reserved */
.highlight .kt { color: #66d9ef } /* Keyword.Type */
.highlight .ld { color: #e6db74 } /* Literal.Date */
.highlight .m { color: #ae81ff } /* Literal.Number */
.highlight .s { color: #e6db74 } /* Literal.String */
.highlight .na { color: #a6e22e } /* Name.Attribute */
.highlight .nb { color: #f8f8f2 } /* Name.Builtin */
.highlight .nc { color: #a6e22e } /* Name.Class */
.highlight .no { color: #66d9ef } /* Name.Constant */
.highlight .nd { color: #a6e22e } /* Name.Decorator */
.highlight .ni { color: #f8f8f2 } /* Name.Entity */
.highlight .ne { color: #a6e22e } /* Name.Exception */
.highlight .nf { color: #a6e22e } /* Name.Function */
.highlight .nl { color: #f8f8f2 } /* Name.Label */
.highlight .nn { color: #f8f8f2 } /* Name.Namespace */
.highlight .nx { color: #a6e22e } /* Name.Other */
.highlight .py { color: #f8f8f2 } /* Name.Property */
.highlight .nt { color: #ff4689 } /* Name.Tag */
.highlight .nv { color: #f8f8f2 } /* Name.Variable */
.highlight .ow { color: #ff4689 } /* Operator.Word */
.highlight .pm { color: #f8f8f2 } /* Punctuation.Marker */
.highlight .w { color: #f8f8f2 } /* Text.Whitespace */
.highlight .mb { color: #ae81ff } /* Literal.Number.Bin */
.highlight .mf { color: #ae81ff } /* Literal.Number.Float */
.highlight .mh { color: #ae81ff } /* Literal.Number.Hex */
.highlight .mi { color: #ae81ff } /* Literal.Number.Integer */
.highlight .mo { color: #ae81ff } /* Literal.Number.Oct */
.highlight .sa { color: #e6db74 } /* Literal.String.Affix */
.highlight .sb { color: #e6db74 } /* Literal.String.Backtick */
.highlight .sc { color: #e6db74 } /* Literal.String.Char */
.highlight .dl { color: #e6db74 } /* Literal.String.Delimiter */
.highlight .sd { color: #e6db74 } /* Literal.String.Doc */
.highlight .s2 { color: #e6db74 } /* Literal.String.Double */
.highlight .se { color: #ae81ff } /* Literal.String.Escape */
.highlight .sh { color: #e6db74 } /* Literal.String.Heredoc */
.highlight .si { color: #e6db74 } /* Literal.String.Interpol */
.highlight .sx { color: #e6db74 } /* Literal.String.Other */
.highlight .sr { color: #e6db74 } /* Literal.String.Regex */
.highlight .s1 { color: #e6db74 } /* Literal.String.Single */
.highlight .ss { color: #e6db74 } /* Literal.String.Symbol */
.highlight .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #a6e22e } /* Name.Function.Magic */
.highlight .vc { color: #f8f8f2 } /* Name.Variable.Class */
.highlight .vg { color: #f8f8f2 } /* Name.Variable.Global */
.highlight .vi { color: #f8f8f2 } /* Name.Variable.Instance */
.highlight .vm { color: #f8f8f2 } /* Name.Variable.Magic */
.highlight .il { color: #ae81ff } /* Literal.Number.Integer.Long */package thumbnailgen
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os/exec"
"regexp"
"slices"
"strconv"
"strings"
"time"
)
type TimeFilter struct {
Start float64
End float64
}
type Options struct {
Interval int
Segments int
UseSegments bool
Format string
Scale string
EnableFilter bool
MaxThumbnails int
}
func NewDefaultOptions() Options {
return Options{
Interval: 5,
Segments: 0,
UseSegments: false,
Format: "webp",
Scale: "-1:-1",
EnableFilter: true,
MaxThumbnails: 0,
}
}
func (options *Options) Apply(opts ...func(*Options)) *Options {
for _, opt := range opts {
opt(options)
}
return options
}
type videoStreamProbe struct {
Streams []struct {
CodecType string `json:"codec_type"`
Duration string `json:"duration"`
} `json:"streams"`
}
func getThumbnailDuration(path string) (float64, error) {
stderr := bytes.NewBuffer(nil)
stdout := bytes.NewBuffer(nil)
cmd := exec.Command(
"ffprobe",
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=codec_type,duration",
"-of", "json",
path,
)
cmd.Stdout = stdout
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
return 0, fmt.Errorf(
"probe video stream: %w: %s",
err,
strings.TrimSpace(stderr.String()),
)
}
var probe videoStreamProbe
if err := json.Unmarshal(stdout.Bytes(), &probe); err != nil {
return 0, fmt.Errorf("decode video-stream probe: %w", err)
}
if len(probe.Streams) == 0 || probe.Streams[0].CodecType != "video" {
return 0, errors.New("input has no video stream")
}
durationText := strings.TrimSpace(probe.Streams[0].Duration)
if durationText == "" || durationText == "N/A" {
return GetVideoLength(path) // container-duration fallback for animated WebP
}
duration, err := strconv.ParseFloat(durationText, 64)
if err != nil {
return 0, fmt.Errorf("parse video-stream duration %q: %w", durationText, err)
}
if duration <= 0 {
return 0, fmt.Errorf("invalid video-stream duration %v", duration)
}
return duration, nil
}
func (opts *Options) GetThumbnail(path string) ([][]byte, int, error) {
var filters []TimeFilter
if opts.EnableFilter {
f, err := GetFilter(path)
if err != nil {
return nil, 0, err
}
filters = f
}
length, err := getThumbnailDuration(path)
if err != nil {
return nil, 0, err
}
var interval int
if opts.UseSegments {
interval = int(length / float64(opts.Segments))
} else {
interval = opts.Interval
}
return GetThumbnailUnderlying(path, opts.MaxThumbnails, filters, length, interval, opts.EnableFilter, opts.Format, opts.Scale)
}
func GetThumbnailUnderlying(path string, maxThumbnails int, filters []TimeFilter, length float64, intervalSeconds int, enableFilter bool, format string, scale string) ([][]byte, int, error) {
buf := bytes.NewBuffer(nil)
framesExtracted := 0
var out [][]byte
if maxThumbnails > 0 {
out = make([][]byte, maxThumbnails)
} else {
out = make([][]byte, 0)
}
var time float64 = 1
for {
if (maxThumbnails > 0 && framesExtracted >= maxThumbnails) || time >= length {
break
}
if enableFilter {
if ok, next := FrameLiesWithinFilter(time, filters); ok {
time = next + 1
continue
}
}
err := GetImage(buf, path, int(time), format, scale)
if err != nil {
return nil, 0, err
}
b := bytes.Clone(buf.Bytes())[0:buf.Len()]
if maxThumbnails > 0 {
out[framesExtracted] = b
} else {
out = append(out, b)
}
time += float64(intervalSeconds)
framesExtracted += 1
buf.Reset()
}
return out, framesExtracted, nil
}
// Return true, if time is inside filter, also returns next time, when not in filter anymore.
// If False it returns input time
func FrameLiesWithinFilter(time float64, filters []TimeFilter) (bool, float64) {
for _, filter := range filters {
if time >= filter.Start && time <= filter.End {
return true, filter.End
}
}
return false, time
}
func GetImage(buf *bytes.Buffer, path string, timestamp int, format string, scale string) error {
var t time.Time
t = t.Add(time.Duration(timestamp) * time.Second)
stderr := bytes.NewBuffer(nil)
cmd := exec.Command(
"ffmpeg",
"-ss",
t.Format("15:04:05"),
"-i",
path,
"-vframes",
"1",
"-c:v",
format,
"-filter:v",
fmt.Sprintf("scale=%s", scale),
"-f",
"image2pipe",
"-",
)
cmd.Stdout = buf
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("generate thumbnail at %s: %w: %s", t.Format("15:04:05"), err, strings.TrimSpace(stderr.String()))
}
return nil
}
func GetVideoLength(path string) (float64, error) {
cmd := exec.Command("ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", path)
stderr := bytes.NewBuffer(nil)
buf := bytes.NewBuffer(nil)
cmd.Stdout = buf
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
return 0, fmt.Errorf(
"probe video duration: %w: %s",
err,
strings.TrimSpace(stderr.String()),
)
}
return strconv.ParseFloat(strings.ReplaceAll(buf.String(), "\n", ""), 64)
}
func escapeFilterValue(value string) string {
return strings.NewReplacer(
"\\", "\\\\",
"'", "\\'",
":", "\\:",
",", "\\,",
";", "\\;",
"[", "\\[",
"]", "\\]",
).Replace(value)
}
func GetFilter(path string) ([]TimeFilter, error) {
buf := bytes.NewBuffer(nil)
stderr := bytes.NewBuffer(nil)
filter := fmt.Sprintf("movie=filename='%s',blackdetect[out0]", escapeFilterValue(path))
cmd := exec.Command("ffprobe", "-f", "lavfi", "-i", filter, "-show_entries", "tags=lavfi.black_start,lavfi.black_end", "-of", "default=nw=1", "-v", "quiet")
cmd.Stderr = stderr
cmd.Stdout = buf
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("detect black frames: %w: %s", err, strings.TrimSpace(stderr.String()))
}
filterStr := buf.String()
filters := strings.Split(filterStr, "\n")
filters = slices.Compact(filters)
i := 0
reg := regexp.MustCompile(`([0-9]*\.?[0-9]+)`)
blackFilters := make([]TimeFilter, len(filters)/2)
for {
if i >= len(filters) {
break
}
start := reg.FindString(filters[i])
var end string
if i+1 >= len(filters) || filters[i+1] == "" {
end = start
} else {
end = reg.FindString(filters[i+1])
}
if start == "" || end == "" {
break
}
s, err := strconv.ParseFloat(start, 64)
if err != nil {
return nil, err
}
e, err := strconv.ParseFloat(end, 64)
if err != nil {
return nil, err
}
blackFilters[i/2] = TimeFilter{
Start: s,
End: e,
}
i += 2
}
return blackFilters, nil
}
func GetFramerate(path string) (float64, error) {
cmd := exec.Command("ffprobe", "-v", "0", "-of", "csv=p=0", "-select_streams", "v:0", "-show_entries", "stream=r_frame_rate", path)
buf := bytes.NewBuffer(nil)
cmd.Stdout = buf
err := cmd.Run()
if err != nil {
return 0, err
}
s := buf.String()
s = strings.ReplaceAll(s, "\n", "")
s = strings.TrimSpace(s)
ss := strings.Split(s, "/")
f1, err := strconv.Atoi(ss[0])
if err != nil {
return 0, err
}
f2, err := strconv.Atoi(ss[1])
if err != nil {
return 0, err
}
return float64(f1) / float64(f2), nil
}
|
