Refactor and split tui into smaller files

This commit is contained in:
HikariKnight 2023-04-08 19:38:25 +02:00
parent b339dab29f
commit 123d1ca18a
12 changed files with 603 additions and 508 deletions

View file

@ -1,9 +1,9 @@
package main
import (
internal "github.com/HikariKnight/quickpassthrough/internal"
downloader "github.com/HikariKnight/quickpassthrough/internal/ls_iommu_downloader"
"github.com/HikariKnight/quickpassthrough/internal/tuimode"
"github.com/HikariKnight/quickpassthrough/pkg/params"
"github.com/HikariKnight/quickpassthrough/internal/params"
)
func main() {
@ -11,7 +11,7 @@ func main() {
pArg := params.NewParams()
if !pArg.Flag["gui"] {
downloader.GetLsIOMMU()
tuimode.App()
downloader.CheckLsIOMMU()
internal.Tui()
}
}

View file

@ -0,0 +1,57 @@
package internal
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/HikariKnight/quickpassthrough/internal/configs"
)
func generateVBIOSDumper(m model) {
// Get the vbios path
m.vbios_path = getIOMMU("-g", "-i", m.gpu_group, "--rom")[0]
// Get the config directories
config := configs.GetConfigPaths()
// Get the program directory
exe, _ := os.Executable()
scriptdir := filepath.Dir(exe)
// If we are using go run use the working directory instead
if strings.Contains(scriptdir, "/tmp/go-build") {
scriptdir, _ = os.Getwd()
}
vbios_script_template := fmt.Sprint(
"#!/bin/bash\n",
"# THIS FILE IS AUTO GENERATED!\n",
"# IF YOU HAVE CHANGED GPU, PLEASE RE-RUN QUICKPASSTHROUGH!\n",
"echo 1 | sudo tee %s\n",
"sudo bash -c \"cat %s\" > %s/%s/vfio_card.rom\n",
"echo 0 | sudo tee %s\n",
)
vbios_script := fmt.Sprintf(
vbios_script_template,
m.vbios_path,
m.vbios_path,
scriptdir,
config.QUICKEMU,
m.vbios_path,
)
scriptfile, err := os.Create("utils/dump_vbios.sh")
errorcheck.ErrorCheck(err, "Cannot create file \"utils/dump_vbios.sh\"")
defer scriptfile.Close()
// Make the script executable
scriptfile.Chmod(0775)
errorcheck.ErrorCheck(err, "Could not change permissions of \"utils/dump_vbios.sh\"")
// Write the script
scriptfile.WriteString(vbios_script)
}

View file

@ -14,7 +14,7 @@ import (
"time"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/HikariKnight/quickpassthrough/internal/untar"
"github.com/HikariKnight/quickpassthrough/pkg/untar"
"github.com/cavaliergopher/grab/v3"
)
@ -92,7 +92,7 @@ type Response struct {
Body string `json:"body"`
}
func GetLsIOMMU() {
func CheckLsIOMMU() {
// Check the API for releases
resp, err := http.Get("https://api.github.com/repos/hikariknight/ls-iommu/releases/latest")
errorcheck.ErrorCheck(err)

View file

@ -1,499 +0,0 @@
package tuimode
// A simple example demonstrating the use of multiple text input components
// from the Bubbles component library.
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/HikariKnight/quickpassthrough/internal/configs"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
titleStyle = lipgloss.NewStyle().
Background(lipgloss.Color("#5F5FD7")).
Foreground(lipgloss.Color("#FFFFFF")).
PaddingLeft(2).PaddingRight(2)
helpStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color(241))
listStyle = lipgloss.NewStyle().
PaddingLeft(2)
choiceStyle = lipgloss.NewStyle().PaddingLeft(4)
selectedChoiceStyle = lipgloss.NewStyle().
PaddingLeft(2).
Foreground(lipgloss.Color("170"))
dialogStyle = lipgloss.NewStyle().
PaddingLeft(2).
Width(78)
)
// Make a status type
type status int
// List item struct
type item struct {
title, desc string
}
// Functions needed for item struct
func (i item) Title() string { return i.title }
func (i item) Description() string { return i.desc }
func (i item) FilterValue() string { return i.title }
// Choice delegate (for our dialog boxes)
type choiceDelegate struct{}
func (d choiceDelegate) Height() int { return 1 }
func (d choiceDelegate) Spacing() int { return 0 }
func (d choiceDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
func (d choiceDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
i, ok := listItem.(item)
if !ok {
return
}
str := i.title
fn := choiceStyle.Render
if index == m.Index() {
fn = func(s ...string) string {
return selectedChoiceStyle.Render("| " + strings.Join(s, " "))
}
}
fmt.Fprint(w, fn(str))
}
// Main Model
type model struct {
fetched []bool
lists []list.Model
gpu_group string
vbios_path string
loaded bool
focused status
width int
height int
}
// Consts used to navigate the main model
const (
INTRO status = iota
GPUS
GPU_GROUP
VBIOS
VIDEO
USB
USB_GROUP
DONE
)
func (m *model) initLists(width, height int) {
defaultList := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 10)
choiceList := list.New([]list.Item{}, choiceDelegate{}, 0, 7)
// Disable features we wont need
defaultList.SetShowTitle(false)
defaultList.SetFilteringEnabled(false)
defaultList.SetSize(width, height)
choiceList.SetShowTitle(false)
choiceList.SetFilteringEnabled(false)
// Add height and width to our model so we can use it later
m.width = width
m.height = height
m.lists = []list.Model{
choiceList,
defaultList,
defaultList,
choiceList,
choiceList,
defaultList,
defaultList,
choiceList,
}
m.fetched = []bool{
false,
false,
false,
false,
false,
false,
false,
false,
}
m.focused = INTRO
// Init INTRO choices
items := []list.Item{
item{title: "CONTINUE"},
}
m.lists[INTRO].SetHeight(5)
m.lists[INTRO].SetItems(items)
// Init GPU list
//m.lists[GPUS].Title = "Select a GPU to check the IOMMU groups of"
items = StringList2ListItem(GetIOMMU("-g", "-F", "name,device_id,optional_revision"))
m.lists[GPUS].SetItems(items)
m.fetched[GPUS] = true
m.lists[GPU_GROUP].Title = ""
m.lists[GPU_GROUP].SetItems(items)
// Init USB Controller list
items = StringList2ListItem(GetIOMMU("-u", "-F", "name,device_id,optional_revision"))
m.lists[USB].SetItems(items)
m.fetched[USB] = true
m.lists[USB_GROUP].Title = ""
m.lists[USB_GROUP].SetItems(items)
// Init VBIOS choices
items = []list.Item{
item{title: "OK"},
}
m.lists[VBIOS].SetItems(items)
// Init VIDEO disable choises
items = []list.Item{
item{title: "YES"},
item{title: "NO"},
}
m.lists[VIDEO].SetItems(items)
// Init VIDEO disable choises
items = []list.Item{
item{title: "FINISH"},
}
m.lists[DONE].SetItems(items)
}
func (m model) Init() tea.Cmd {
return nil
}
// This function processes the enter event
func (m *model) processSelection() {
switch m.focused {
case GPUS:
configs.InitConfigs()
// Gets the selected item
selectedItem := m.lists[m.focused].SelectedItem()
// Gets the IOMMU group of the selected item
iommu_group_regex := regexp.MustCompile(`(\d{1,3})`)
iommu_group := iommu_group_regex.FindString(selectedItem.(item).desc)
// Add the gpu group to our model
m.gpu_group = iommu_group
items := StringList2ListItem(GetIOMMU("-gr", "-i", m.gpu_group, "-F", "name,device_id,optional_revision"))
m.lists[GPU_GROUP].SetItems(items)
// Adjust height to correct for a bigger title
m.lists[GPU_GROUP].SetSize(m.width, m.height-1)
// Change focus to next index
m.focused++
case GPU_GROUP:
// Generate the VBIOS dumper script once the user has selected a GPU
GenerateVBIOSDumper(*m)
m.focused++
case USB:
// Gets the selected item
selectedItem := m.lists[m.focused].SelectedItem()
// Gets the IOMMU group of the selected item
iommu_group_regex := regexp.MustCompile(`(\d{1,3})`)
iommu_group := iommu_group_regex.FindString(selectedItem.(item).desc)
items := StringList2ListItem(GetIOMMU("-ur", "-i", iommu_group, "-F", "name,device_id,optional_revision"))
m.lists[USB_GROUP].SetItems(items)
// Adjust height to correct for a bigger title
m.lists[USB_GROUP].SetSize(m.width, m.height-1)
// Change focus to next index
m.focused++
case USB_GROUP:
m.focused++
case VBIOS:
m.focused++
case VIDEO:
m.focused++
case INTRO:
m.focused++
case DONE:
os.Exit(0)
}
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "enter":
if m.loaded {
m.processSelection()
}
case "ctrl+z", "backspace":
if m.focused > 0 {
m.focused--
return m, nil
} else {
return m, tea.Quit
}
}
case tea.WindowSizeMsg:
if !m.loaded {
// Get the terminal frame size
//h, v := docStyle.GetFrameSize()
// Initialize the static lists and make sure the content
// does not extend past the screen
m.initLists(msg.Width-2, msg.Height-2)
// Set model loaded to true
m.loaded = true
}
}
var cmd tea.Cmd
m.lists[m.focused], cmd = m.lists[m.focused].Update(msg)
return m, cmd
}
func (m model) View() string {
if m.loaded {
title := ""
switch m.focused {
case INTRO:
title = dialogStyle.Render(
fmt.Sprint(
titleStyle.Render("Welcome to QuickPassthrough!"),
"\n\n",
"This script is meant to make it easier to setup GPU passthrough for Qemu systems.\n",
"However due to the complexity of GPU passthrough, this script assumes you know how to do (or have done) the following.\n\n",
"* You have already enabled IOMMU, VT-d, SVM and/or AMD-v\n inside your UEFI/BIOS advanced settings.\n",
"* Know how to edit your bootloader\n",
"* Have a bootloader timeout of at least 3 seconds to access the menu\n",
"* Enable & Configure kernel modules\n",
"* Have a backup/snapshot of your system in case the script causes your\n system to be unbootable\n\n",
"By continuing you accept that I am not liable if your system\n",
"becomes unbootable, as you will be asked to verify the files generated",
),
)
case GPUS:
title = titleStyle.Render(
"Select a GPU to check the IOMMU groups of",
)
case GPU_GROUP:
title = titleStyle.Render(
fmt.Sprint(
"Press ENTER/RETURN to set up all these devices for passthrough.\n",
"This list should only contain items related to your GPU.",
),
)
case USB:
title = titleStyle.Render(
"[OPTIONAL]: Select a USB Controller to check the IOMMU groups of",
)
case USB_GROUP:
title = titleStyle.Render(
fmt.Sprint(
"Press ENTER/RETURN to set up all these devices for passthrough.\n",
"This list should only contain the USB controller you want to use.",
),
)
case VBIOS:
// Get the program directory
exe, _ := os.Executable()
scriptdir := filepath.Dir(exe)
// If we are using go run use the working directory instead
if strings.Contains(scriptdir, "/tmp/go-build") {
scriptdir, _ = os.Getwd()
}
text := dialogStyle.Render(
fmt.Sprint(
"Based on your GPU selection, a vbios extraction script has been generated for your convenience.\n",
"Passing a VBIOS rom to the card used for passthrough is required for some cards, but not all.\n",
"Some cards also requires you to patch your VBIOS romfile, check online if this is neccessary for your card!\n",
"The VBIOS will be read from:\n",
"%s\n\n",
"The script to extract the vbios has to be run as sudo and without a displaymanager running for proper dumping!\n",
"\n",
"You can run the script with:\n",
"%s/utils/dump_vbios.sh",
),
)
title = fmt.Sprintf(text, m.vbios_path, scriptdir)
case VIDEO:
title = dialogStyle.Render(
fmt.Sprint(
"Disabling video output in Linux for the card you want to use in a VM\n",
"will make it easier to successfully do the passthrough without issues.\n",
"\n",
"Do you want to force disable video output in linux on this card?",
),
)
case DONE:
title = dialogStyle.Render(
fmt.Sprint(
"The configuration files have been generated and are\n",
"located inside the \"config\" folder\n",
"\n",
"* The \"cmdline\" file contains kernel arguments that your bootloader needs\n",
"* The \"quickemu\" folder contains files that might be\n useable for quickemu in the future\n",
"* The files inside the \"etc\" folder must be copied to your system.\n NOTE: Verify that these files are correctly formated/edited!\n",
"\n",
"A script file named \"install.sh\" has been generated, run it to copy the files to your system and make a backup of your old files.",
),
)
}
//return listStyle.SetString(fmt.Sprintf("%s\n\n", title)).Render(m.lists[m.focused].View())
return lipgloss.JoinVertical(lipgloss.Left, fmt.Sprintf("%s\n%s\n", title, listStyle.Render(m.lists[m.focused].View())))
} else {
return "Loading..."
}
}
func NewModel() *model {
// Create a blank model and return it
return &model{}
}
// This is where we build everything
func App() {
// Make a blank model to keep our state in
m := NewModel()
// Start the program with the model
p := tea.NewProgram(m, tea.WithAltScreen())
_, err := p.Run()
errorcheck.ErrorCheck(err, "Failed to initialize UI")
}
func GetIOMMU(args ...string) []string {
var stdout, stderr bytes.Buffer
// Configure the ls-iommu command
cmd := exec.Command("utils/ls-iommu", args...)
cmd.Stderr = &stderr
cmd.Stdout = &stdout
// Execute the command
err := cmd.Run()
// If ls-iommu returns an error then IOMMU is disabled
errorcheck.ErrorCheck(err, "IOMMU disabled in either UEFI/BIOS or in bootloader!")
// Read the output
var items []string
output, _ := io.ReadAll(&stdout)
// Parse the output line by line
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
// Write the objects into the list
items = append(items, scanner.Text())
}
// Return our list of items
return items
}
func StringList2ListItem(stringList []string) []list.Item {
// Make the []list.Item struct
items := []list.Item{}
// Parse the output line by line
for _, v := range stringList {
// Get the current line and split by :
objects := strings.Split(v, ": ")
// Write the objects into the list
items = append(items, item{title: objects[1], desc: objects[0]})
}
// Return our list of items
return items
}
func GenerateVBIOSDumper(m model) {
// Get the vbios path
m.vbios_path = GetIOMMU("-g", "-i", m.gpu_group, "--rom")[0]
// Get the config directories
config := configs.GetConfigPaths()
// Get the program directory
exe, _ := os.Executable()
scriptdir := filepath.Dir(exe)
// If we are using go run use the working directory instead
if strings.Contains(scriptdir, "/tmp/go-build") {
scriptdir, _ = os.Getwd()
}
vbios_script_template := fmt.Sprint(
"#!/bin/bash\n",
"# THIS FILE IS AUTO GENERATED!\n",
"# IF YOU HAVE CHANGED GPU, PLEASE RE-RUN QUICKPASSTHROUGH!\n",
"echo 1 | sudo tee %s\n",
"sudo bash -c \"cat %s\" > %s/%s/vfio_card.rom\n",
"echo 0 | sudo tee %s\n",
)
vbios_script := fmt.Sprintf(
vbios_script_template,
m.vbios_path,
m.vbios_path,
scriptdir,
config.QUICKEMU,
m.vbios_path,
)
scriptfile, err := os.Create("utils/dump_vbios.sh")
errorcheck.ErrorCheck(err, "Cannot create file \"utils/dump_vbios.sh\"")
defer scriptfile.Close()
// Make the script executable
scriptfile.Chmod(0775)
errorcheck.ErrorCheck(err, "Could not change permissions of \"utils/dump_vbios.sh\"")
// Write the script
scriptfile.WriteString(vbios_script)
}

77
internal/ui_main.go Normal file
View file

@ -0,0 +1,77 @@
package internal
// A simple example demonstrating the use of multiple text input components
// from the Bubbles component library.
import (
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
tea "github.com/charmbracelet/bubbletea"
)
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
// Setup keybindings
switch msg.String() {
case "ctrl+c", "q":
// Exit when user presses Q or CTRL+C
return m, tea.Quit
case "enter":
if m.loaded {
// Process the selected item
m.processSelection()
}
case "ctrl+z", "backspace":
// Go backwards in the model
if m.focused > 0 {
m.focused--
return m, nil
} else {
// If we are at the beginning, just exit
return m, tea.Quit
}
}
case tea.WindowSizeMsg:
if !m.loaded {
// Initialize the static lists and make sure the content
// does not extend past the screen
m.initLists(msg.Width, msg.Height)
// Set model loaded to true
m.loaded = true
} else {
// Else we are loaded and will update the sizing on the fly
m.height = msg.Height
m.width = msg.Width
// TODO: Find a better way to resize widgets when word wrapping happens
// BUG: currently breaks the UI rendering if word wrapping happens in some cases...
views := len(m.lists)
if msg.Width > 83 {
for i := 0; i < views; i++ {
m.lists[i].SetSize(m.width-m.offsetx[i], m.height-m.offsety[i])
// Update the styles with the correct width
dialogStyle = dialogStyle.Width(m.width)
listStyle = listStyle.Width(m.width)
titleStyle = titleStyle.Width(m.width - 2)
choiceStyle = choiceStyle.Width(m.width)
}
}
}
}
m.lists[m.focused], cmd = m.lists[m.focused].Update(msg)
return m, cmd
}
// This is where we build everything
func Tui() {
// Make a blank model to keep our state in
m := NewModel()
// Start the program with the model
p := tea.NewProgram(m, tea.WithAltScreen())
_, err := p.Run()
errorcheck.ErrorCheck(err, "Failed to initialize UI")
}

View file

@ -0,0 +1,72 @@
package internal
import (
"os"
"regexp"
"github.com/HikariKnight/quickpassthrough/internal/configs"
)
// This function processes the enter event
func (m *model) processSelection() {
switch m.focused {
case GPUS:
configs.InitConfigs()
// Gets the selected item
selectedItem := m.lists[m.focused].SelectedItem()
// Gets the IOMMU group of the selected item
iommu_group_regex := regexp.MustCompile(`(\d{1,3})`)
iommu_group := iommu_group_regex.FindString(selectedItem.(item).desc)
// Add the gpu group to our model (this is so we can grab the vbios details later)
m.gpu_group = iommu_group
// Get all the gpu devices and related devices (same device id or in the same group)
items := iommuList2ListItem(getIOMMU("-grr", "-i", m.gpu_group, "-F", "vendor:,prod_name,optional_revision:,device_id"))
// Add the devices to the list
m.lists[GPU_GROUP].SetItems(items)
// Change focus to next index
m.focused++
case GPU_GROUP:
// Generate the VBIOS dumper script once the user has selected a GPU
generateVBIOSDumper(*m)
m.focused++
case USB:
// Gets the selected item
selectedItem := m.lists[m.focused].SelectedItem()
// Gets the IOMMU group of the selected item
iommu_group_regex := regexp.MustCompile(`(\d{1,3})`)
iommu_group := iommu_group_regex.FindString(selectedItem.(item).desc)
// Get the USB controllers in the selected iommu group
items := iommuList2ListItem(getIOMMU("-ur", "-i", iommu_group, "-F", "vendor:,prod_name,optional_revision:,device_id"))
// Add the items to the list
m.lists[USB_GROUP].SetItems(items)
// Change focus to next index
m.focused++
case USB_GROUP:
m.focused++
case VBIOS:
m.focused++
case VIDEO:
m.focused++
case INTRO:
m.focused++
case DONE:
os.Exit(0)
}
}

117
internal/ui_main_view.go Normal file
View file

@ -0,0 +1,117 @@
package internal
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/charmbracelet/lipgloss"
)
func (m model) View() string {
if m.loaded {
title := ""
switch m.focused {
case INTRO:
title = dialogStyle.Render(
fmt.Sprint(
titleStyle.MarginLeft(0).Render("Welcome to QuickPassthrough!"),
"\n\n",
"This script is meant to make it easier to setup GPU passthrough for\n",
"Qemu based systems.\n",
"However due to the complexity of GPU passthrough\n",
"This script assumes you know how to do (or have done) the following.\n\n",
"* You have already enabled IOMMU, VT-d, SVM and/or AMD-v\n inside your UEFI/BIOS advanced settings.\n",
"* Know how to edit your bootloader\n",
"* Have a bootloader timeout of at least 3 seconds to access the menu\n",
"* Enable & Configure kernel modules\n",
"* Have a backup/snapshot of your system in case the script causes your\n system to be unbootable\n\n",
"By continuing you accept that I am not liable if your system\n",
"becomes unbootable, as you will be asked to verify the files generated",
),
)
case GPUS:
title = titleStyle.MarginLeft(2).Render(
"Select a GPU to check the IOMMU groups of",
)
case GPU_GROUP:
title = titleStyle.Render(
fmt.Sprint(
"Press ENTER/RETURN to set up all these devices for passthrough.\n",
"This list should only contain items related to your GPU.",
),
)
case USB:
title = titleStyle.Render(
"[OPTIONAL]: Select a USB Controller to check the IOMMU groups of",
)
case USB_GROUP:
title = titleStyle.Render(
fmt.Sprint(
"Press ENTER/RETURN to set up all these devices for passthrough.\n",
"This list should only contain the USB controller you want to use.",
),
)
case VBIOS:
// Get the program directory
exe, _ := os.Executable()
scriptdir := filepath.Dir(exe)
// If we are using go run use the working directory instead
if strings.Contains(scriptdir, "/tmp/go-build") {
scriptdir, _ = os.Getwd()
}
text := dialogStyle.Render(
fmt.Sprint(
"Based on your GPU selection, a vbios extraction script has been generated for your convenience.\n",
"Passing a VBIOS rom to the card used for passthrough is required for some cards, but not all.\n",
"Some cards also requires you to patch your VBIOS romfile, check online if this is neccessary for your card!\n",
"The VBIOS will be read from:\n",
"%s\n\n",
"The script to extract the vbios has to be run as sudo and without a displaymanager running for proper dumping!\n",
"\n",
"You can run the script with:\n",
"%s/utils/dump_vbios.sh",
),
)
title = fmt.Sprintf(text, m.vbios_path, scriptdir)
case VIDEO:
title = dialogStyle.Render(
fmt.Sprint(
"Disabling video output in Linux for the card you want to use in a VM\n",
"will make it easier to successfully do the passthrough without issues.\n",
"\n",
"Do you want to force disable video output in linux on this card?",
),
)
case DONE:
title = dialogStyle.Render(
fmt.Sprint(
"The configuration files have been generated and are\n",
"located inside the \"config\" folder\n",
"\n",
"* The \"cmdline\" file contains kernel arguments that your bootloader needs\n",
"* The \"quickemu\" folder contains files that might be\n useable for quickemu in the future\n",
"* The files inside the \"etc\" folder must be copied to your system.\n",
" NOTE: Verify that these files are correctly formated/edited!\n",
"\n",
"A script file named \"install.sh\" has been generated,\n",
"run it to copy the files to your system and make a backup of your old files.",
),
)
}
//return listStyle.SetString(fmt.Sprintf("%s\n\n", title)).Render(m.lists[m.focused].View())
return lipgloss.JoinVertical(lipgloss.Left, fmt.Sprintf("%s\n%s\n", title, listStyle.Render(m.lists[m.focused].View())))
} else {
return "Loading..."
}
}

154
internal/ui_model.go Normal file
View file

@ -0,0 +1,154 @@
package internal
import (
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
)
// Make a status type
type status int
// List item struct
type item struct {
title, desc string
}
// Functions needed for item struct
func (i item) Title() string { return i.title }
func (i item) Description() string { return i.desc }
func (i item) FilterValue() string { return i.title }
// Main Model
type model struct {
fetched []bool
lists []list.Model
gpu_group string
vbios_path string
loaded bool
focused status
offsetx []int
offsety []int
width int
height int
}
// Consts used to navigate the main model
const (
INTRO status = iota
GPUS
GPU_GROUP
VBIOS
VIDEO
USB
USB_GROUP
DONE
)
func NewModel() *model {
// Create a blank model and return it
return &model{}
}
func (m model) Init() tea.Cmd {
return nil
}
func (m *model) initLists(width, height int) {
defaultList := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 10)
choiceList := list.New([]list.Item{}, choiceDelegate{}, 0, 7)
// Disable features we wont need
defaultList.SetShowTitle(false)
defaultList.SetFilteringEnabled(false)
defaultList.SetSize(m.width, m.height)
choiceList.SetShowTitle(false)
choiceList.SetFilteringEnabled(false)
// Add height and width to our model so we can use it later
m.width = width
m.height = height
m.lists = []list.Model{
choiceList,
defaultList,
defaultList,
choiceList,
choiceList,
defaultList,
defaultList,
choiceList,
}
// Configure offsets for sizing
m.offsetx = []int{
0, 0, 0, 0, 0, 0, 0, 0,
}
m.offsety = []int{
18, 2, 3, 13, 5, 2, 3, 12,
}
// Update the styles with the correct width
dialogStyle = dialogStyle.Width(m.width)
listStyle = listStyle.Width(m.width)
titleStyle = titleStyle.Width(m.width - 2)
choiceStyle = choiceStyle.Width(m.width)
// Make m.fetched and set all values to FALSE
m.fetched = []bool{}
for range m.lists {
m.fetched = append(m.fetched, false)
}
// Set INTRO to the focused view
m.focused = INTRO
// Init INTRO choices
items := []list.Item{
item{title: "CONTINUE"},
}
//m.lists[INTRO].SetHeight(5)
m.lists[INTRO].SetItems(items)
m.lists[INTRO].SetSize(m.width-m.offsetx[INTRO], m.height-m.offsety[INTRO])
// Init GPU list
items = iommuList2ListItem(getIOMMU("-g", "-F", "vendor:,prod_name,optional_revision:,device_id"))
m.lists[GPUS].SetItems(items)
m.lists[GPUS].SetSize(m.width-m.offsetx[GPUS], m.height-m.offsety[GPUS])
m.fetched[GPUS] = true
// Setup the initial GPU_GROUP list
// The content in this list is generated from the selected choice from the GPU view
m.lists[GPU_GROUP].SetSize(m.width-m.offsetx[GPU_GROUP], m.height-m.offsety[GPU_GROUP])
// Init USB Controller list
items = iommuList2ListItem(getIOMMU("-u", "-F", "vendor:,prod_name,optional_revision:,device_id"))
m.lists[USB].SetItems(items)
m.lists[USB].SetSize(m.width-m.offsetx[USB], m.height-m.offsety[USB])
m.fetched[USB] = true
// Setup the initial USB_GROUP list
// The content in this list is generated from the selected choice from the USB view
m.lists[USB_GROUP].SetSize(m.width-m.offsetx[USB_GROUP], m.height-m.offsety[USB_GROUP])
// Init VBIOS choices
items = []list.Item{
item{title: "OK"},
}
m.lists[VBIOS].SetItems(items)
m.lists[VBIOS].SetSize(m.width-m.offsetx[VBIOS], m.height-m.offsety[VBIOS])
// Init VIDEO disable choises
items = []list.Item{
item{title: "YES"},
item{title: "NO"},
}
m.lists[VIDEO].SetItems(items)
m.lists[VIDEO].SetSize(m.width-m.offsetx[VIDEO], m.height-m.offsety[VIDEO])
// Init DONE choises
items = []list.Item{
item{title: "FINISH"},
}
m.lists[DONE].SetItems(items)
m.lists[DONE].SetSize(m.width-m.offsetx[DONE], m.height-m.offsety[DONE])
}

55
internal/ui_style.go Normal file
View file

@ -0,0 +1,55 @@
package internal
import (
"fmt"
"io"
"strings"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
titleStyle = lipgloss.NewStyle().
Background(lipgloss.Color("#5F5FD7")).
Foreground(lipgloss.Color("#FFFFFF")).
PaddingLeft(2).PaddingRight(2)
helpStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color(241))
listStyle = lipgloss.NewStyle().
PaddingLeft(2).
PaddingRight(2)
choiceStyle = lipgloss.NewStyle().
PaddingLeft(4).
PaddingRight(4)
selectedChoiceStyle = lipgloss.NewStyle().
PaddingLeft(2).
Foreground(lipgloss.Color("170"))
dialogStyle = lipgloss.NewStyle().
PaddingLeft(2)
)
// Choice delegate (for our dialog boxes)
type choiceDelegate struct{}
func (d choiceDelegate) Height() int { return 1 }
func (d choiceDelegate) Spacing() int { return 0 }
func (d choiceDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
func (d choiceDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
i, ok := listItem.(item)
if !ok {
return
}
str := i.title
fn := choiceStyle.Render
if index == m.Index() {
fn = func(s ...string) string {
return selectedChoiceStyle.Render("| " + strings.Join(s, " "))
}
}
fmt.Fprint(w, fn(str))
}

View file

@ -0,0 +1,61 @@
package internal
import (
"bufio"
"bytes"
"fmt"
"io"
"os/exec"
"regexp"
"strings"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/charmbracelet/bubbles/list"
)
func getIOMMU(args ...string) []string {
var stdout, stderr bytes.Buffer
// Configure the ls-iommu command
cmd := exec.Command("utils/ls-iommu", args...)
cmd.Stderr = &stderr
cmd.Stdout = &stdout
// Execute the command
err := cmd.Run()
// If ls-iommu returns an error then IOMMU is disabled
errorcheck.ErrorCheck(err, "IOMMU disabled in either UEFI/BIOS or in bootloader!")
// Read the output
var items []string
output, _ := io.ReadAll(&stdout)
// Parse the output line by line
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
// Write the objects into the list
items = append(items, scanner.Text())
}
// Return our list of items
return items
}
func iommuList2ListItem(stringList []string) []list.Item {
// Make the []list.Item struct
items := []list.Item{}
deviceID := regexp.MustCompile(`\[[a-f0-9]{4}:[a-f0-9]{4}\]\s+`)
// Parse the output line by line
for _, v := range stringList {
// Get the current line and split by :
objects := strings.Split(v, ": ")
// Write the objects into the list
items = append(items, item{title: deviceID.ReplaceAllString(objects[2], ""), desc: fmt.Sprintf("%s: %s: DeviceID: %s", objects[0], objects[1], objects[3])})
}
// Return our list of items
return items
}

View file

@ -3,6 +3,7 @@ package untar
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
@ -10,13 +11,13 @@ import (
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
)
// Source: https://medium.com/@skdomino/taring-untaring-files-in-go-6b07cf56bc07
// Slightly modified from source: https://medium.com/@skdomino/taring-untaring-files-in-go-6b07cf56bc07
// Untar takes a destination path and a reader; a tar reader loops over the tarfile
// Untar takes a destination path and a path to a file; a tar reader loops over the tarfile
// creating the file structure at 'dst' along the way, and writing any files
func Untar(dst string, fileName string) error {
r, err := os.Open(fileName)
errorcheck.ErrorCheck(err)
errorcheck.ErrorCheck(err, fmt.Sprintf("Failed to open: %s", fileName))
defer r.Close()
gzr, err := gzip.NewReader(r)