From 123d1ca18ae077cec6b75ca949b70ef9888039d5 Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Sat, 8 Apr 2023 19:38:25 +0200 Subject: [PATCH] Refactor and split tui into smaller files --- cmd/main.go | 8 +- internal/gen_vbios_dumper.go | 57 ++ .../ls_iommu_downloader.go | 4 +- {pkg => internal}/params/params.go | 0 internal/tuimode/tuimode.go | 499 ------------------ internal/ui_main.go | 77 +++ internal/ui_main_functions.go | 72 +++ internal/ui_main_view.go | 117 ++++ internal/ui_model.go | 154 ++++++ internal/ui_style.go | 55 ++ internal/utiil_ls-iommu.go | 61 +++ {internal => pkg}/untar/untar.go | 7 +- 12 files changed, 603 insertions(+), 508 deletions(-) create mode 100644 internal/gen_vbios_dumper.go rename {pkg => internal}/params/params.go (100%) delete mode 100644 internal/tuimode/tuimode.go create mode 100644 internal/ui_main.go create mode 100644 internal/ui_main_functions.go create mode 100644 internal/ui_main_view.go create mode 100644 internal/ui_model.go create mode 100644 internal/ui_style.go create mode 100644 internal/utiil_ls-iommu.go rename {internal => pkg}/untar/untar.go (85%) diff --git a/cmd/main.go b/cmd/main.go index b51b847..5973bf6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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() } } diff --git a/internal/gen_vbios_dumper.go b/internal/gen_vbios_dumper.go new file mode 100644 index 0000000..942793d --- /dev/null +++ b/internal/gen_vbios_dumper.go @@ -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) +} diff --git a/internal/ls_iommu_downloader/ls_iommu_downloader.go b/internal/ls_iommu_downloader/ls_iommu_downloader.go index 0ed0042..8fd5032 100644 --- a/internal/ls_iommu_downloader/ls_iommu_downloader.go +++ b/internal/ls_iommu_downloader/ls_iommu_downloader.go @@ -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) diff --git a/pkg/params/params.go b/internal/params/params.go similarity index 100% rename from pkg/params/params.go rename to internal/params/params.go diff --git a/internal/tuimode/tuimode.go b/internal/tuimode/tuimode.go deleted file mode 100644 index 8e7bb30..0000000 --- a/internal/tuimode/tuimode.go +++ /dev/null @@ -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) -} diff --git a/internal/ui_main.go b/internal/ui_main.go new file mode 100644 index 0000000..13fbdc3 --- /dev/null +++ b/internal/ui_main.go @@ -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") +} diff --git a/internal/ui_main_functions.go b/internal/ui_main_functions.go new file mode 100644 index 0000000..b851f3b --- /dev/null +++ b/internal/ui_main_functions.go @@ -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) + } +} diff --git a/internal/ui_main_view.go b/internal/ui_main_view.go new file mode 100644 index 0000000..53de41a --- /dev/null +++ b/internal/ui_main_view.go @@ -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..." + } +} diff --git a/internal/ui_model.go b/internal/ui_model.go new file mode 100644 index 0000000..bbe3549 --- /dev/null +++ b/internal/ui_model.go @@ -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]) +} diff --git a/internal/ui_style.go b/internal/ui_style.go new file mode 100644 index 0000000..d6784af --- /dev/null +++ b/internal/ui_style.go @@ -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)) +} diff --git a/internal/utiil_ls-iommu.go b/internal/utiil_ls-iommu.go new file mode 100644 index 0000000..11259fa --- /dev/null +++ b/internal/utiil_ls-iommu.go @@ -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 +} diff --git a/internal/untar/untar.go b/pkg/untar/untar.go similarity index 85% rename from internal/untar/untar.go rename to pkg/untar/untar.go index ee605b0..96ab454 100644 --- a/internal/untar/untar.go +++ b/pkg/untar/untar.go @@ -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)