Remember last session for each user (closes #619)

Signed-off-by: AnErrupTion <anerruption@disroot.org>
This commit is contained in:
AnErrupTion 2025-10-14 21:05:54 +02:00
parent aef1dd9c1a
commit b3f1e91cf6
No known key found for this signature in database
9 changed files with 324 additions and 92 deletions

98
res/init.sh Normal file
View File

@ -0,0 +1,98 @@
#!/bin/sh
# Styling options
local BOLD = 0x01000000
local UNDERLINE = 0x02000000
local REVERSE = 0x04000000
local ITALIC = 0x08000000
local BLINK = 0x10000000
local HI_BLACK = 0x20000000
local BRIGHT = 0x40000000
local DIM = 0x80000000
# Common colors
local DEFAULT = 0x00000000
local RED = 0x00FF0000
local GREEN = 0x0000FF00
local YELLOW = 0x00FFFF00
local BLUE = 0x000000FF
local MAGENTA = 0x00FF00FF
local CYAN = 0x0000FFFF
local WHITE = 0x00FFFFFF
source lang/en.sh # From lang
# It'd be a good idea to condense multiple options into 1 command (e.g.
# animation settings)
ly set-config allow_empty_password true
ly set-config animation none
ly set-config animation_timeout_sec 0
ly set-config asterisk "*"
ly set-config auth_fails 10
ly set-config bg $DEFAULT
ly set-config bigclock_12hr false
ly set-config bigclock_seconds false
ly set-config blank_box true
ly set-config border_fg $WHITE
ly set-config box_title null
ly set-config clear_password false
ly set-config clock null
ly set-config cmatrix_fg $GREEN
ly set-config cmatrix_head_col $(($WHITE | $BOLD))
ly set-config cmatrix_min_codepoint 0x21
ly set-config cmatrix_max_codepoint = 0x7B
ly set-config colormix_col1 $RED
ly set-config colormix_col2 $BLUE
ly set-config colormix_col3 $HI_BLACK
ly set-config default_input login
ly set-config doom_fire_height 6
ly set-config doom_fire_spread 2
ly set-config doom_top_color 0x009F2707
ly set-config doom_middle_color 0x00C78F17
ly set-config doom_bottom_color $WHITE
ly set-config error_bg $DEFAULT
ly set-config error_fg $(($RED | $BOLD))
ly set-config fg $WHITE
ly set-config full_color true
ly set-config gameoflife_entropy_interval 10
ly set-config gameoflife_fg $GREEN
ly set-config gameoflife_frame_delay 6
ly set-config gameoflife_initial_density 0.4
ly set-config hide_borders false
ly set-config initial_info_text null
ly set-config input_len 34
ly set-config login_cmd null
ly set-config login_defs_path "/etc/login.defs"
ly set-config logout_cmd null
ly set-config ly_log "/var/log/ly.log"
ly set-config margin_box_h 2
ly set-config margin_box_v 1
ly set-config min_refresh_delta 5
ly set-config numlock false
ly set-config path "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
ly set-config save true
ly set-config service_name ly
ly set-config session_log "ly-session.log"
ly set-config setup_cmd "$CONFIG_DIRECTORY/ly/setup.sh"
ly set-config text_in_center false
ly set-config vi_default_mode normal
ly set-config vi_mode false
ly set-config x_cmd "$PREFIX_DIRECTORY/bin/X"
ly set-config xauth_cmd "$PREFIX_DIRECTORY/bin/xauth"
# Replaces respective options
# X11 requires special support from the display manager, which is why a separate
# command is required
ly add-session "System shell" shell "/bin/sh"
ly add-x11-session xinitrc x11 "~/.xinitrc"
# ly add-session-dir custom "$CONFIG_DIRECTORY/ly/custom-sessions"
ly add-session-dir wayland "$PREFIX_DIRECTORY/share/wayland-sessions"
ly add-x11-session-dir x11 "$PREFIX_DIRECTORY/share/xsessions"
ly add-hud 0 0 "Ly version %VERSION" null null top left
ly add-hud 1 0 "F1 shutdown" F1 "/sbin/shutdown -a now" top left
ly add-hud 2 0 "F2 reboot" F2 "/sbin/shutdown -r now" top left
#ly add-hud 0 0 "F3 sleep" F3 null top left
ly add-hud 3 0 "F5 decrease brightness" F5 "$PREFIX_DIRECTORY/bin/brightnessctl -q s 10%-" top left
ly add-hud 4 0 "F6 increase brightness" F6 "$PREFIX_DIRECTORY/bin/brightnessctl -q s +10%" top left
ly add-hud 5 0 "%CLOCK" null null top right

22
src/config/SavedUsers.zig Normal file
View File

@ -0,0 +1,22 @@
const std = @import("std");
const SavedUsers = @This();
const User = struct {
username: []const u8,
session_index: usize,
};
user_list: std.ArrayList(User),
last_username_index: ?usize,
pub fn init() SavedUsers {
return .{
.user_list = .empty,
.last_username_index = null,
};
}
pub fn deinit(self: *SavedUsers, allocator: std.mem.Allocator) void {
self.user_list.deinit(allocator);
}

View File

@ -5,7 +5,8 @@
const std = @import("std");
const ini = @import("zigini");
const Config = @import("Config.zig");
const Save = @import("Save.zig");
const OldSave = @import("OldSave.zig");
const SavedUsers = @import("SavedUsers.zig");
const TerminalBuffer = @import("../tui/TerminalBuffer.zig");
const Color = TerminalBuffer.Color;
@ -186,8 +187,8 @@ pub fn lateConfigFieldHandler(config: *Config) void {
}
}
pub fn tryMigrateSaveFile(user_buf: *[32]u8) Save {
var save = Save{};
pub fn tryMigrateFirstSaveFile(user_buf: *[32]u8) OldSave {
var save = OldSave{};
if (maybe_save_file) |path| {
defer temporary_allocator.free(path);
@ -216,3 +217,29 @@ pub fn tryMigrateSaveFile(user_buf: *[32]u8) Save {
return save;
}
pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, save_ini: *ini.Ini(OldSave), path: []const u8, saved_users: *SavedUsers, usernames: [][]const u8) !bool {
var old_save_file_exists = true;
var user_buf: [32]u8 = undefined;
const save = save_ini.readFileToStruct(path, .{
.fieldHandler = null,
.comment_characters = "#",
}) catch no_save_file: {
old_save_file_exists = false;
break :no_save_file tryMigrateFirstSaveFile(&user_buf);
};
if (!old_save_file_exists) return false;
// Add all other users to the list
for (usernames, 0..) |username, i| {
if (save.user) |user| {
if (std.mem.eql(u8, user, username)) saved_users.last_username_index = i;
}
try saved_users.user_list.append(allocator, .{ .username = username, .session_index = save.session_index orelse 0 });
}
return true;
}

View File

@ -21,7 +21,8 @@ const InfoLine = @import("tui/components/InfoLine.zig");
const UserList = @import("tui/components/UserList.zig");
const Config = @import("config/Config.zig");
const Lang = @import("config/Lang.zig");
const Save = @import("config/Save.zig");
const OldSave = @import("config/OldSave.zig");
const SavedUsers = @import("config/SavedUsers.zig");
const migrator = @import("config/migrator.zig");
const SharedError = @import("SharedError.zig");
const UidRange = @import("UidRange.zig");
@ -118,11 +119,14 @@ pub fn main() !void {
var config: Config = undefined;
var lang: Lang = undefined;
var save: Save = undefined;
var old_save_file_exists = false;
var maybe_config_load_error: ?anyerror = null;
var can_get_lock_state = true;
var can_draw_clock = true;
var saved_users = SavedUsers.init();
defer saved_users.deinit(allocator);
if (res.args.help != 0) {
try clap.help(stderr, clap.Help, &params, .{});
@ -143,13 +147,15 @@ pub fn main() !void {
var lang_ini = Ini(Lang).init(allocator);
defer lang_ini.deinit();
var save_ini = Ini(Save).init(allocator);
defer save_ini.deinit();
var old_save_ini = ini.Ini(OldSave).init(allocator);
defer old_save_ini.deinit();
var save_path: []const u8 = build_options.config_directory ++ "/ly/save.ini";
var save_path: []const u8 = build_options.config_directory ++ "/ly/save.txt";
var old_save_path: []const u8 = build_options.config_directory ++ "/ly/save.ini";
var save_path_alloc = false;
defer {
if (save_path_alloc) allocator.free(save_path);
if (save_path_alloc) allocator.free(old_save_path);
}
const comment_characters = "#";
@ -178,18 +184,9 @@ pub fn main() !void {
}) catch Lang{};
if (config.save) {
save_path = try std.fmt.allocPrint(allocator, "{s}{s}save.ini", .{ s, trailing_slash });
save_path = try std.fmt.allocPrint(allocator, "{s}{s}save.txt", .{ s, trailing_slash });
old_save_path = try std.fmt.allocPrint(allocator, "{s}{s}save.ini", .{ s, trailing_slash });
save_path_alloc = true;
var user_buf: [32]u8 = undefined;
save = save_ini.readFileToStruct(save_path, .{
.fieldHandler = null,
.comment_characters = comment_characters,
}) catch migrator.tryMigrateSaveFile(&user_buf);
}
if (maybe_config_load_error == null) {
migrator.lateConfigFieldHandler(&config);
}
} else {
const config_path = build_options.config_directory ++ "/ly/config.ini";
@ -210,17 +207,47 @@ pub fn main() !void {
.fieldHandler = null,
.comment_characters = comment_characters,
}) catch Lang{};
}
if (config.save) {
var user_buf: [32]u8 = undefined;
save = save_ini.readFileToStruct(save_path, .{
.fieldHandler = null,
.comment_characters = comment_characters,
}) catch migrator.tryMigrateSaveFile(&user_buf);
}
if (maybe_config_load_error == null) {
migrator.lateConfigFieldHandler(&config);
}
if (maybe_config_load_error == null) {
migrator.lateConfigFieldHandler(&config);
var usernames = try getAllUsernames(allocator, config.login_defs_path);
defer {
for (usernames.items) |username| allocator.free(username);
usernames.deinit(allocator);
}
if (config.save) read_save_file: {
old_save_file_exists = migrator.tryMigrateIniSaveFile(allocator, &old_save_ini, old_save_path, &saved_users, usernames.items) catch break :read_save_file;
// Don't read the new save file if the old one still exists
if (old_save_file_exists) break :read_save_file;
var save_file = std.fs.cwd().openFile(save_path, .{}) catch break :read_save_file;
defer save_file.close();
var file_buffer: [256]u8 = undefined;
var file_reader = save_file.reader(&file_buffer);
var reader = &file_reader.interface;
const last_username_index_str = reader.takeDelimiterInclusive('\n') catch break :read_save_file;
saved_users.last_username_index = std.fmt.parseInt(usize, last_username_index_str[0..(last_username_index_str.len - 1)], 10) catch break :read_save_file;
while (reader.seek < reader.buffer.len) {
const line = reader.takeDelimiterInclusive('\n') catch break;
var user = std.mem.splitScalar(u8, line[0..(line.len - 1)], ':');
const username = user.next() orelse continue;
const session_index_str = user.next() orelse continue;
const session_index = std.fmt.parseInt(usize, session_index_str, 10) catch continue;
try saved_users.user_list.append(allocator, .{
.username = username,
.session_index = session_index,
});
}
}
@ -345,7 +372,9 @@ pub fn main() !void {
try log_writer.print("failed to set numlock: {s}\n", .{@errorName(err)});
};
var session = Session.init(allocator, &buffer);
var login: UserList = undefined;
var session = Session.init(allocator, &buffer, &login);
defer session.deinit();
addOtherEnvironment(&session, lang, .shell, null) catch |err| {
@ -396,12 +425,6 @@ pub fn main() !void {
try crawl(&session, lang, dir, .custom);
}
var usernames = try getAllUsernames(allocator, config.login_defs_path);
defer {
for (usernames.items) |username| allocator.free(username);
usernames.deinit(allocator);
}
if (usernames.items.len == 0) {
// If we have no usernames, simply add an error to the info line.
// This effectively means you can't login, since there would be no local
@ -411,7 +434,7 @@ pub fn main() !void {
try log_writer.writeAll("no users found\n");
}
var login = try UserList.init(allocator, &buffer, usernames);
login = try UserList.init(allocator, &buffer, usernames, &saved_users, &session);
defer login.deinit();
var password = Text.init(allocator, &buffer, true, config.asterisk);
@ -422,23 +445,21 @@ pub fn main() !void {
// Load last saved username and desktop selection, if any
if (config.save) {
if (save.user) |user| {
if (saved_users.last_username_index) |index| {
const user = saved_users.user_list.items[index];
// Find user with saved name, and switch over to it
// If it doesn't exist (anymore), we don't change the value
// Note that we could instead save the username index, but migrating
// from the raw username to an index is non-trivial and I'm lazy :P
for (usernames.items, 0..) |username, i| {
if (std.mem.eql(u8, username, user)) {
if (std.mem.eql(u8, username, user.username)) {
login.label.current = i;
break;
}
}
active_input = .password;
}
if (save.session_index) |session_index| {
if (session_index < session.label.list.items.len) session.label.current = session_index;
if (user.session_index < session.label.list.items.len) session.label.current = user.session_index;
}
}
@ -853,22 +874,33 @@ pub fn main() !void {
_ = termbox.tb_present();
if (config.save) save_last_settings: {
var file = std.fs.cwd().createFile(save_path, .{}) catch break :save_last_settings;
// It isn't worth cluttering the code with precise error
// handling, so let's just report a generic error message,
// that should be good enough for debugging anyway.
errdefer log_writer.writeAll("failed to save current user data\n") catch {};
var file = std.fs.cwd().createFile(save_path, .{}) catch |err| {
log_writer.print("failed to create save file: {s}\n", .{@errorName(err)}) catch break :save_last_settings;
break :save_last_settings;
};
defer file.close();
var file_buffer: [64]u8 = undefined;
var file_buffer: [256]u8 = undefined;
var file_writer = file.writer(&file_buffer);
var writer = &file_writer.interface;
const save_data = Save{
.user = login.getCurrentUser(),
.session_index = session.label.current,
};
ini.writeFromStruct(save_data, writer, null, .{}) catch break :save_last_settings;
try writer.print("{d}\n", .{login.label.current});
for (saved_users.user_list.items) |user| {
try writer.print("{s}:{d}\n", .{ user.username, user.session_index });
}
try writer.flush();
// Delete previous save file if it exists
if (migrator.maybe_save_file) |path| std.fs.cwd().deleteFile(path) catch {};
if (migrator.maybe_save_file) |path| {
std.fs.cwd().deleteFile(path) catch {};
} else if (old_save_file_exists) {
std.fs.cwd().deleteFile(old_save_path) catch {};
}
}
var shared_err = try SharedError.init();
@ -877,7 +909,7 @@ pub fn main() !void {
{
session_pid = try std.posix.fork();
if (session_pid == 0) {
const current_environment = session.label.list.items[session.label.current];
const current_environment = session.label.list.items[session.label.current].environment;
const auth_options = auth.AuthOptions{
.tty = active_tty,
.service_name = config.service_name,
@ -898,7 +930,7 @@ pub fn main() !void {
};
std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null);
auth.authenticate(allocator, log_writer, auth_options, current_environment, login.getCurrentUser(), password.text.items) catch |err| {
auth.authenticate(allocator, log_writer, auth_options, current_environment, login.getCurrentUsername(), password.text.items) catch |err| {
shared_err.writeError(err);
std.process.exit(1);
};
@ -1026,7 +1058,7 @@ fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplaySer
.name = name,
.xdg_session_desktop = null,
.xdg_desktop_names = null,
.cmd = exec orelse "",
.cmd = exec,
.specifier = lang.other,
.display_server = display_server,
.is_terminal = display_server == .shell,

View File

@ -4,7 +4,7 @@ const generic = @import("generic.zig");
const Allocator = std.mem.Allocator;
const MessageLabel = generic.CyclableLabel(Message);
const MessageLabel = generic.CyclableLabel(Message, Message);
const InfoLine = @This();
@ -19,7 +19,7 @@ label: MessageLabel,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer) InfoLine {
return .{
.label = MessageLabel.init(allocator, buffer, drawItem),
.label = MessageLabel.init(allocator, buffer, drawItem, null, null),
};
}

View File

@ -3,41 +3,53 @@ const TerminalBuffer = @import("../TerminalBuffer.zig");
const enums = @import("../../enums.zig");
const Environment = @import("../../Environment.zig");
const generic = @import("generic.zig");
const UserList = @import("UserList.zig");
const Allocator = std.mem.Allocator;
const DisplayServer = enums.DisplayServer;
const EnvironmentLabel = generic.CyclableLabel(Environment);
const Env = struct {
environment: Environment,
index: usize,
};
const EnvironmentLabel = generic.CyclableLabel(Env, *UserList);
const Session = @This();
label: EnvironmentLabel,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer) Session {
pub fn init(allocator: Allocator, buffer: *TerminalBuffer, user_list: *UserList) Session {
return .{
.label = EnvironmentLabel.init(allocator, buffer, drawItem),
.label = EnvironmentLabel.init(allocator, buffer, drawItem, sessionChanged, user_list),
};
}
pub fn deinit(self: *Session) void {
for (self.label.list.items) |*environment| {
if (environment.entry_ini) |*entry_ini| entry_ini.deinit();
for (self.label.list.items) |*env| {
if (env.environment.entry_ini) |*entry_ini| entry_ini.deinit();
}
self.label.deinit();
}
pub fn addEnvironment(self: *Session, environment: Environment) !void {
try self.label.addItem(environment);
try self.label.addItem(.{ .environment = environment, .index = self.label.list.items.len });
}
fn drawItem(label: *EnvironmentLabel, environment: Environment, x: usize, y: usize) bool {
const length = @min(environment.name.len, label.visible_length - 3);
fn sessionChanged(env: Env, maybe_user_list: ?*UserList) void {
if (maybe_user_list) |user_list| {
user_list.label.list.items[user_list.label.current].session_index.* = env.index;
}
}
fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize) bool {
const length = @min(env.environment.name.len, label.visible_length - 3);
if (length == 0) return false;
const nx = if (label.text_in_center) (label.x + (label.visible_length - environment.name.len) / 2) else (label.x + 2);
label.first_char_x = nx + environment.name.len;
const nx = if (label.text_in_center) (label.x + (label.visible_length - env.environment.name.len) / 2) else (label.x + 2);
label.first_char_x = nx + env.environment.name.len;
label.buffer.drawLabel(environment.specifier, x, y);
label.buffer.drawLabel(environment.name, nx, label.y);
label.buffer.drawLabel(env.environment.specifier, x, y);
label.buffer.drawLabel(env.environment.name, nx, label.y);
return true;
}

View File

@ -1,45 +1,83 @@
const std = @import("std");
const TerminalBuffer = @import("../TerminalBuffer.zig");
const generic = @import("generic.zig");
const Session = @import("Session.zig");
const SavedUsers = @import("../../config/SavedUsers.zig");
const StringList = std.ArrayListUnmanaged([]const u8);
const Allocator = std.mem.Allocator;
const UsernameText = generic.CyclableLabel([]const u8);
pub const User = struct {
name: []const u8,
session_index: *usize,
allocated_index: bool,
};
const UserLabel = generic.CyclableLabel(User, *Session);
const UserList = @This();
label: UsernameText,
label: UserLabel,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList) !UserList {
pub fn init(allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList, saved_users: *SavedUsers, session: *Session) !UserList {
var userList = UserList{
.label = UsernameText.init(allocator, buffer, drawItem),
.label = UserLabel.init(allocator, buffer, drawItem, usernameChanged, session),
};
for (usernames.items) |username| {
if (username.len == 0) continue;
try userList.label.addItem(username);
var maybe_session_index: ?*usize = null;
for (saved_users.user_list.items) |*saved_user| {
if (std.mem.eql(u8, username, saved_user.username)) {
maybe_session_index = &saved_user.session_index;
break;
}
}
var allocated_index = false;
if (maybe_session_index == null) {
maybe_session_index = try allocator.create(usize);
maybe_session_index.?.* = 0;
allocated_index = true;
}
try userList.label.addItem(.{
.name = username,
.session_index = maybe_session_index.?,
.allocated_index = allocated_index,
});
}
return userList;
}
pub fn deinit(self: *UserList) void {
for (self.label.list.items) |user| {
if (user.allocated_index) {
self.label.allocator.destroy(user.session_index);
}
}
self.label.deinit();
}
pub fn getCurrentUser(self: UserList) []const u8 {
return self.label.list.items[self.label.current];
pub fn getCurrentUsername(self: UserList) []const u8 {
return self.label.list.items[self.label.current].name;
}
fn drawItem(label: *UsernameText, username: []const u8, _: usize, _: usize) bool {
const length = @min(username.len, label.visible_length - 3);
fn usernameChanged(user: User, maybe_session: ?*Session) void {
if (maybe_session) |session| {
session.label.current = user.session_index.*;
}
}
fn drawItem(label: *UserLabel, user: User, _: usize, _: usize) bool {
const length = @min(user.name.len, label.visible_length - 3);
if (length == 0) return false;
const x = if (label.text_in_center) (label.x + (label.visible_length - username.len) / 2) else (label.x + 2);
label.first_char_x = x + username.len;
const x = if (label.text_in_center) (label.x + (label.visible_length - user.name.len) / 2) else (label.x + 2);
label.first_char_x = x + user.name.len;
label.buffer.drawLabel(username, x, label.y);
label.buffer.drawLabel(user.name, x, label.y);
return true;
}

View File

@ -2,11 +2,12 @@ const std = @import("std");
const interop = @import("../../interop.zig");
const TerminalBuffer = @import("../TerminalBuffer.zig");
pub fn CyclableLabel(comptime ItemType: type) type {
pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) type {
return struct {
const Allocator = std.mem.Allocator;
const ItemList = std.ArrayListUnmanaged(ItemType);
const DrawItemFn = *const fn (*Self, ItemType, usize, usize) bool;
const ChangeItemFn = *const fn (ItemType, ?ChangeItemType) void;
const termbox = interop.termbox;
@ -22,8 +23,10 @@ pub fn CyclableLabel(comptime ItemType: type) type {
first_char_x: usize,
text_in_center: bool,
draw_item_fn: DrawItemFn,
change_item_fn: ?ChangeItemFn,
change_item_arg: ?ChangeItemType,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer, draw_item_fn: DrawItemFn) Self {
pub fn init(allocator: Allocator, buffer: *TerminalBuffer, draw_item_fn: DrawItemFn, change_item_fn: ?ChangeItemFn, change_item_arg: ?ChangeItemType) Self {
return .{
.allocator = allocator,
.buffer = buffer,
@ -35,6 +38,8 @@ pub fn CyclableLabel(comptime ItemType: type) type {
.first_char_x = 0,
.text_in_center = false,
.draw_item_fn = draw_item_fn,
.change_item_fn = change_item_fn,
.change_item_arg = change_item_arg,
};
}
@ -94,21 +99,19 @@ pub fn CyclableLabel(comptime ItemType: type) type {
}
fn goLeft(self: *Self) void {
if (self.current == 0) {
self.current = self.list.items.len - 1;
return;
}
self.current = if (self.current == 0) self.list.items.len - 1 else self.current - 1;
self.current -= 1;
if (self.change_item_fn) |change_item_fn| {
@call(.auto, change_item_fn, .{ self.list.items[self.current], self.change_item_arg });
}
}
fn goRight(self: *Self) void {
if (self.current == self.list.items.len - 1) {
self.current = 0;
return;
}
self.current = if (self.current == self.list.items.len - 1) 0 else self.current + 1;
self.current += 1;
if (self.change_item_fn) |change_item_fn| {
@call(.auto, change_item_fn, .{ self.list.items[self.current], self.change_item_arg });
}
}
};
}