mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-03-16 13:20:52 +00:00
Merge with master
This commit is contained in:
commit
793e329fa5
13
README.md
13
README.md
@ -87,7 +87,8 @@ sunshine needs access to uinput to create mouse and gamepad events:
|
||||
- When Moonlight request you insert the correct pin on sunshine:
|
||||
- Type in the URL bar of your browser: `https://xxx.xxx.xxx.xxx:47990` where `xxx.xxx.xxx.xxx` is the IP address of your computer
|
||||
- Ignore any warning given by your browser about "insecure website"
|
||||
- Type in the username and password shown the first time you run Sunshine
|
||||
- You should compile the next page with a new username and a password, needed to login into the next step
|
||||
- Press "Save" and log in using the credentials given above
|
||||
- Go to "PIN" in the Header
|
||||
- Type in your PIN and press Enter, you should get a Success Message
|
||||
- Click on one of the Applications listed
|
||||
@ -99,16 +100,6 @@ All shortcuts start with CTRL + ALT + SHIFT, just like Moonlight
|
||||
- CTRL + ALT + SHIFT + N --> Hide/Unhide the cursor (This may be usefull for Remote Desktop Mode for Moonlight)
|
||||
- CTRL + ALT + SHIFT + F1/F13 --> Switch to different monitor for Streaming
|
||||
|
||||
## Note:
|
||||
- The Windows key is not passed through by Moonlight, therefore Sunshine maps Right-Alt key to the Windows key
|
||||
- If you set Video Bitrate to 0.5Mb/s:
|
||||
- Sunshine will use CRF or QP to controll the quality of the stream. (See example configuration file for more details)
|
||||
- This is less CPU intensive and it has lower average bandwith requirements compared to manually setting bitrate to acceptable quality
|
||||
- However, it has higher peak bitrates, forcing Sunshine to drop entire frames when streaming 1080P due to their size.
|
||||
- When this happens, the video portion of the stream appears to be frozen.
|
||||
- This is rare enough that using this for the desktop environment is tolerable (in my opinion), however for gaming not so much.
|
||||
|
||||
|
||||
## Credits:
|
||||
- [Simple-Web-Server](https://gitlab.com/eidheim/Simple-Web-Server)
|
||||
- [Moonlight](https://github.com/moonlight-stream)
|
||||
|
@ -77,6 +77,20 @@
|
||||
# 3840x1600,
|
||||
# ]
|
||||
|
||||
# Sometimes it may be usefull to map keybindings.
|
||||
# Wayland won't allow clients to capture the Win Key for example
|
||||
#
|
||||
# See https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
#
|
||||
# Note:
|
||||
# keybindings needs to have a multiple of two elements
|
||||
# keybindings = [
|
||||
# 0x10, 0xA0,
|
||||
# 0x11, 0xA2,
|
||||
# 0x12, 0xA4,
|
||||
# 0x4A, 0x4B
|
||||
# ]
|
||||
|
||||
# How long to wait in milliseconds for data from moonlight before shutting down the stream
|
||||
# ping_timeout = 10000
|
||||
|
||||
|
@ -111,6 +111,18 @@
|
||||
are supported.
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mapping Key AltRight to Key Windows -->
|
||||
<div class="mb-3">
|
||||
<label for="mapkey" class="form-label">Map Right Alt key to Windows key</label>
|
||||
<select id="mapkey" class="form-select" v-model="config.key_rightalt_to_key_win">
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="enabled">Enabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
It may be possible that you cannot send the Windows Key from Moonlight directly.<br />
|
||||
In those cases it may be usefull to make Sunshine think the Right Alt key is the Windows key
|
||||
</div>
|
||||
</div>
|
||||
<!--Files Tab-->
|
||||
<div v-if="currentTab === 'files'" class="config-page">
|
||||
@ -533,6 +545,7 @@
|
||||
delete this.config.status;
|
||||
delete this.config.platform;
|
||||
//Populate default values if not present in config
|
||||
this.config.key_rightalt_to_key_win = this.config.key_rightalt_to_key_win || "disabled";
|
||||
this.config.gamepad = this.config.gamepad || 'x360';
|
||||
this.config.upnp = this.config.upnp || 'disabled';
|
||||
this.config.min_log_level = this.config.min_log_level || 2;
|
||||
@ -561,6 +574,7 @@
|
||||
let nl = this.config === 'windows' ? "\r\n" : "\n";
|
||||
this.config.resolutions = "[" + nl + " " + this.resolutions.join("," + nl + " ") + nl + "]";
|
||||
this.config.fps = JSON.stringify(this.fps);
|
||||
|
||||
fetch("/api/config", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(this.config)
|
||||
@ -588,4 +602,4 @@
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</style>
|
17
assets/web/header-no-nav.html
Normal file
17
assets/web/header-no-nav.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sunshine</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-p34f1UUtsS3wqzfto5wAAmdvj+osOnFyQFpp4Ua3gs/ZVWx6oOypYoCJhGGScy+8" crossorigin="anonymous">
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
69
assets/web/welcome.html
Normal file
69
assets/web/welcome.html
Normal file
@ -0,0 +1,69 @@
|
||||
<main role="main" id="app" style="max-width: 600px;margin: 0 auto;">
|
||||
<div class="container-parent">
|
||||
<div class="container py-3">
|
||||
<h1 class="mb-0">Welcome to Sunshine!</h1>
|
||||
<p class="mb-0 align-self-start">Before Getting Started, write down below these credentials</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-warning">These Credentials down below are needed to access the rest of the application.<br> Keep them safe, since <b>you will never see them again!</b></div>
|
||||
<form @submit.prevent="save" class="card p-4" style="width: 100%;">
|
||||
<div class="mb-2">
|
||||
<label for="" class="form-label">Username: </label>
|
||||
<input type="text" class="form-control" v-model="passwordData.newUsername">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="" class="form-label">Password: </label>
|
||||
<input type="password" class="form-control" v-model="passwordData.newPassword">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="" class="form-label">Password: </label>
|
||||
<input type="password" class="form-control" v-model="passwordData.confirmNewPassword">
|
||||
</div>
|
||||
<button class="mb-2 btn btn-primary" style="margin: 1em auto;">Login</button>
|
||||
<div class="alert alert-danger" v-if="error"><b>Error: </b>{{error}}</div>
|
||||
<div class="alert alert-success" v-if="success"><b>Success! </b>This page will reload soon, your browser will ask you for the new credentials</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
success: false,
|
||||
passwordData: {
|
||||
newUsername: 'sunshine',
|
||||
newPassword: '',
|
||||
confirmNewPassword: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.error = null;
|
||||
fetch("/api/password", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(this.passwordData)
|
||||
}).then((r) => {
|
||||
if (r.status == 200){
|
||||
r.json().then((rj) => {
|
||||
if(rj.status.toString() === "true"){
|
||||
this.success = true;
|
||||
setTimeout(()=>{
|
||||
document.location.reload();
|
||||
},5000);
|
||||
} else {
|
||||
this.error = rj.error;
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.error = "Internal Server Error"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
@ -210,6 +210,11 @@ nvhttp_t nvhttp {
|
||||
};
|
||||
|
||||
input_t input {
|
||||
{
|
||||
{ 0x10, 0xA0 },
|
||||
{ 0x11, 0xA2 },
|
||||
{ 0x12, 0xA4 },
|
||||
},
|
||||
2s, // back_button_timeout
|
||||
500ms, // key_repeat_delay
|
||||
std::chrono::duration<double> { 1 / 24.9 }, // key_repeat_period
|
||||
@ -399,8 +404,20 @@ void int_f(std::unordered_map<std::string, std::string> &vars, const std::string
|
||||
return;
|
||||
}
|
||||
|
||||
auto &val = it->second;
|
||||
input = util::from_chars(&val[0], &val[0] + val.size());
|
||||
std::string_view val = it->second;
|
||||
|
||||
// If value is something like: "756" instead of 756
|
||||
if(val.size() >= 2 && val[0] == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
}
|
||||
|
||||
// If that integer is in hexadecimal
|
||||
if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
|
||||
input = util::from_hex<int>(val.substr(2));
|
||||
}
|
||||
else {
|
||||
input = util::from_view(val);
|
||||
}
|
||||
|
||||
vars.erase(it);
|
||||
}
|
||||
@ -412,8 +429,20 @@ void int_f(std::unordered_map<std::string, std::string> &vars, const std::string
|
||||
return;
|
||||
}
|
||||
|
||||
auto &val = it->second;
|
||||
input = util::from_chars(&val[0], &val[0] + val.size());
|
||||
std::string_view val = it->second;
|
||||
|
||||
// If value is something like: "756" instead of 756
|
||||
if(val.size() >= 2 && val[0] == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
}
|
||||
|
||||
// If that integer is in hexadecimal
|
||||
if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
|
||||
input = util::from_hex<int>(val.substr(2));
|
||||
}
|
||||
else {
|
||||
input = util::from_view(val);
|
||||
}
|
||||
|
||||
vars.erase(it);
|
||||
}
|
||||
@ -545,7 +574,42 @@ void list_int_f(std::unordered_map<std::string, std::string> &vars, const std::s
|
||||
list_string_f(vars, name, list);
|
||||
|
||||
for(auto &el : list) {
|
||||
input.emplace_back(util::from_view(el));
|
||||
std::string_view val = el;
|
||||
|
||||
// If value is something like: "756" instead of 756
|
||||
if(val.size() >= 2 && val[0] == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
}
|
||||
|
||||
int tmp;
|
||||
|
||||
// If the integer is a hexadecimal
|
||||
if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
|
||||
tmp = util::from_hex<int>(val.substr(2));
|
||||
}
|
||||
else {
|
||||
tmp = util::from_view(val);
|
||||
}
|
||||
input.emplace_back(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
void map_int_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::unordered_map<int, int> &input) {
|
||||
std::vector<int> list;
|
||||
list_int_f(vars, name, list);
|
||||
|
||||
// The list needs to be a multiple of 2
|
||||
if(list.size() % 2) {
|
||||
std::cout << "Warning: expected "sv << name << " to have a multiple of two elements --> not "sv << list.size() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
int x = 0;
|
||||
while(x < list.size()) {
|
||||
auto key = list[x++];
|
||||
auto val = list[x++];
|
||||
|
||||
input.emplace(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
@ -633,6 +697,17 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
|
||||
path_f(vars, "file_apps", stream.file_apps);
|
||||
int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 255 });
|
||||
|
||||
map_int_int_f(vars, "keybindings"s, input.keybindings);
|
||||
|
||||
// This config option will only be used by the UI
|
||||
// When editing in the config file itself, use "keybindings"
|
||||
bool map_rightalt_to_win = false;
|
||||
bool_f(vars, "key_rightalt_to_key_win", map_rightalt_to_win);
|
||||
|
||||
if(map_rightalt_to_win) {
|
||||
input.keybindings.emplace(0xA5, 0x5B);
|
||||
}
|
||||
|
||||
to = std::numeric_limits<int>::min();
|
||||
int_f(vars, "back_button_timeout", to);
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
namespace config {
|
||||
struct video_t {
|
||||
// ffmpeg params
|
||||
int qp; // higher == more compression and less quality
|
||||
int qp; // higher == more compression and less quality
|
||||
|
||||
int hevc_mode;
|
||||
|
||||
@ -73,6 +73,8 @@ struct nvhttp_t {
|
||||
};
|
||||
|
||||
struct input_t {
|
||||
std::unordered_map<int, int> keybindings;
|
||||
|
||||
std::chrono::milliseconds back_button_timeout;
|
||||
std::chrono::milliseconds key_repeat_delay;
|
||||
std::chrono::duration<double> key_repeat_period;
|
||||
|
@ -73,6 +73,15 @@ void send_unauthorized(resp_https_t response, req_https_t request) {
|
||||
response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers);
|
||||
}
|
||||
|
||||
void send_redirect(resp_https_t response, req_https_t request, const char *path) {
|
||||
auto address = request->remote_endpoint_address();
|
||||
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
|
||||
const SimpleWeb::CaseInsensitiveMultimap headers {
|
||||
{ "Location", path }
|
||||
};
|
||||
response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers);
|
||||
}
|
||||
|
||||
bool authenticate(resp_https_t response, req_https_t request) {
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto ip_type = net::from_address(address);
|
||||
@ -83,6 +92,12 @@ bool authenticate(resp_https_t response, req_https_t request) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//If credentials are shown, redirect the user to a /welcome page
|
||||
if(config::sunshine.username.empty()){
|
||||
send_redirect(response,request,"/welcome");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
send_unauthorized(response, request);
|
||||
});
|
||||
@ -185,6 +200,17 @@ void getPasswordPage(resp_https_t response, req_https_t request) {
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getWelcomePage(resp_https_t response, req_https_t request) {
|
||||
print_req(request);
|
||||
if(!config::sunshine.username.empty()){
|
||||
send_redirect(response,request,"/");
|
||||
return;
|
||||
}
|
||||
std::string header = read_file(WEB_DIR "header-no-nav.html");
|
||||
std::string content = read_file(WEB_DIR "welcome.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getApps(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
@ -371,7 +397,7 @@ void saveConfig(resp_https_t response, req_https_t request) {
|
||||
}
|
||||
|
||||
void savePassword(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
if(!config::sunshine.username.empty() && !authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
@ -390,27 +416,31 @@ void savePassword(resp_https_t response, req_https_t request) {
|
||||
try {
|
||||
//TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
auto username = inputTree.get<std::string>("currentUsername");
|
||||
auto username = inputTree.count("currentUsername") > 0 ? inputTree.get<std::string>("currentUsername") : "";
|
||||
auto newUsername = inputTree.get<std::string>("newUsername");
|
||||
auto password = inputTree.get<std::string>("currentPassword");
|
||||
auto password = inputTree.count("currentPassword") > 0 ? inputTree.get<std::string>("currentPassword") : "";
|
||||
auto newPassword = inputTree.get<std::string>("newPassword");
|
||||
auto confirmPassword = inputTree.get<std::string>("confirmNewPassword");
|
||||
if(newUsername.length() == 0) newUsername = username;
|
||||
|
||||
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
||||
if(username == config::sunshine.username && hash == config::sunshine.password) {
|
||||
if(newPassword != confirmPassword) {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Password Mismatch");
|
||||
}
|
||||
|
||||
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
|
||||
http::reload_user_creds(config::sunshine.credentials_file);
|
||||
outputTree.put("status", true);
|
||||
}
|
||||
else {
|
||||
if(newUsername.length() == 0){
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Invalid Current Credentials");
|
||||
outputTree.put("error", "Invalid Username");
|
||||
} else {
|
||||
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
||||
if(config::sunshine.username.empty() || (username == config::sunshine.username && hash == config::sunshine.password)) {
|
||||
if(newPassword != confirmPassword) {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Password Mismatch");
|
||||
} else {
|
||||
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
|
||||
http::reload_user_creds(config::sunshine.credentials_file);
|
||||
outputTree.put("status", true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Invalid Current Credentials");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
@ -467,6 +497,7 @@ void start() {
|
||||
server.resource["^/clients$"]["GET"] = getClientsPage;
|
||||
server.resource["^/config$"]["GET"] = getConfigPage;
|
||||
server.resource["^/password$"]["GET"] = getPasswordPage;
|
||||
server.resource["^/welcome$"]["GET"] = getWelcomePage;
|
||||
server.resource["^/api/pin"]["POST"] = savePin;
|
||||
server.resource["^/api/apps$"]["GET"] = getApps;
|
||||
server.resource["^/api/apps$"]["POST"] = saveApp;
|
||||
|
@ -54,15 +54,11 @@ int init() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if(!user_creds_exist(config::sunshine.credentials_file)) {
|
||||
if(save_user_creds(config::sunshine.credentials_file, "sunshine"s, crypto::rand_alphabet(16), true)) {
|
||||
return -1;
|
||||
}
|
||||
if(user_creds_exist(config::sunshine.credentials_file)) {
|
||||
if(reload_user_creds(config::sunshine.credentials_file)) return -1;
|
||||
} else {
|
||||
BOOST_LOG(info) << "Open the Web UI to set your new username and password and getting started";
|
||||
}
|
||||
if(reload_user_creds(config::sunshine.credentials_file)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -92,12 +88,6 @@ int save_user_creds(const std::string &file, const std::string &username, const
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "New credentials have been created"sv;
|
||||
|
||||
if(run_our_mouth) {
|
||||
BOOST_LOG(info) << "Username: "sv << username;
|
||||
BOOST_LOG(info) << "Password: "sv << password;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -409,13 +409,9 @@ void repeat_key(short key_code) {
|
||||
}
|
||||
|
||||
short map_keycode(short keycode) {
|
||||
switch(keycode) {
|
||||
case 0x10:
|
||||
return 0xA0;
|
||||
case 0x11:
|
||||
return 0xA2;
|
||||
case 0x12:
|
||||
return 0xA4;
|
||||
auto it = config::input.keybindings.find(keycode);
|
||||
if(it != std::end(config::input.keybindings)) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
return keycode;
|
||||
|
@ -190,7 +190,7 @@ stream::launch_session_t make_launch_session(bool host_audio, const args_t &args
|
||||
stream::launch_session_t launch_session;
|
||||
|
||||
launch_session.host_audio = host_audio;
|
||||
launch_session.gcm_key = *util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
|
||||
launch_session.gcm_key = util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
|
||||
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(args.at("rikeyid"s)));
|
||||
auto prepend_iv_p = (uint8_t *)&prepend_iv;
|
||||
|
||||
@ -211,7 +211,7 @@ void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin
|
||||
|
||||
auto salt = util::from_hex<std::array<uint8_t, 16>>(salt_view, true);
|
||||
|
||||
auto key = crypto::gen_aes_key(*salt, pin);
|
||||
auto key = crypto::gen_aes_key(salt, pin);
|
||||
sess.cipher_key = std::make_unique<crypto::aes_t>(key);
|
||||
|
||||
tree.put("root.paired", 1);
|
||||
|
@ -720,6 +720,42 @@ std::shared_ptr<display_t> display(platf::mem_type_e hwdevice_type, int framerat
|
||||
return x11_disp;
|
||||
}
|
||||
|
||||
std::vector<std::string> display_names() {
|
||||
if(xcb::init_shm() || xcb::init() || x11::init() || x11::rr::init() || x11::fix::init()) {
|
||||
BOOST_LOG(error) << "Couldn't init x11 libraries"sv;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Detecting connected monitors"sv;
|
||||
|
||||
xdisplay_t xdisplay { x11::OpenDisplay(nullptr) };
|
||||
if(!xdisplay) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto xwindow = DefaultRootWindow(xdisplay.get());
|
||||
screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) };
|
||||
int output = screenr->noutput;
|
||||
|
||||
int monitor = 0;
|
||||
for(int x = 0; x < output; ++x) {
|
||||
output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) };
|
||||
if(out_info && out_info->connection == RR_Connected) {
|
||||
++monitor;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> names;
|
||||
names.reserve(monitor);
|
||||
|
||||
for(auto x = 0; x < monitor; ++x) {
|
||||
names.emplace_back(std::to_string(x));
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
void freeImage(XImage *p) {
|
||||
XDestroyImage(p);
|
||||
}
|
||||
|
@ -268,11 +268,12 @@ std::string hex_vec(C &&c, bool rev = false) {
|
||||
}
|
||||
|
||||
template<class T>
|
||||
std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
|
||||
T from_hex(const std::string_view &hex, bool rev = false) {
|
||||
std::uint8_t buf[sizeof(T)];
|
||||
|
||||
static char constexpr shift_bit = 'a' - 'A';
|
||||
auto is_convertable = [](char ch) -> bool {
|
||||
|
||||
auto is_convertable = [](char ch) -> bool {
|
||||
if(isdigit(ch)) {
|
||||
return true;
|
||||
}
|
||||
@ -287,9 +288,7 @@ std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
|
||||
};
|
||||
|
||||
auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2;
|
||||
if(buf_size != sizeof(T)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto padding = sizeof(T) - buf_size;
|
||||
|
||||
const char *data = hex.data() + hex.size() - 1;
|
||||
|
||||
@ -301,7 +300,9 @@ std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
|
||||
return (std::uint8_t)(ch | (char)32) - 'a' + (char)10;
|
||||
};
|
||||
|
||||
for(auto &el : buf) {
|
||||
std::fill_n(buf + buf_size, padding, 0);
|
||||
|
||||
std::for_each_n(buf, buf_size, [&](auto &el) {
|
||||
while(!is_convertable(*data)) { --data; }
|
||||
std::uint8_t ch_r = convert(*data--);
|
||||
|
||||
@ -309,7 +310,7 @@ std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
|
||||
std::uint8_t ch_l = convert(*data--);
|
||||
|
||||
el = (ch_l << 4) | ch_r;
|
||||
}
|
||||
});
|
||||
|
||||
if(rev) {
|
||||
std::reverse(std::begin(buf), std::end(buf));
|
||||
|
Loading…
x
Reference in New Issue
Block a user