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.

462 lines
15KB

  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. #my $swaysock = "/home/jpm/.config/sway-ipc.sock";
  29. my $swaysock = `ls /run/user/1000/sway-ipc.1000*`;
  30. chomp($swaysock);
  31. # Path to actual config file generated from template
  32. my $waybar_config = "$ENV{'HOME'}/.config/waybar/config";
  33. # File to log and recover last used layout name
  34. my $last = "$ENV{'HOME'}/.config/last_display";
  35. # File to log active outputs (used by swayidle, and to attempt to restore
  36. my $active_outputs = "$ENV{'HOME'}/.config/active_outputs";
  37. ########################################################################
  38. # Display Serials and Names
  39. ########################################################################
  40. # Hash to match display Serial with a friendly name.
  41. # You can get the serial from (quoted with *):
  42. # $ swaymsg -t get_outputs
  43. # Output eDP-1 'Unknown 0x057D *0x00000000*'
  44. my %outputs = (
  45. #'0x00000101' => 'Sam',
  46. 'HTNCB00059' => 'Sam',
  47. #'3CQ4342S6W' => 'HP-1',
  48. '0x00000101' => 'HP-1',
  49. '3CQ3310Q1Q' => 'HP-2',
  50. '0x00000000' => 'LVDS'
  51. );
  52. ########################################################################
  53. # Display Configurations
  54. ########################################################################
  55. # First-level key of hash is the name of each configuration
  56. # Second-level keys are the display friendly-names, above
  57. # Third-level are the actual settings for that display
  58. my %configs = (
  59. 'detached' => {
  60. 'HP-1' => {
  61. 'on' => 0
  62. },
  63. 'HP-2' => {
  64. 'on' => 0
  65. },
  66. 'Sam' => {
  67. 'on' => 0
  68. },
  69. 'eDP-1' => {
  70. 'on' => 1,
  71. 'width' => 1920,
  72. 'height' => 1080,
  73. 'x' => 0,
  74. 'y' => 0,
  75. 'rotate' => 0,
  76. 'waybar' => 'bottom',
  77. }
  78. },
  79. 'stacked-laptop' => {
  80. 'Sam' => {
  81. 'on' => 1,
  82. 'width' => 1920,
  83. 'height' => 1200,
  84. 'x' => 960,
  85. 'y' => 0,
  86. 'rotate' => 0,
  87. 'waybar' => 'bottom'
  88. },
  89. 'HP-1' => {
  90. 'on' => 1,
  91. 'width' => 1920,
  92. 'height' => 1080,
  93. 'x' => 1920,
  94. 'y' => 1200,
  95. 'rotate' => 0,
  96. 'waybar' => 'top'
  97. },
  98. 'HP-2' => {
  99. 'on' => 0
  100. },
  101. 'eDP-1' => {
  102. 'on' => 1,
  103. 'width' => 1920,
  104. 'height' => 1080,
  105. 'x' => 0,
  106. 'y' => 1200,
  107. 'rotate' => 0,
  108. 'waybar' => 'bottom'
  109. }
  110. },
  111. 'stacked' => {
  112. 'Sam' => {
  113. 'on' => 1,
  114. 'width' => 1920,
  115. 'height' => 1200,
  116. 'x' => 960,
  117. 'y' => 0,
  118. 'rotate' => 0,
  119. 'waybar' => 'bottom'
  120. },
  121. 'HP-1' => {
  122. 'on' => 1,
  123. 'width' => 1920,
  124. 'height' => 1080,
  125. 'x' => 1920,
  126. 'y' => 1200,
  127. 'rotate' => 0,
  128. 'waybar' => 'top'
  129. },
  130. 'HP-2' => {
  131. 'on' => 1,
  132. 'width' => 1920,
  133. 'height' => 1080,
  134. 'x' => 0,
  135. 'y' => 1200,
  136. 'rotate' => 0,
  137. 'waybar' => 'top'
  138. },
  139. 'eDP-1' => {
  140. 'on' => 0
  141. }
  142. },
  143. 'sidebyside' => {
  144. 'HP-1' => {
  145. 'on' => 1,
  146. 'width' => 1080,
  147. 'height' => 1920,
  148. 'x' => 0,
  149. 'y' => 225,
  150. 'rotate' => 270,
  151. 'waybar' => 'top'
  152. },
  153. 'Sam' => {
  154. 'on' => 1,
  155. 'width' => 1200,
  156. 'height' => 1920,
  157. 'x' => 1080,
  158. 'y' => 0,
  159. 'rotate' => 90,
  160. 'waybar' => 'top'
  161. },
  162. 'HP-2' => {
  163. 'on' => 1,
  164. 'width' => 1080,
  165. 'height' => 1920,
  166. 'x' => 2280,
  167. 'y' => 225,
  168. 'rotate' => 90,
  169. 'waybar' => 'top'
  170. },
  171. 'LVDS' => {
  172. 'on' => 0,
  173. }
  174. }
  175. );
  176. my $hostname = `hostname`;
  177. chomp($hostname);
  178. if ($hostname eq "yoga.lan.john.me.tz") {
  179. $configs{'detached'}->{'eDP-1'}->{'width'} = 2560;
  180. $configs{'detached'}->{'eDP-1'}->{'height'} = 1440;
  181. $configs{'detached'}->{'eDP-1'}->{'scale'} = 1.333333;
  182. }
  183. ########################################################################
  184. # Disable display from other laptop
  185. ########################################################################
  186. if ($ENV{'SSH_AUTH_SOCK'} eq '/home/jpm/.ssh/ssh-agent.yoga.lan.john.me.tz.sock') {
  187. foreach my $layout (keys %configs) {
  188. delete($configs{$layout}{'LVDS'});
  189. }
  190. $outputs{'0x00000000'} = 'eDP-1';
  191. } else {
  192. foreach my $layout (keys %configs) {
  193. delete($configs{$layout}{'eDP-1'});
  194. }
  195. }
  196. ########################################################################
  197. # Program (do not edit below)
  198. ########################################################################
  199. use strict;
  200. use warnings;
  201. my $waybar_only = 0;
  202. my $restore = 0; # Set if no config is provided. Requires validation.
  203. my $config;
  204. if (scalar(@ARGV)) {
  205. while (@ARGV) {
  206. my $arg = shift;
  207. if ($arg eq "-w") {
  208. $waybar_only = 1;
  209. } else {
  210. if (defined $config) {
  211. die "Too many arguments.\n";
  212. }
  213. $config = $arg;
  214. }
  215. }
  216. }
  217. # Get previous config if one is not provided
  218. unless (defined $config) {
  219. open(my $fh, '<', $last) ||
  220. die "Config name not provided and failed to open $last\n";
  221. $config = <$fh>;
  222. close($fh);
  223. chomp $config;
  224. $restore = 1;
  225. }
  226. # Bail if requested config doesn't exist
  227. if (!defined($configs{$config})) {
  228. if ($restore) {
  229. # If restoration is attempted with invalid config, just enable eDP-1
  230. $config = 'detached';
  231. } else {
  232. die "$config is not a defined configuration: "
  233. . join(', ', keys %configs) . "\n";
  234. }
  235. }
  236. # Write config that is to be used so that it can be recovered as default
  237. open(my $fh, '>', $last) ||
  238. print STDERR "Config name cannot be written to $last\n";
  239. print $fh $config;
  240. close($fh);
  241. # Fetch connected displays
  242. use JSON::XS;
  243. my $json = JSON::XS->new();
  244. my $displays_raw = `swaymsg -s $swaysock -t get_outputs --raw`;
  245. my $displays = $json->decode($displays_raw);
  246. # For each connected display, collect the desired settings for enabled
  247. # displays and a list of displays to disable
  248. my $on;
  249. my @off;
  250. for (my $i = 0; $i < scalar(@$displays); $i++) {
  251. # If a display is found without any settings print error and turn it off
  252. unless (defined $configs{$config}{$outputs{$displays->[$i]->{serial}}}){
  253. print STDERR "Output $displays->[$i]->{name} ("
  254. . $displays->[$i]->{serial}
  255. . ") found without defined function. Disabling.\n";
  256. push @off, $displays->[$i]->{name};
  257. next;
  258. }
  259. # If display is enabled, copy all of the desired settings
  260. if ($configs{$config}{$outputs{$displays->[$i]->{serial}}}{on}) {
  261. $on->{$outputs{$displays->[$i]->{serial}}} =
  262. $configs{$config}{$outputs{$displays->[$i]->{serial}}};
  263. $on->{$outputs{$displays->[$i]->{serial}}}{output} =
  264. $displays->[$i]->{name};
  265. # Otherwise simply list it for disabling
  266. } else {
  267. push @off, $displays->[$i]->{name};
  268. }
  269. }
  270. # Verify that all requested displays are actually available
  271. my @unavailable;
  272. foreach my $output (keys %$on) {
  273. my $found = 0;
  274. foreach my $o (keys %outputs) {
  275. if ($outputs{$o} eq $output) {
  276. $found = 1;
  277. last;
  278. }
  279. }
  280. unless ($found) {
  281. push @unavailable, $output;
  282. }
  283. }
  284. if (scalar(@unavailable)) {
  285. die "Config requires unavailable output(s) " . join(', ', @unavailable)
  286. . "\n";
  287. }
  288. # Skip enabling/disabling displays if only running waybar (re)start
  289. unless ($waybar_only) {
  290. # Number of simultaneous outputs is limited by gpu, so disabled displays
  291. # first
  292. foreach (@off) {
  293. # Sway returns status as JSON
  294. my $res_raw = `swaymsg -s $swaysock output $_ disable`;
  295. my $res = $json->decode($res_raw)->[0];
  296. # If failed, print to STDERR
  297. unless ($res->{success}) {
  298. print STDERR "Error ($res->{error}) in command 'swaymsg"
  299. . " -s $swaysock output $_ disable'\n";
  300. }
  301. }
  302. }
  303. # Kill existing Waybars
  304. require Proc::ProcessTable;
  305. my $t = new Proc::ProcessTable;
  306. foreach my $p ( @{ $t->table } ) {
  307. my $cmndline = $p->{'cmndline'};
  308. $cmndline =~ s/\s*$//g;
  309. if ($cmndline =~ /^waybar.*/) {
  310. # Never kill this process
  311. if ($p->{'pid'} == $$) {
  312. next;
  313. } else {
  314. $p->kill(9);
  315. }
  316. }
  317. }
  318. # Load in config template
  319. my $template;
  320. if (open (my $fh, '<', $waybar_template)) {
  321. while (<$fh>) {
  322. $template .= $_;
  323. }
  324. close $fh;
  325. chomp $template;
  326. } else {
  327. print STDERR "Failed to load template $waybar_template\n";
  328. }
  329. # If template is already set up as an array, remove the square brackets so that
  330. # we can concatenate multiple displays
  331. $template =~ s/^[^\[\{]*\[(.*)\]$/$1/s;
  332. # Setup waybar config file
  333. my $waybar = '';
  334. # Configure each enabled display
  335. my @active;
  336. foreach my $out (keys %$on) {
  337. push @active, $on->{$out}->{output};
  338. unless ($waybar_only) {
  339. # Build command, starting by enabling and powering on
  340. my $cmd = "swaymsg -s $swaysock output $on->{$out}->{output}" .
  341. " enable dpms on";
  342. # If additional options are provided, add them to command
  343. if (defined $on->{$out}->{scale}) {
  344. $cmd .= " scale $on->{$out}->{scale}";
  345. }
  346. if (defined $on->{$out}->{rotate}) {
  347. $cmd .= " transform $on->{$out}->{rotate}";
  348. }
  349. if (defined $on->{$out}->{x} && defined $on->{$out}->{y}) {
  350. $cmd .= " position $on->{$out}->{x} $on->{$out}->{y}";
  351. }
  352. if (defined $on->{$out}->{width} &&
  353. defined $on->{$out}->{height} )
  354. {
  355. $cmd .= " mode $on->{$out}->{width}x" .
  356. "$on->{$out}->{height}";
  357. }
  358. # Sway returns status as JSON
  359. my $res_raw = `$cmd`;
  360. my $res = $json->decode($res_raw)->[0];
  361. # If failed, print to STDERR
  362. unless ($res->{success}) {
  363. print STDERR "Error ($res->{error}) in command " .
  364. "'$cmd'\n";
  365. }
  366. }
  367. # Skip waybar setup if template failed to be loaded
  368. if ( (defined $template) &&
  369. (defined $on->{$out}->{waybar}) &&
  370. ($on->{$out}->{waybar} =~ m/(top|bottom|left|right)/) )
  371. {
  372. # If there's already a display set up, add a comma
  373. unless ($waybar eq '') {
  374. $waybar .= ',';
  375. }
  376. $waybar .= $template;
  377. # Replace basic preferences
  378. $waybar =~ s/__OUTPUT__/"$on->{$out}->{output}"/;
  379. $waybar =~ s/__POSITION__/"$on->{$out}->{waybar}"/;
  380. if (defined $on->{$out}->{width}) {
  381. my $x = $on->{$out}->{width};
  382. if (defined $on->{$out}->{scale}) {
  383. $x = sprintf("%.0d", $x / $on->{$out}->{scale});
  384. print "width: $x\n";
  385. }
  386. $waybar =~ s/__WIDTH__/$x/;
  387. # If width is not set, comment that line out to use default
  388. } else {
  389. $waybar =~ s/([^\s]*\s*)__WIDTH__/\/\/ $1__WIDTH__/gg;
  390. }
  391. }
  392. }
  393. open($fh, '>', $active_outputs);
  394. print $fh join(' ', @active);
  395. close($fh);
  396. # Restore array formatting
  397. $waybar = '[' . $waybar . ']';
  398. # Start Waybar as fork
  399. my $pid = fork;
  400. unless ($pid) {
  401. open STDIN, '/dev/null';
  402. open STDOUT, '>>/dev/null';
  403. open STDERR, '>>/dev/null';
  404. # Write config to a temporary file
  405. open ($fh, '>', $waybar_config);
  406. print $fh $waybar;
  407. close $fh;
  408. my $waydisplay = $ENV{'WAYLAND_DISPLAY'} || 'wayland-0';
  409. `WAYLAND_DISPLAY=$waydisplay nohup waybar --config=$waybar_config >> waybar.log`;
  410. }