Select Git revision
prvocisla-vypis.py
-
Martin Mareš authoredMartin Mareš authored
Program.cs 17.93 KiB
// i3csstatus - Alternative generator of i3 status bar written in c#
// (c) 2022 Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz>
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#nullable enable
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using Pastel;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Linq;
using System.Threading;
using StringInfo = System.Globalization.StringInfo;
using System.CommandLine;
using System.Runtime.InteropServices;
using Color = System.Drawing.Color;
using Process = System.Diagnostics.Process;
using Config;
namespace i3csstatus;
class PrintAndExit: Exception
{
public string ToPrint;
public int ExitCode;
public PrintAndExit(string _ToPrint, int _ExitCode=1)
{
ToPrint = _ToPrint;
ExitCode = _ExitCode;
}
public override string Message { get => $"Print \"{ToPrint}\" and exit with exit code {ExitCode}"; }
}
static class POSIX
{
[DllImport("libc", SetLastError = true)]
public static extern int mkfifo(string path, int mode);
[DllImport("libc", CallingConvention=CallingConvention.Cdecl, SetLastError=true)]
public static extern int kill(int pid, int sig);
public static int SIGUSR1 = 10;
public static int EEXIST = 17;
public static int ERRNO { get => Marshal.GetLastPInvokeError();}
public static void Bash(string cmd)
{
var process = new Process();
process.StartInfo.FileName = "bash";
process.StartInfo.Arguments = "";
process.StartInfo.RedirectStandardInput = true;
process.Start();
TextWriter stdin = process.StandardInput;
stdin.WriteLine("exec 1> /dev/stderr");
stdin.Write(cmd);
stdin.Close();
}
}
static class ProcesExtended
{
public static int KillBySignal(this Process p, int sig)
{
return POSIX.kill(p.Id, sig);
}
}
static class JsonExtended
{
public static JsonObject RemoveNull(this JsonObject o)
{
List<string> toDelete = new();
foreach(var (k, v) in o)
{
if(v == null)
toDelete.Add(k);
}
foreach(var k in toDelete)
o.Remove(k);
return o;
}
}
static class ColorExtended
{
static public string ToHex(this Color c)
{
return $"#{c.R:X2}{c.G:X2}{c.B:X2}";
}
static public Color ToColor(this string s)
{
return System.Drawing.ColorTranslator.FromHtml(s);
}
}
enum Align
{
Left,
Right,
Center
}
enum Markup
{
None,
Pango
}
record Coordinates(
int X,
int Y
);
enum MouseButton
{
Left,
Right,
Middle
}
[Flags]
enum Modifiers
{
Shift=1,
Control=2,
Super=4,
Alt=8
}
record ClickEvent(
Coordinates Relative, // relative_x, relative_y
Coordinates Size, // width, height
MouseButton Button,
Coordinates? Absolute = null, // x, y
Coordinates? Output = null, // output_x, output_y
Modifiers Modifiers = 0
);
record Block(
string Text,
string? ShortText=null,
Color? Color=null,
Color? BackgroundColor=null,
Color? BorderColor=null,
int? BorderTop=null,
int? BorderRight=null,
int? BorderBottom = null,
int? BorderLeft = null,
string? MinWidth_string = null,
int? MinWidth_int = null,
Align Align = Align.Left,
bool Urgent = false,
bool Separator = true,
int? SeparatorBlockWidth=null,
Markup Markup = Markup.None,
Action<ClickEvent>? OnClick=null
);
interface ModuleParent
{
public abstract void Schedule(int in_ms);
public abstract T GetGlobal<T>() where T: GlobalModuleResource, new();
}
abstract partial class StatusBar: ModuleParent
{
static public string DefaultConfigText =
@"
refresh = 1
[wireless]
_type=i3status
global_cache = 1
name = wireless _first_
config
format_up = ""W: (%quality %essid) %ip""
format_down = ""W: down""
[ethernet]
_type=i3status
name=ethernet _first_
config
format_up = ""E: %ip (%speed)""
format_down = ""E: down""
[battery]
[load]
_type = i3status
name = load
config = format = ""%1min L""
[memory]
_type = i3status
name = memory
config
format = ""%available""
threshold_degraded = ""1G""
format_degraded = ""MEMORY %available""
[time]
format = yyyy-MM-dd HH:mm:ss
refresh
";
List<Module> modules = new();
protected void parseConfigFile(FileInfo? configFile)
{
if(configFile == null)
{
string configText;
string? fileName;
try
{
fileName = Environment.GetEnvironmentVariable("HOME")+"/.config/i3/i3csstatus.conf";
configText = File.ReadAllText(fileName);
}
catch(Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException)
{
fileName = null;
configText = DefaultConfigText;
}
parseConfig(new ConfigParser(configText, fileName));
}
else
parseConfigString(File.ReadAllText(configFile.ToString()), configFile.ToString());
}
protected void parseConfigString(string configString, string? fileName)
{
try
{
parseConfig(new ConfigParser(configString, fileName));
}
catch(ConfigException e) when (e.Parser.FileName == fileName && fileName != null)
{
throw new PrintAndExit(e.ErrorStringWithFile());
}
}
virtual protected void parseConfig(ConfigParser p)
{
var moduleTypes = new Dictionary<string, Type>(
from assembly in AppDomain.CurrentDomain.GetAssemblies()
from type in assembly.GetTypes()
where type.IsDefined(typeof(ModuleName), false)
where typeof(Module).IsAssignableFrom(type)
from name in type.GetCustomAttributes(typeof(ModuleName), false)
select new KeyValuePair<string, Type>(((ModuleName)name).GetName(), type)
);
var constructorSignature = new Type[]{};
foreach(var s in p)
{
ConfigValue type = s.OptionalDefaultIsSectionName("_type");
if(!moduleTypes.ContainsKey(type.AsString()))
throw new ConfigMistake(type, $"Module type with name \"{type.AsString()}\" not exists.");
var constructor = moduleTypes[type.AsString()].GetConstructor(constructorSignature);
if(constructor == null)
throw new Exception($"Missing constructor of {type} module");
Module module = (Module) constructor.Invoke(new object[]{});
modules.Add(addStandardModuleWrappers(s, module, this));
s.Use();
s.CheckUnused();
}
p.MainSection.CheckUnused();
p.CheckUnused();
}
public List<Block> Get()
{
var elements = new List<Block>();
foreach(var m in modules)
elements.AddRange(m.Get());
return elements;
}
public abstract void Schedule(int in_ms);
public abstract T GetGlobal<T>() where T: GlobalModuleResource, new();
}
class InnerStatusBar: StatusBar
{
ModuleParent parrent;
public InnerStatusBar(ModuleParent _parrent, ConfigParser p)
{
parrent = _parrent;
parseConfig(p);
}
public override void Schedule(int in_ms) => parrent.Schedule(in_ms);
public override T GetGlobal<T>() => parrent.GetGlobal<T>();
}
abstract class RootStatusBar: StatusBar
{
long nextRun = 0;
long refresh_ms;
object nextRunMonitor = new();
public RootStatusBar(FileInfo configFile)
{
parseConfigFile(configFile);
}
override protected void parseConfig(ConfigParser p)
{
refresh_ms = p.MainSection["refresh"]?.AsMs() ?? 1000;
base.parseConfig(p);
foreach(var g in globalModuleResources)
g.Value.InitEnd();
}
void wait()
{
lock(nextRunMonitor)
{
while(true)
{
long actual = Environment.TickCount64;
if(nextRun <= actual)
break;
long wait_ms = Math.Min(10000, nextRun - actual);
Monitor.Wait(nextRunMonitor, (int)wait_ms, true);
}
nextRun = Environment.TickCount64 + refresh_ms;
}
}
public void Run(TextWriter w)
{
initOutput(w);
while(true)
{
wait();
Print(w);
}
}
abstract protected void format(TextWriter w, List<Block> elements);
virtual protected void initOutput(TextWriter w){}
public void Print(TextWriter w)
{
foreach(var g in globalModuleResources)
g.Value.GetBegin();
var elements = Get();
foreach(var g in globalModuleResources)
g.Value.GetEnd();
format(w, elements);
w.Flush();
}
public override void Schedule(int in_ms)
{
lock(nextRunMonitor)
{
long actual = Environment.TickCount64;
if(nextRun <= actual + in_ms)
return;
nextRun = actual + in_ms;
Monitor.Pulse(nextRunMonitor);
}
}
Dictionary<Type, GlobalModuleResource> globalModuleResources = new();
public override T GetGlobal<T>()
{
if(!globalModuleResources.ContainsKey(typeof(T)))
globalModuleResources[typeof(T)] = new T();
return (T)globalModuleResources[typeof(T)];
}
}
class StatusBarPlainText: RootStatusBar
{
public static string RemoveMarkup(string s, Markup m)
{
if(m == Markup.Pango)
{
bool inTag = new();
StringBuilder res = new();
foreach(char c in s)
{
if(c == '<') inTag = true;
if(!inTag) res.Append(c);
if(c == '>') inTag = false;
}
return res.ToString();
}
return s;
}
protected virtual string elementAsString(Block e)
{
string s = RemoveMarkup(e.Text, e.Markup);
if(e.MinWidth_string != null || e.MinWidth_int != null)
{
int minWidth = 0;
if(e.MinWidth_string != null)
minWidth = new StringInfo(e.MinWidth_string).LengthInTextElements;
if(e.MinWidth_int != null)
minWidth = (int)e.MinWidth_int/6;
int current = new StringInfo(s).LengthInTextElements;
if(current < minWidth)
{
if(e.Align == Align.Left) s = s + new String(' ', minWidth-current);
if(e.Align == Align.Right) s = new String(' ', minWidth-current) + s;
if(e.Align == Align.Center) s = new String(' ', (minWidth-current+1)/2) + s + new String(' ', (minWidth-current)/2);
}
}
return s;
}
public StatusBarPlainText(FileInfo configFile):base(configFile){}
override protected void format(TextWriter w, List<Block> elements)
{
Block? last = null;
foreach(var e in elements)
{
if(last != null)
w.Write(last.Separator?"|":" ");
w.Write(elementAsString(e));
last = e;
}
w.WriteLine("");
}
}
class StatusBarTerminal: StatusBarPlainText
{
Thread? inputThread;
public StatusBarTerminal(FileInfo configFile, bool doInput):base(configFile)
{
if(doInput)
{
inputThread = new Thread(this.inputThreadFunc);
inputThread.IsBackground = true;
inputThread.Start();
}
}
void inputThreadFunc()
{
while(true)
{
System.ConsoleKeyInfo k = Console.ReadKey();
if(k.KeyChar == '\r')
Schedule(0);
else if(k.KeyChar == 'q')
Environment.Exit(0);
else
Console.WriteLine("");
}
}
override protected string elementAsString(Block e)
{
string msg = base.elementAsString(e);
if(e.Urgent) msg=$"\x1B[1m{msg}\x1B[0m";
if(e.Color != null) msg = msg.Pastel(e.Color.Value);
if(e.BackgroundColor != null) msg = msg.PastelBg(e.BackgroundColor.Value);
return msg;
}
override protected void format(TextWriter w, List<Block> elements)
{
Block? last = null;
foreach(var e in elements)
{
if(last != null)
w.Write((last.Separator?"|":" ").Pastel(Color.FromArgb(165, 229, 250)));
w.Write(elementAsString(e));
last = e;
}
w.WriteLine("");
}
}
class StatusBarI3: RootStatusBar
{
Thread? inputThread;
record HistoryElement(
long time_ms,
long id,
IEnumerable<Block> data
);
Queue<HistoryElement> history = new(); // for finding correct lambda in inputThread
int outputCounter = 0;
public StatusBarI3(FileInfo configFile, bool doInput):base(configFile)
{
Console.Error.Flush();
if(doInput)
{
inputThread = new Thread(this.inputThreadFunc);
inputThread.IsBackground = true;
inputThread.Start();
}
}
void inputThreadFunc()
{
Console.ReadLine(); // read "["
while(true)
{
#pragma warning disable 8602
string? line = Console.ReadLine();
if(line == null) throw new Exception("I3bar close input");
if(line[0] == ',') line = line[1..];
JsonObject json = JsonObject.Parse(line).AsObject();
MouseButton button;
int jsonButton = json["button"].AsValue().GetValue<int>();
if(jsonButton == 1) button = MouseButton.Left; else
if(jsonButton == 2) button = MouseButton.Middle; else
if(jsonButton == 3) button = MouseButton.Right; else
continue;
Modifiers mod = 0;
foreach(var i in json["modifiers"].AsArray())
{
var s = i.AsValue().GetValue<string>();
if(s=="Shift") mod |= Modifiers.Shift;
if(s=="Control") mod |= Modifiers.Control;
if(s=="Mod4") mod |= Modifiers.Super;
if(s=="Mod1") mod |= Modifiers.Alt;
}
Console.Error.WriteLine(mod);
Console.Error.Flush();
var ev = new ClickEvent(
Relative: new Coordinates(json["relative_x"].AsValue().GetValue<int>(), json["relative_y"].AsValue().GetValue<int>()),
Absolute: new Coordinates(json["x"].AsValue().GetValue<int>(), json["y"].AsValue().GetValue<int>()),
Output: new Coordinates(json["output_x"].AsValue().GetValue<int>(), json["output_x"].AsValue().GetValue<int>()),
Size: new Coordinates(json["height"].AsValue().GetValue<int>(), json["width"].AsValue().GetValue<int>()),
Button: button,
Modifiers: mod
);
string name = json["name"].AsValue().GetValue<string>();
int outputId = int.Parse(name.Split(".")[0]);
int blockId = int.Parse(name.Split(".")[1]);
HistoryElement currentBar;
lock(history)
{
while(history.Count > 0 && history.Peek().id < outputId)
history.Dequeue();
if(history.Count <= 0) break;
currentBar = history.Peek();
}
var block = currentBar.data.ElementAt(blockId);
if(block.OnClick != null)
block.OnClick(ev);
#pragma warning restore 8602
}
}
override protected void initOutput(TextWriter w)
{
if(inputThread == null)
w.WriteLine("{\"version\":1}");
else
w.WriteLine("{\"version\":1, \"click_events\": true }");
w.WriteLine("[{}");
}
override protected void format(TextWriter w, List<Block> elements)
{
if(inputThread != null)
{
lock(history)
{
while(history.Count > 0 && history.Peek().time_ms < Environment.TickCount64 - 1000)
history.Dequeue();
history.Enqueue(new HistoryElement(Environment.TickCount64, outputCounter, elements));
}
}
JsonNode? intStringUnion(int? _int, string? _string)
{
JsonNode? n = null;
if(_int != null) n = _int;
if(_string != null) n = _string;
return n;
}
int i = 0;
var json = new JsonArray((from e in elements select new JsonObject(){
["name"] = $"{outputCounter}.{i++}",
["full_text"] = e.Text,
["short_text"] = e.ShortText,
["color"] = e.Color?.ToHex(),
["background"] = e.BackgroundColor?.ToHex(),
["border"] = e.BorderColor?.ToHex(),
["border_top"] = e.BorderTop,
["border_right"] = e.BorderTop,
["border_bottom"] = e.BorderTop,
["border_left"] = e.BorderTop,
["min_width"] = intStringUnion(e.MinWidth_int, e.MinWidth_string),
["align"] = e.Align==Align.Right?"right": e.Align==Align.Center?"center" :null,
["urgent"] = e.Urgent?true:null,
["separator"] = e.Separator?null:false,
["separator_block_width"] = e.SeparatorBlockWidth,
["markup"] = e.Markup==Markup.Pango?"pango": null
}.RemoveNull()).ToArray());
var opt = new JsonSerializerOptions();
opt.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;
w.Write(",");
w.WriteLine(json.ToJsonString(opt));
outputCounter++;
}
}
class Program
{
#pragma warning disable 1998
static void LinuxCatch(Action f)
{
try
{
f();
}
catch (PrintAndExit e)
{
Console.Error.WriteLine(e.ToPrint.Pastel(Color.FromArgb(255,0,0)));
Environment.Exit(e.ExitCode);
}
catch (Exception e)
{
Console.Error.WriteLine(e.ToString().Pastel(Color.FromArgb(255,0,0)));
Console.Error.WriteLine(
@"
this is not supposed to happen. Please report this bug to
jirikalvoda+i3csstatus@kam.mff.cuni.cz. Thank you."
.Pastel(Color.FromArgb(255,0,0)));
Environment.Exit(134);
}
}
static int Main(string[] args)
{
var configOption = new Option<FileInfo>
("--config", "Path to configuration file. If not set configuration will be taken from ~/.config/i3/i3csstatus.conf or if there is no file, default config will be taken.");
configOption.AddAlias("-c");
var rootCommand = new RootCommand(
@"i3csstatus - Alternative generator of i3 status bar written in c#
(c) 2022 Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.");
rootCommand.AddGlobalOption(configOption);
var c_plainText = new Command("plain-text", "Print status bar as plain text.");
rootCommand.Add(c_plainText);
c_plainText.SetHandler(async (config) =>
{
LinuxCatch(() => (new StatusBarPlainText(config)).Run(Console.Out));
}, configOption);
var c_terminal = new Command("terminal", "Print status bar with terminal escape secvence.");
rootCommand.Add(c_terminal);
var c_terminal_input = new Option<bool>
("--input", "Read key input.");
c_terminal_input.AddAlias("-i");
c_terminal.Add(c_terminal_input);
c_terminal.SetHandler(async (config, input) =>
{
LinuxCatch(() => (new StatusBarTerminal(config, input)).Run(Console.Out));
}, configOption, c_terminal_input);
var c_i3 = new Command("i3", "Comunicate with i3bar.");
rootCommand.Add(c_i3);
var c_i3_input = new Option<bool>
("--input", "Read mouse clicks.");
c_i3_input.AddAlias("-i");
c_i3.Add(c_i3_input);
c_i3.SetHandler(async (config, input) =>
{
LinuxCatch(() => (new StatusBarI3(config, input)).Run(Console.Out));
}, configOption, c_i3_input);
return rootCommand.Invoke(args);
}
#pragma warning restore 1998
}