mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-14 17:52:10 -04:00
* Add support for headless mode. Allow dankinstall run with command-line flags. * Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056146219 * Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056146253 * Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056146271 * Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056146296 * Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056146348 * FIx https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056146328 * Update headless mode instructions * Add log dir config. Use DANKINSTALL_LOG env var, fallback to /var/tmp * Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056737552 * Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056737572 * Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056737592 * Add explanations for headless validating rules and log file location * Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3058087146 and https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3058087234 * Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3058087271 * Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3058310408 * Enhance configuration deployment logic to support missing files and add corresponding unit tests * Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3058310495 * Reworked the log channel handling logic to simplify the code and added the `drainLogChan` function to prevent blocking (https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3058609491) * Added dependency-checking functionality to ensure installation requirements are met, and optimized the pre-installation logic for AUR packages * feat: output log messages to stdout during installation * Revert dependency-checking functionality due to official fix * Revert compositor provider workaround due to upstream fix
220 lines
6.3 KiB
Go
220 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/headless"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var Version = "dev"
|
|
|
|
// Flag variables bound via pflag
|
|
var (
|
|
compositor string
|
|
term string
|
|
includeDeps []string
|
|
excludeDeps []string
|
|
replaceConfigs []string
|
|
replaceConfigsAll bool
|
|
yes bool
|
|
)
|
|
|
|
var rootCmd = &cobra.Command{
|
|
Use: "dankinstall",
|
|
Short: "Install DankMaterialShell and its dependencies",
|
|
Long: `dankinstall sets up DankMaterialShell with your chosen compositor and terminal.
|
|
|
|
Without flags, it launches an interactive TUI. Providing either --compositor
|
|
or --term activates headless (unattended) mode, which requires both flags.
|
|
|
|
Headless mode requires cached sudo credentials. Run 'sudo -v' beforehand, or
|
|
configure passwordless sudo for your user.`,
|
|
Args: cobra.NoArgs,
|
|
RunE: runDankinstall,
|
|
SilenceErrors: true,
|
|
SilenceUsage: true,
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri or hyprland (enables headless mode)")
|
|
rootCmd.Flags().StringVarP(&term, "term", "t", "", "Terminal emulator to install: ghostty, kitty, or alacritty (enables headless mode)")
|
|
rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter)")
|
|
rootCmd.Flags().StringSliceVar(&excludeDeps, "exclude-deps", []string{}, "Deps to skip during installation")
|
|
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
|
|
rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
|
|
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
|
|
}
|
|
|
|
func main() {
|
|
if os.Getuid() == 0 {
|
|
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err := rootCmd.Execute(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runDankinstall(cmd *cobra.Command, args []string) error {
|
|
headlessMode := compositor != "" || term != ""
|
|
|
|
if !headlessMode {
|
|
// Reject headless-only flags when running in TUI mode.
|
|
headlessOnly := []string{
|
|
"include-deps",
|
|
"exclude-deps",
|
|
"replace-configs",
|
|
"replace-configs-all",
|
|
"yes",
|
|
}
|
|
var set []string
|
|
for _, name := range headlessOnly {
|
|
if cmd.Flags().Changed(name) {
|
|
set = append(set, "--"+name)
|
|
}
|
|
}
|
|
if len(set) > 0 {
|
|
return fmt.Errorf("flags %s are only valid in headless mode (requires both --compositor and --term)", strings.Join(set, ", "))
|
|
}
|
|
}
|
|
|
|
if headlessMode {
|
|
return runHeadless()
|
|
}
|
|
return runTUI()
|
|
}
|
|
|
|
func runHeadless() error {
|
|
// Validate required flags
|
|
if compositor == "" {
|
|
return fmt.Errorf("--compositor is required for headless mode (niri or hyprland)")
|
|
}
|
|
if term == "" {
|
|
return fmt.Errorf("--term is required for headless mode (ghostty, kitty, or alacritty)")
|
|
}
|
|
|
|
cfg := headless.Config{
|
|
Compositor: compositor,
|
|
Terminal: term,
|
|
IncludeDeps: includeDeps,
|
|
ExcludeDeps: excludeDeps,
|
|
ReplaceConfigs: replaceConfigs,
|
|
ReplaceConfigsAll: replaceConfigsAll,
|
|
Yes: yes,
|
|
}
|
|
|
|
runner := headless.NewRunner(cfg)
|
|
|
|
// Set up file logging
|
|
fileLogger, err := log.NewFileLogger()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: Failed to create log file: %v\n", err)
|
|
}
|
|
|
|
if fileLogger != nil {
|
|
fmt.Printf("Logging to: %s\n", fileLogger.GetLogPath())
|
|
fileLogger.StartListening(runner.GetLogChan())
|
|
defer func() {
|
|
if err := fileLogger.Close(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: Failed to close log file: %v\n", err)
|
|
}
|
|
}()
|
|
} else {
|
|
// Drain the log channel to prevent blocking sends from deadlocking
|
|
// downstream components (distros, config deployer) that write to it.
|
|
// Use an explicit stop signal because this code does not own the
|
|
// runner log channel and cannot assume it will be closed.
|
|
defer drainLogChan(runner.GetLogChan())()
|
|
}
|
|
|
|
if err := runner.Run(); err != nil {
|
|
if fileLogger != nil {
|
|
fmt.Fprintf(os.Stderr, "\nFull logs are available at: %s\n", fileLogger.GetLogPath())
|
|
}
|
|
return err
|
|
}
|
|
|
|
if fileLogger != nil {
|
|
fmt.Printf("\nFull logs are available at: %s\n", fileLogger.GetLogPath())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runTUI() error {
|
|
fileLogger, err := log.NewFileLogger()
|
|
if err != nil {
|
|
fmt.Printf("Warning: Failed to create log file: %v\n", err)
|
|
fmt.Println("Continuing without file logging...")
|
|
}
|
|
|
|
logFilePath := ""
|
|
if fileLogger != nil {
|
|
logFilePath = fileLogger.GetLogPath()
|
|
fmt.Printf("Logging to: %s\n", logFilePath)
|
|
defer func() {
|
|
if err := fileLogger.Close(); err != nil {
|
|
fmt.Printf("Warning: Failed to close log file: %v\n", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
model := tui.NewModel(Version, logFilePath)
|
|
|
|
if fileLogger != nil {
|
|
fileLogger.StartListening(model.GetLogChan())
|
|
} else {
|
|
// Drain the log channel to prevent blocking sends from deadlocking
|
|
// downstream components (distros, config deployer) that write to it.
|
|
// Use an explicit stop signal because this code does not own the
|
|
// model log channel and cannot assume it will be closed.
|
|
defer drainLogChan(model.GetLogChan())()
|
|
}
|
|
|
|
p := tea.NewProgram(model, tea.WithAltScreen())
|
|
if _, err := p.Run(); err != nil {
|
|
if logFilePath != "" {
|
|
fmt.Fprintf(os.Stderr, "\nFull logs are available at: %s\n", logFilePath)
|
|
}
|
|
return fmt.Errorf("error running program: %w", err)
|
|
}
|
|
|
|
if logFilePath != "" {
|
|
fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// drainLogChan starts a goroutine that discards all messages from logCh,
|
|
// preventing blocking sends from deadlocking downstream components. It returns
|
|
// a cleanup function that signals the goroutine to stop and waits for it to
|
|
// exit. Callers should defer the returned function.
|
|
func drainLogChan(logCh <-chan string) func() {
|
|
drainStop := make(chan struct{})
|
|
drainDone := make(chan struct{})
|
|
go func() {
|
|
defer close(drainDone)
|
|
for {
|
|
select {
|
|
case <-drainStop:
|
|
return
|
|
case _, ok := <-logCh:
|
|
if !ok {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return func() {
|
|
close(drainStop)
|
|
<-drainDone
|
|
}
|
|
}
|