A variety of simple automation scripts and enhancements to Sway and i3.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

395 lines
12KB

  1. #!/usr/bin/perl
  2. ########################################################################
  3. # Usage
  4. ########################################################################
  5. #
  6. # $ displays.pl [layout] [-w]
  7. #
  8. # layout Name of desired layout, as configured starting at line 100
  9. # If missing, last known layout is used (logged to file $last)
  10. # -w (Re)load waybar only. Skip display configuration. Only kill
  11. # existing waybars and start new ones for desired layout
  12. ########################################################################
  13. # Dependencies
  14. ########################################################################
  15. #
  16. # Depends on JSON::XS and Proc::ProcessTable
  17. #
  18. # Debian:
  19. # apt install libjson-xs-perl libproc-processtable-perl
  20. ########################################################################
  21. # Directories and Files
  22. ########################################################################
  23. # Template is a normal config with the following in place of values:
  24. # "output": __OUTPUT__
  25. # "position": __POSITION__
  26. # "width": __WIDTH__ (optional)
  27. my $waybar_template = "$ENV{'HOME'}/.config/waybar/config.template";
  28. # Temporary directory to save generated waybar config(s)
  29. my $waybar_temporary = '/tmp';
  30. # File to log and recover last used layout name
  31. my $last = "$ENV{'HOME'}/.config/last_display";
  32. # File to log active outputs (used by swayidle, and to attempt to restore
  33. my $active_outputs = "$ENV{'HOME'}/.config/active_outputs";
  34. ########################################################################
  35. # Display Serials and Names
  36. ########################################################################
  37. # Hash to match display Serial with a friendly name.
  38. # You can get the serial from (quoted with *):
  39. # $ swaymsg -t get_outputs
  40. # Output eDP-1 'Unknown 0x057D *0x00000000*'
  41. my %outputs = (
  42. #'0x00000101' => 'Sam',
  43. 'HTNCB00059' => 'Sam',
  44. #'3CQ3310Q1Q' => 'Sam',
  45. '3CQ4342S6W' => 'HP-1',
  46. '0x00000101' => 'HP-2',
  47. #'3CQ3310Q1Q' => 'HP-2',
  48. '0x00000000' => 'LVDS'
  49. );
  50. ########################################################################
  51. # Display Configurations
  52. ########################################################################
  53. # First-level key of hash is the name of each configuration
  54. # Second-level keys are the display friendly-names, above
  55. # Third-level are the actual settings for that display
  56. my %configs = (
  57. 'detached' => {
  58. 'HP-1' => {
  59. 'on' => 0
  60. },
  61. 'HP-2' => {
  62. 'on' => 0
  63. },
  64. 'Sam' => {
  65. 'on' => 0
  66. },
  67. 'LVDS' => {
  68. 'on' => 1,
  69. 'width' => 1920,
  70. 'height' => 1080,
  71. 'x' => 0,
  72. 'y' => 0,
  73. 'rotate' => 0,
  74. 'waybar' => 'bottom'
  75. }
  76. },
  77. 'stacked' => {
  78. 'Sam' => {
  79. 'on' => 1,
  80. 'width' => 1920,
  81. 'height' => 1080,
  82. 'x' => 960,
  83. 'y' => 0,
  84. 'rotate' => 0,
  85. 'waybar' => 'bottom'
  86. },
  87. 'HP-1' => {
  88. 'on' => 1,
  89. 'width' => 1920,
  90. 'height' => 1080,
  91. 'x' => 1920,
  92. 'y' => 1200,
  93. 'rotate' => 0,
  94. 'waybar' => 'top'
  95. },
  96. 'HP-2' => {
  97. 'on' => 1,
  98. 'width' => 1920,
  99. 'height' => 1080,
  100. 'x' => 0,
  101. 'y' => 1200,
  102. 'rotate' => 0,
  103. 'waybar' => 'top'
  104. },
  105. 'LVDS' => {
  106. 'on' => 0
  107. }
  108. },
  109. 'sidebyside' => {
  110. 'HP-1' => {
  111. 'on' => 1,
  112. 'width' => 1080,
  113. 'height' => 1920,
  114. 'x' => 0,
  115. 'y' => 225,
  116. 'rotate' => 270,
  117. 'waybar' => 'top'
  118. },
  119. 'Sam' => {
  120. 'on' => 1,
  121. 'width' => 1200,
  122. 'height' => 1920,
  123. 'x' => 1080,
  124. 'y' => 0,
  125. 'rotate' => 90,
  126. 'waybar' => 'top'
  127. },
  128. 'HP-2' => {
  129. 'on' => 1,
  130. 'width' => 1080,
  131. 'height' => 1920,
  132. 'x' => 2280,
  133. 'y' => 225,
  134. 'rotate' => 90,
  135. 'waybar' => 'top'
  136. },
  137. 'LVDS' => {
  138. 'on' => 0,
  139. }
  140. }
  141. );
  142. ########################################################################
  143. # Program (do not edit below)
  144. ########################################################################
  145. use strict;
  146. use warnings;
  147. my $waybar_only = 0;
  148. my $restore = 0; # Set if no config is provided. Requires validation.
  149. my $config;
  150. if (scalar(@ARGV)) {
  151. while (@ARGV) {
  152. my $arg = shift;
  153. if ($arg eq "-w") {
  154. $waybar_only = 1;
  155. } else {
  156. if (defined $config) {
  157. die "Too many arguments.\n";
  158. }
  159. $config = $arg;
  160. }
  161. }
  162. }
  163. # Get previous config if one is not provided
  164. unless (defined $config) {
  165. open(my $fh, '<', $last) ||
  166. die "Config name not provided and failed to open $last\n";
  167. $config = <$fh>;
  168. close($fh);
  169. chomp $config;
  170. $restore = 1;
  171. }
  172. # Bail if requested config doesn't exist
  173. if (!defined($configs{$config})) {
  174. if ($restore) {
  175. # If restoration is attempted with invalid config, just enable eDP-1
  176. $config = 'detached';
  177. } else {
  178. die "$config is not a defined configuration: "
  179. . join(', ', keys %configs) . "\n";
  180. }
  181. }
  182. # Write config that is to be used so that it can be recovered as default
  183. open(my $fh, '>', $last) ||
  184. print STDERR "Config name cannot be written to $last\n";
  185. print $fh $config;
  186. close($fh);
  187. # Fetch connected displays
  188. use JSON::XS;
  189. my $json = JSON::XS->new();
  190. my $displays_raw = `swaymsg -t get_outputs --raw`;
  191. my $displays = $json->decode($displays_raw);
  192. # For each connected display, collect the desired settings for enabled
  193. # displays and a list of displays to disable
  194. my $on;
  195. my @off;
  196. for (my $i = 0; $i < scalar(@$displays); $i++) {
  197. # If a display is found without any settings print error and turn it off
  198. unless (defined $configs{$config}{$outputs{$displays->[$i]->{serial}}}){
  199. print STDERR "Output $displays->[$i]->{name} ("
  200. . $displays->[$i]->{serial}
  201. . ") found without defined function. Disabling.\n";
  202. push @off, $displays->[$i]->{name};
  203. next;
  204. }
  205. # If display is enabled, copy all of the desired settings
  206. if ($configs{$config}{$outputs{$displays->[$i]->{serial}}}{on}) {
  207. $on->{$outputs{$displays->[$i]->{serial}}} =
  208. $configs{$config}{$outputs{$displays->[$i]->{serial}}};
  209. $on->{$outputs{$displays->[$i]->{serial}}}{output} =
  210. $displays->[$i]->{name};
  211. # Otherwise simply list it for disabling
  212. } else {
  213. push @off, $displays->[$i]->{name};
  214. }
  215. }
  216. # Verify that all requested displays are actually available
  217. my @unavailable;
  218. foreach my $output (keys %$on) {
  219. my $found = 0;
  220. foreach my $o (keys %outputs) {
  221. if ($outputs{$o} eq $output) {
  222. $found = 1;
  223. last;
  224. }
  225. }
  226. unless ($found) {
  227. push @unavailable, $output;
  228. }
  229. }
  230. if (scalar(@unavailable)) {
  231. die "Config requires unavailable output(s) " . join(', ', @unavailable) . "\n";
  232. }
  233. # Skip enabling/disabling displays if only running waybar (re)start
  234. unless ($waybar_only) {
  235. # Number of simultaneous outputs is limited by gpu, so disabled displays
  236. # first
  237. foreach (@off) {
  238. # Sway returns status as JSON
  239. my $res_raw = `sway output $_ disable`;
  240. my $res = $json->decode($res_raw)->[0];
  241. # If failed, print to STDERR
  242. unless ($res->{success}) {
  243. print STDERR "Error ($res->{error}) in command 'sway"
  244. . "output $_ disable'\n";
  245. }
  246. }
  247. }
  248. # Kill existing Waybars
  249. require Proc::ProcessTable;
  250. my $t = new Proc::ProcessTable;
  251. foreach my $p ( @{ $t->table } ) {
  252. my $cmndline = $p->{'cmndline'};
  253. $cmndline =~ s/\s*$//g;
  254. if ($cmndline =~ /^waybar.*/) {
  255. # Never kill this process
  256. if ($p->{'pid'} == $$) {
  257. next;
  258. } else {
  259. $p->kill(9);
  260. }
  261. }
  262. }
  263. # Load in config template
  264. my $template;
  265. if (open (my $fh, '<', $waybar_template)) {
  266. while (<$fh>) {
  267. $template .= $_;
  268. }
  269. close $fh;
  270. chomp $template;
  271. } else {
  272. print STDERR "Failed to load template $waybar_template\n";
  273. }
  274. # If template is already set up as an array, remove the square brackets so that
  275. # we can concatenate multiple displays
  276. $template =~ s/^[^\[\{]*\[(.*)\]$/$1/s;
  277. # Setup waybar config file
  278. my $waybar = '';
  279. # Configure each enabled display
  280. my @active;
  281. foreach my $out (keys %$on) {
  282. push @active, $on->{$out}->{output};
  283. unless ($waybar_only) {
  284. # Build command, starting by enabling and powering on
  285. my $cmd = "sway output $on->{$out}->{output} enable dpms on";
  286. # If additional options are provided, add them to command
  287. if (defined $on->{$out}->{rotate}) {
  288. $cmd .= " transform $on->{$out}->{rotate}";
  289. }
  290. if (defined $on->{$out}->{x} && defined $on->{$out}->{y}) {
  291. $cmd .= " position $on->{$out}->{x} $on->{$out}->{y}";
  292. }
  293. if (defined $on->{$out}->{width} &&
  294. defined $on->{$out}->{height} )
  295. {
  296. $cmd .= " mode $on->{$out}->{width}x$on->{$out}->{height}";
  297. }
  298. # Sway returns status as JSON
  299. my $res_raw = `$cmd`;
  300. my $res = $json->decode($res_raw)->[0];
  301. # If failed, print to STDERR
  302. unless ($res->{success}) {
  303. print STDERR "Error ($res->{error}) in command '$cmd'\n";
  304. }
  305. }
  306. # Skip waybar setup if template failed to be loaded
  307. if ( (defined $template) &&
  308. (defined $on->{$out}->{waybar}) &&
  309. ($on->{$out}->{waybar} =~ m/(top|bottom|left|right)/) )
  310. {
  311. # If there's already a display set up, add a comma
  312. unless ($waybar eq '') {
  313. $waybar .= ',';
  314. }
  315. $waybar .= $template;
  316. # Replace basic preferences
  317. $waybar =~ s/__OUTPUT__/"$on->{$out}->{output}"/;
  318. $waybar =~ s/__POSITION__/"$on->{$out}->{waybar}"/;
  319. if (defined $on->{$out}->{width}) {
  320. $waybar =~ s/__WIDTH__/$on->{$out}->{width}/;
  321. # If width is not set, comment that line out to use default
  322. } else {
  323. $waybar =~ s/([^\s]*\s*)__WIDTH__/\/\/ $1__WIDTH__/gg;
  324. }
  325. }
  326. }
  327. open($fh, '>', $active_outputs);
  328. print $fh join(' ', @active);
  329. close($fh);
  330. # Restore array formatting
  331. $waybar = '[' . $waybar . ']';
  332. # Start Waybar as fork
  333. my $pid = fork;
  334. unless ($pid) {
  335. open STDIN, '/dev/null';
  336. open STDOUT, '>>/dev/null';
  337. open STDERR, '>>/dev/null';
  338. # Write config to a temporary file
  339. my $tmp = $waybar_temporary . "/waybar-" . time() .".config";
  340. open ($fh, '>', $tmp);
  341. print $fh $waybar;
  342. close $fh;
  343. `nohup waybar --config=$tmp >> waybar.log`;
  344. # Remove config
  345. unlink $tmp;
  346. }