// Package config handles all the user-configuration. The fields here are
// all in PascalCase but in your actual config.yml they'll be in camelCase.
// You can view the default config with `lazydocker --config`.
// You can open your config file by going to the status panel (using left-arrow)
// and pressing 'o'.
// You can directly edit the file (e.g. in vim) by pressing 'e' instead.
// To see the final config after your user-specific options have been merged
// with the defaults, go to the 'about' tab in the status panel.
// Because of the way we merge your user config with the defaults you may need
// to be careful: if for example you set a `commandTemplates:` yaml key but then
// give it no child values, it will scrap all of the defaults and the app will
// probably crash.
package config
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/OpenPeeDeeP/xdg"
"github.com/jesseduffield/yaml"
)
// UserConfig holds all of the user-configurable options
type UserConfig struct {
// Gui is for configuring visual things like colors and whether we show or
// hide things
Gui GuiConfig ` yaml:"gui,omitempty" `
// ConfirmOnQuit when enabled prompts you to confirm you want to quit when you
// hit esc or q when no confirmation panels are open
ConfirmOnQuit bool ` yaml:"confirmOnQuit,omitempty" `
// Logs determines how we render/filter a container's logs
Logs LogsConfig ` yaml:"logs,omitempty" `
// CommandTemplates determines what commands actually get called when we run
// certain commands
CommandTemplates CommandTemplatesConfig ` yaml:"commandTemplates,omitempty" `
// CustomCommands determines what shows up in your custom commands menu when
// you press 'c'. You can use go templates to access three items on the
// struct: the DockerCompose command (defaulted to 'docker-compose'), the
// Service if present, and the Container if present. The struct types for
// those are found in the commands package
CustomCommands CustomCommands ` yaml:"customCommands,omitempty" `
// BulkCommands are commands that apply to all items in a panel e.g.
// killing all containers, stopping all services, or pruning all images
BulkCommands CustomCommands ` yaml:"bulkCommands,omitempty" `
// OS determines what defaults are set for opening files and links
OS OSConfig ` yaml:"oS,omitempty" `
// Stats determines how long lazydocker will gather container stats for, and
// what stat info to graph
Stats StatsConfig ` yaml:"stats,omitempty" `
// Replacements determines how we render an item's info
Replacements Replacements ` yaml:"replacements,omitempty" `
// For demo purposes: any list item with one of these strings as a substring
// will be filtered out and not displayed.
// Not documented because it's subject to change
Ignore [ ] string ` yaml:"ignore,omitempty" `
}
// ThemeConfig is for setting the colors of panels and some text.
type ThemeConfig struct {
ActiveBorderColor [ ] string ` yaml:"activeBorderColor,omitempty" `
InactiveBorderColor [ ] string ` yaml:"inactiveBorderColor,omitempty" `
OptionsTextColor [ ] string ` yaml:"optionsTextColor,omitempty" `
}
// GuiConfig is for configuring visual things like colors and whether we show or
// hide things
type GuiConfig struct {
// ScrollHeight determines how many characters you scroll at a time when
// scrolling the main panel
ScrollHeight int ` yaml:"scrollHeight,omitempty" `
// Language determines which language the GUI displayed.
Language string ` yaml:"language,omitempty" `
// ScrollPastBottom determines whether you can scroll past the bottom of the
// main view
ScrollPastBottom bool ` yaml:"scrollPastBottom,omitempty" `
// IgnoreMouseEvents is for when you do not want to use your mouse to interact
// with anything
IgnoreMouseEvents bool ` yaml:"mouseEvents,omitempty" `
// Theme determines what colors and color attributes your panel borders have.
// I always set inactiveBorderColor to black because in my terminal it's more
// of a grey, but that doesn't work in your average terminal. I highly
// recommended finding a combination that works for you
Theme ThemeConfig ` yaml:"theme,omitempty" `
// ShowAllContainers determines whether the Containers panel contains all the
// containers returned by `docker ps -a`, or just those containers that aren't
// directly linked to a service. It is probably desirable to enable this if
// you have multiple containers per service, but otherwise it can cause a lot
// of clutter
ShowAllContainers bool ` yaml:"showAllContainers,omitempty" `
// ReturnImmediately determines whether you get the 'press enter to return to
// lazydocker' message after a subprocess has completed. You would set this to
// true if you often want to see the output of subprocesses before returning
// to lazydocker. I would default this to false but then people who want it
// set to true won't even know the config option exists.
ReturnImmediately bool ` yaml:"returnImmediately,omitempty" `
// WrapMainPanel determines whether we use word wrap on the main panel
WrapMainPanel bool ` yaml:"wrapMainPanel,omitempty" `
// LegacySortContainers determines if containers should be sorted using legacy approach.
// By default, containers are now sorted by status. This setting allows users to
// use legacy behaviour instead.
LegacySortContainers bool ` yaml:"legacySortContainers,omitempty" `
// If 0.333, then the side panels will be 1/3 of the screen's width
SidePanelWidth float64 ` yaml:"sidePanelWidth" `
// Determines whether we show the bottom line (the one containing keybinding
// info and the status of the app).
ShowBottomLine bool ` yaml:"showBottomLine" `
// When true, increases vertical space used by focused side panel,
// creating an accordion effect
ExpandFocusedSidePanel bool ` yaml:"expandFocusedSidePanel" `
}
// CommandTemplatesConfig determines what commands actually get called when we
// run certain commands
type CommandTemplatesConfig struct {
// RestartService is for restarting a service. docker-compose restart {{
// .Service.Name }} works but I prefer docker-compose up --force-recreate {{
// .Service.Name }}
RestartService string ` yaml:"restartService,omitempty" `
// StartService is just like the above but for starting
StartService string ` yaml:"startService,omitempty" `
// UpService ups the service (creates and starts)
UpService string ` yaml:"upService,omitempty" `
// Runs "docker-compose up -d"
Up string ` yaml:"up,omitempty" `
// downs everything
Down string ` yaml:"down,omitempty" `
// downs and removes volumes
DownWithVolumes string ` yaml:"downWithVolumes,omitempty" `
// DockerCompose is for your docker-compose command. You may want to combine a
// few different docker-compose.yml files together, in which case you can set
// this to "docker-compose -f foo/docker-compose.yml -f
// bah/docker-compose.yml". The reason that the other docker-compose command
// templates all start with {{ .DockerCompose }} is so that they can make use
// of whatever you've set in this value rather than you having to copy and
// paste it to all the other commands
DockerCompose string ` yaml:"dockerCompose,omitempty" `
// StopService is the command for stopping a service
StopService string ` yaml:"stopService,omitempty" `
// ServiceLogs get the logs for a service. This is actually not currently
// used; we just get the logs of the corresponding container. But we should
// probably support explicitly returning the logs of the service when you've
// selected the service, given that a service may have multiple containers.
ServiceLogs string ` yaml:"serviceLogs,omitempty" `
// ViewServiceLogs is for when you want to view the logs of a service as a
// subprocess. This defaults to having no filter, unlike the in-app logs
// commands which will usually filter down to the last hour for the sake of
// performance.
ViewServiceLogs string ` yaml:"viewServiceLogs,omitempty" `
// RebuildService is the command for rebuilding a service. Defaults to
// something along the lines of `{{ .DockerCompose }} up --build {{
// .Service.Name }}`
RebuildService string ` yaml:"rebuildService,omitempty" `
// RecreateService is for force-recreating a service. I prefer this to
// restarting a service because it will also restart any dependent services
// and ensure they're running before trying to run the service at hand
RecreateService string ` yaml:"recreateService,omitempty" `
// AllLogs is for showing what you get from doing `docker-compose logs`. It
// combines all the logs together
AllLogs string ` yaml:"allLogs,omitempty" `
// ViewAllLogs is the command we use when you want to see all logs in a subprocess with no filtering
ViewAllLogs string ` yaml:"viewAlLogs,omitempty" `
// DockerComposeConfig is the command for viewing the config of your docker
// compose. It basically prints out the yaml from your docker-compose.yml
// file(s)
DockerComposeConfig string ` yaml:"dockerComposeConfig,omitempty" `
// CheckDockerComposeConfig is what we use to check whether we are in a
// docker-compose context. If the command returns an error then we clearly
// aren't in a docker-compose config and we then just hide the services panel
// and only show containers
CheckDockerComposeConfig string ` yaml:"checkDockerComposeConfig,omitempty" `
// ServiceTop is the command for viewing the processes under a given service
ServiceTop string ` yaml:"serviceTop,omitempty" `
}
// OSConfig contains config on the level of the os
type OSConfig struct {
// OpenCommand is the command for opening a file
OpenCommand string ` yaml:"openCommand,omitempty" `
// OpenCommand is the command for opening a link
OpenLinkCommand string ` yaml:"openLinkCommand,omitempty" `
}
// GraphConfig specifies how to make a graph of recorded container stats
type GraphConfig struct {
// Min sets the minimum value that you want to display. If you want to set
// this, you should also set MinType to "static". The reason for this is that
// if Min == 0, it's not clear if it has not been set (given that the
// zero-value of an int is 0) or if it's intentionally been set to 0.
Min float64 ` yaml:"min,omitempty" `
// Max sets the maximum value that you want to display. If you want to set
// this, you should also set MaxType to "static". The reason for this is that
// if Max == 0, it's not clear if it has not been set (given that the
// zero-value of an int is 0) or if it's intentionally been set to 0.
Max float64 ` yaml:"max,omitempty" `
// Height sets the height of the graph in ascii characters
Height int ` yaml:"height,omitempty" `
// Caption sets the caption of the graph. If you want to show CPU Percentage
// you could set this to "CPU (%)"
Caption string ` yaml:"caption,omitempty" `
// This is the path to the stat that you want to display. It is based on the
// RecordedStats struct in container_stats.go, so feel free to look there to
// see all the options available. Alternatively if you go into lazydocker and
// go to the stats tab, you'll see that same struct in JSON format, so you can
// just PascalCase the path and you'll have a valid path. E.g.
// ClientStats.blkio_stats -> "ClientStats.BlkioStats"
StatPath string ` yaml:"statPath,omitempty" `
// This determines the color of the graph. This can be any color attribute,
// e.g. 'blue', 'green'
Color string ` yaml:"color,omitempty" `
// MinType and MaxType are each one of "", "static". blank means the min/max
// of the data set will be used. "static" means the min/max specified will be
// used
MinType string ` yaml:"minType,omitempty" `
// MaxType is just like MinType but for the max value
MaxType string ` yaml:"maxType,omitempty" `
}
// StatsConfig contains the stuff relating to stats and graphs
type StatsConfig struct {
// Graphs contains the configuration for the stats graphs we want to show in
// the app
Graphs [ ] GraphConfig
// MaxDuration tells us how long to collect stats for. Currently this defaults
// to "5m" i.e. 5 minutes.
MaxDuration time . Duration ` yaml:"maxDuration,omitempty" `
}
// CustomCommands contains the custom commands that you might want to use on any
// given service or container
type CustomCommands struct {
// Containers contains the custom commands for containers
Containers [ ] CustomCommand ` yaml:"containers,omitempty" `
// Services contains the custom commands for services
Services [ ] CustomCommand ` yaml:"services,omitempty" `
// Images contains the custom commands for images
Images [ ] CustomCommand ` yaml:"images,omitempty" `
// Volumes contains the custom commands for volumes
Volumes [ ] CustomCommand ` yaml:"volumes,omitempty" `
// Networks contains the custom commands for networks
Networks [ ] CustomCommand ` yaml:"networks,omitempty" `
}
// Replacements contains the stuff relating to rendering a container's info
type Replacements struct {
// ImageNamePrefixes tells us how to replace a prefix in the Docker image name
ImageNamePrefixes map [ string ] string ` yaml:"imageNamePrefixes,omitempty" `
}
// CustomCommand is a template for a command we want to run against a service or
// container
type CustomCommand struct {
// Name is the name of the command, purely for visual display
Name string ` yaml:"name" `
// Attach tells us whether to switch to a subprocess to interact with the
// called program, or just read its output. If Attach is set to false, the
// command will run in the background. I'm open to the idea of having a third
// option where the output plays in the main panel.
Attach bool ` yaml:"attach" `
// Shell indicates whether to invoke the Command on a shell or not.
// Example of a bash invoked command: `/bin/bash -c "{Command}".
Shell bool ` yaml:"shell" `
// Command is the command we want to run. We can use the go templates here as
// well. One example might be `{{ .DockerCompose }} exec {{ .Service.Name }}
// /bin/sh`
Command string ` yaml:"command" `
// ServiceNames is used to restrict this command to just one or more services.
// An example might be 'rails migrate' for your rails api service(s). This
// field has no effect on customcommands under the 'communications' part of
// the customCommand config.
ServiceNames [ ] string ` yaml:"serviceNames" `
// InternalFunction is the name of a function inside lazydocker that we want to run, as opposed to a command-line command. This is only used internally and can't be configured by the user
InternalFunction func ( ) error ` yaml:"-" `
}
type LogsConfig struct {
Timestamps bool ` yaml:"timestamps,omitempty" `
Since string ` yaml:"since,omitempty" `
Tail string ` yaml:"tail,omitempty" `
}
// GetDefaultConfig returns the application default configuration NOTE (to
// contributors, not users): do not default a boolean to true, because false is
// the boolean zero value and this will be ignored when parsing the user's
// config
func GetDefaultConfig ( ) UserConfig {
duration , err := time . ParseDuration ( "3m" )
if err != nil {
panic ( err )
}
return UserConfig {
Gui : GuiConfig {
ScrollHeight : 2 ,
Language : "auto" ,
ScrollPastBottom : false ,
IgnoreMouseEvents : false ,
Theme : ThemeConfig {
ActiveBorderColor : [ ] string { "green" , "bold" } ,
InactiveBorderColor : [ ] string { "default" } ,
OptionsTextColor : [ ] string { "blue" } ,
} ,
ShowAllContainers : false ,
ReturnImmediately : false ,
WrapMainPanel : true ,
LegacySortContainers : false ,
SidePanelWidth : 0.3333 ,
ShowBottomLine : true ,
ExpandFocusedSidePanel : false ,
} ,
ConfirmOnQuit : false ,
Logs : LogsConfig {
Timestamps : false ,
Since : "60m" ,
Tail : "" ,
} ,
CommandTemplates : CommandTemplatesConfig {
DockerCompose : "docker-compose" ,
RestartService : "{{ .DockerCompose }} restart {{ .Service.Name }}" ,
StartService : "{{ .DockerCompose }} start {{ .Service.Name }}" ,
Up : "{{ .DockerCompose }} up -d" ,
Down : "{{ .DockerCompose }} down" ,
DownWithVolumes : "{{ .DockerCompose }} down --volumes" ,
UpService : "{{ .DockerCompose }} up -d {{ .Service.Name }}" ,
RebuildService : "{{ .DockerCompose }} up -d --build {{ .Service.Name }}" ,
RecreateService : "{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}" ,
StopService : "{{ .DockerCompose }} stop {{ .Service.Name }}" ,
ServiceLogs : "{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}" ,
ViewServiceLogs : "{{ .DockerCompose }} logs --follow {{ .Service.Name }}" ,
AllLogs : "{{ .DockerCompose }} logs --tail=300 --follow" ,
ViewAllLogs : "{{ .DockerCompose }} logs" ,
DockerComposeConfig : "{{ .DockerCompose }} config" ,
CheckDockerComposeConfig : "{{ .DockerCompose }} config --quiet" ,
ServiceTop : "{{ .DockerCompose }} top {{ .Service.Name }}" ,
} ,
CustomCommands : CustomCommands {
Containers : [ ] CustomCommand { } ,
Services : [ ] CustomCommand { } ,
Images : [ ] CustomCommand { } ,
Volumes : [ ] CustomCommand { } ,
} ,
BulkCommands : CustomCommands {
Services : [ ] CustomCommand {
{
Name : "up" ,
Command : "{{ .DockerCompose }} up -d" ,
} ,
{
Name : "up (attached)" ,
Command : "{{ .DockerCompose }} up" ,
Attach : true ,
} ,
{
Name : "stop" ,
Command : "{{ .DockerCompose }} stop" ,
} ,
{
Name : "pull" ,
Command : "{{ .DockerCompose }} pull" ,
Attach : true ,
} ,
{
Name : "build" ,
Command : "{{ .DockerCompose }} build --parallel --force-rm" ,
Attach : true ,
} ,
{
Name : "down" ,
Command : "{{ .DockerCompose }} down" ,
} ,
{
Name : "down with volumes" ,
Command : "{{ .DockerCompose }} down --volumes" ,
} ,
{
Name : "down with images" ,
Command : "{{ .DockerCompose }} down --rmi all" ,
} ,
{
Name : "down with volumes and images" ,
Command : "{{ .DockerCompose }} down --volumes --rmi all" ,
} ,
} ,
Containers : [ ] CustomCommand { } ,
Images : [ ] CustomCommand { } ,
Volumes : [ ] CustomCommand { } ,
} ,
OS : GetPlatformDefaultConfig ( ) ,
Stats : StatsConfig {
MaxDuration : duration ,
Graphs : [ ] GraphConfig {
{
Caption : "CPU (%)" ,
StatPath : "DerivedStats.CPUPercentage" ,
Color : "cyan" ,
} ,
{
Caption : "Memory (%)" ,
StatPath : "DerivedStats.MemoryPercentage" ,
Color : "green" ,
} ,
} ,
} ,
Replacements : Replacements {
ImageNamePrefixes : map [ string ] string { } ,
} ,
}
}
// AppConfig contains the base configuration fields required for lazydocker.
type AppConfig struct {
Debug bool ` long:"debug" env:"DEBUG" default:"false" `
Version string ` long:"version" env:"VERSION" default:"unversioned" `
Commit string ` long:"commit" env:"COMMIT" `
BuildDate string ` long:"build-date" env:"BUILD_DATE" `
Name string ` long:"name" env:"NAME" default:"lazydocker" `
BuildSource string ` long:"build-source" env:"BUILD_SOURCE" default:"" `
UserConfig * UserConfig
ConfigDir string
ProjectDir string
}
// NewAppConfig makes a new app config
func NewAppConfig ( name , version , commit , date string , buildSource string , debuggingFlag bool , composeFiles [ ] string , projectDir string ) ( * AppConfig , error ) {
configDir , err := findOrCreateConfigDir ( name )
if err != nil {
return nil , err
}
userConfig , err := loadUserConfigWithDefaults ( configDir )
if err != nil {
return nil , err
}
// Pass compose files as individual -f flags to docker-compose
if len ( composeFiles ) > 0 {
userConfig . CommandTemplates . DockerCompose += " -f " + strings . Join ( composeFiles , " -f " )
}
appConfig := & AppConfig {
Name : name ,
Version : version ,
Commit : commit ,
BuildDate : date ,
Debug : debuggingFlag || os . Getenv ( "DEBUG" ) == "TRUE" ,
BuildSource : buildSource ,
UserConfig : userConfig ,
ConfigDir : configDir ,
ProjectDir : projectDir ,
}
return appConfig , nil
}
func configDirForVendor ( vendor string , projectName string ) string {
envConfigDir := os . Getenv ( "CONFIG_DIR" )
if envConfigDir != "" {
return envConfigDir
}
configDirs := xdg . New ( vendor , projectName )
return configDirs . ConfigHome ( )
}
func configDir ( projectName string ) string {
legacyConfigDirectory := configDirForVendor ( "jesseduffield" , projectName )
if _ , err := os . Stat ( legacyConfigDirectory ) ; ! os . IsNotExist ( err ) {
return legacyConfigDirectory
}
configDirectory := configDirForVendor ( "" , projectName )
return configDirectory
}
func findOrCreateConfigDir ( projectName string ) ( string , error ) {
folder := configDir ( projectName )
err := os . MkdirAll ( folder , 0 o755 )
if err != nil {
return "" , err
}
return folder , nil
}
func loadUserConfigWithDefaults ( configDir string ) ( * UserConfig , error ) {
config := GetDefaultConfig ( )
return loadUserConfig ( configDir , & config )
}
func loadUserConfig ( configDir string , base * UserConfig ) ( * UserConfig , error ) {
fileName := filepath . Join ( configDir , "config.yml" )
if _ , err := os . Stat ( fileName ) ; err != nil {
if os . IsNotExist ( err ) {
file , err := os . Create ( fileName )
if err != nil {
return nil , err
}
file . Close ( )
} else {
return nil , err
}
}
content , err := ioutil . ReadFile ( fileName )
if err != nil {
return nil , err
}
if err := yaml . Unmarshal ( content , base ) ; err != nil {
return nil , err
}
return base , nil
}
// WriteToUserConfig allows you to set a value on the user config to be saved
// note that if you set a zero-value, it may be ignored e.g. a false or 0 or
// empty string this is because we are using the omitempty yaml directive so
// that we don't write a heap of zero values to the user's config.yml
func ( c * AppConfig ) WriteToUserConfig ( updateConfig func ( * UserConfig ) error ) error {
userConfig , err := loadUserConfig ( c . ConfigDir , & UserConfig { } )
if err != nil {
return err
}
if err := updateConfig ( userConfig ) ; err != nil {
return err
}
file , err := os . OpenFile ( c . ConfigFilename ( ) , os . O_WRONLY | os . O_CREATE , 0 o666 )
if err != nil {
return err
}
return yaml . NewEncoder ( file ) . Encode ( userConfig )
}
// ConfigFilename returns the filename of the current config file
func ( c * AppConfig ) ConfigFilename ( ) string {
return filepath . Join ( c . ConfigDir , "config.yml" )
}