diff --git a/.config/wezterm/modules/mappings.lua b/.config/wezterm/modules/mappings.lua index 84fc60f..7d27acf 100644 --- a/.config/wezterm/modules/mappings.lua +++ b/.config/wezterm/modules/mappings.lua @@ -54,8 +54,12 @@ return { mods = "LEADER", action = act.SplitHorizontal({ domain = "CurrentPaneDomain" }), }, - }, + -- session manager + { key = "s", mods = "LEADER", action = act({ EmitEvent = "save_session" }) }, + { key = "l", mods = "LEADER", action = act({ EmitEvent = "load_session" }) }, + { key = "r", mods = "LEADER", action = act({ EmitEvent = "restore_session" }) }, + }, key_tables = { resize_pane = { { key = "LeftArrow", action = act.AdjustPaneSize({ "Left", 5 }) }, diff --git a/.config/wezterm/wezterm-session-manager/LICENSE b/.config/wezterm/wezterm-session-manager/LICENSE new file mode 100644 index 0000000..20e99b6 --- /dev/null +++ b/.config/wezterm/wezterm-session-manager/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 danielcopper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.config/wezterm/wezterm-session-manager/README.md b/.config/wezterm/wezterm-session-manager/README.md new file mode 100644 index 0000000..8002447 --- /dev/null +++ b/.config/wezterm/wezterm-session-manager/README.md @@ -0,0 +1,76 @@ +# WezTerm Session Manager + +The [WezTerm](https://wezfurlong.org/wezterm/) Session Manager is a Lua script +enhancement for WezTerm that provides functionality to save, load, and restore +terminal sessions. This tool helps manage terminal sessions, its goal is to save +and restore different sessions or better workspaces and later restore them. + +## Features + +- **Save Session State** Captures the current layout of windows, tabs and panes, + along with their working directories and foreground processes. +- **restore Session** Reopens a previously saved session that matches the + current workspace name, restoring its layout and directories. +- **Load Session (Not implemented yet)** Allows selecting which saved session to + load, regardless of the current workspace name. + +## Installation + +1. **Clone the Repository** Clone the Repository into your WezTerm configuration + directory: + + ```bash + git clone https://github.com/danielcopper/wezterm-session-manager.git ~/.config/wezterm/wezterm-session-manager + ``` + +2. **Configure WezTerm:** Edit your 'wezterm.lua' file to include the Session + Manager: + + ```lua + local session_manager = require("wezterm-session-manager/session-manager") + ``` + +3. **Setup Event Bindings:** Edit your 'wezterm.lua' to include the event + bindings to trigger the functions of the session manager + + ```lua + wezterm.on("save_session", function(window) session_manager.save_state(window) end) + wezterm.on("load_session", function(window) session_manager.load_state(window) end) + wezterm.on("restore_session", function(window) session_manager.restore_state(window) end) + ``` + +4. **Set Keybindings:** Define Keybindings in your 'wezterm.lua' for saving, + restoring and loading sessions: + + ```lua + local wezterm = require 'wezterm'; + return { + keys = { + {key = "S", mods = "LEADER", action = wezterm.action{EmitEvent = "save_session"}}, + {key = "L", mods = "LEADER", action = wezterm.action{EmitEvent = "load_session"}}, + {key = "R", mods = "LEADER", action = wezterm.action{EmitEvent = "restore_session"}}, + }, + } + ``` + +5. I also recommend to set up a keybinding for creating **named** workspaces as + explained + [here](https://wezfurlong.org/wezterm/config/lua/keyassignment/SwitchToWorkspace.html). + This helps managing and switching states. + +## Limitations + +There are currently some limitations and improvements that need to be +implemented: + +- The script does not restore the state of running applications within each pane + (except nvim on linux which seems to work fine but the general handling should + be improved) +- It' primarily tested on Linux and Windows, expect some bugs or adjustements + that need to be made +- Complex pane layouts won't be correctly restored, the current implementation + to determine the pane position is extremely basic + +## Contributing + +Feedback, bug reports, and contributions to enhance the script are welcome. diff --git a/.config/wezterm/wezterm-session-manager/session-manager.lua b/.config/wezterm/wezterm-session-manager/session-manager.lua new file mode 100644 index 0000000..ef08a5b --- /dev/null +++ b/.config/wezterm/wezterm-session-manager/session-manager.lua @@ -0,0 +1,233 @@ +local wezterm = require("wezterm") +local session_manager = {} +local os = wezterm.target_triple + +--- Displays a notification in WezTerm. +-- @param message string: The notification message to be displayed. +local function display_notification(message) + wezterm.log_info(message) + -- Additional code to display a GUI notification can be added here if needed +end + +--- Retrieves the current workspace data from the active window. +-- @return table or nil: The workspace data table or nil if no active window is found. +local function retrieve_workspace_data(window) + local workspace_name = window:active_workspace() + local workspace_data = { + name = workspace_name, + tabs = {} + } + + -- Iterate over tabs in the current window + for _, tab in ipairs(window:mux_window():tabs()) do + local tab_data = { + tab_id = tostring(tab:tab_id()), + panes = {} + } + + -- Iterate over panes in the current tab + for _, pane_info in ipairs(tab:panes_with_info()) do + -- Collect pane details, including layout and process information + table.insert(tab_data.panes, { + pane_id = tostring(pane_info.pane:pane_id()), + index = pane_info.index, + is_active = pane_info.is_active, + is_zoomed = pane_info.is_zoomed, + left = pane_info.left, + top = pane_info.top, + width = pane_info.width, + height = pane_info.height, + pixel_width = pane_info.pixel_width, + pixel_height = pane_info.pixel_height, + cwd = tostring(pane_info.pane:get_current_working_dir()), + tty = tostring(pane_info.pane:get_foreground_process_name()) + }) + end + + table.insert(workspace_data.tabs, tab_data) + end + + return workspace_data +end + +--- Saves data to a JSON file. +-- @param data table: The workspace data to be saved. +-- @param file_path string: The file path where the JSON file will be saved. +-- @return boolean: true if saving was successful, false otherwise. +local function save_to_json_file(data, file_path) + if not data then + wezterm.log_info("No workspace data to log.") + return false + end + + local file = io.open(file_path, "w") + if file then + file:write(wezterm.json_encode(data)) + file:close() + return true + else + return false + end +end + +--- Recreates the workspace based on the provided data. +-- @param workspace_data table: The data structure containing the saved workspace state. +local function recreate_workspace(window, workspace_data) + local function extract_path_from_dir(working_directory) + if os == "x86_64-pc-windows-msvc" then + -- On Windows, transform 'file:///C:/path/to/dir' to 'C:/path/to/dir' + return working_directory:gsub("file:///", "") + elseif os == "x86_64-unknown-linux-gnu" then + -- On Linux, transform 'file://{computer-name}/home/{user}/path/to/dir' to '/home/{user}/path/to/dir' + return working_directory:gsub("^.*(/home/)", "/home/") + else + return working_directory:gsub("^.*(/Users/)", "/Users/") + end + end + + if not workspace_data or not workspace_data.tabs then + wezterm.log_info("Invalid or empty workspace data provided.") + return + end + + local tabs = window:mux_window():tabs() + + if #tabs ~= 1 or #tabs[1]:panes() ~= 1 then + wezterm.log_info( + "Restoration can only be performed in a window with a single tab and a single pane, to prevent accidental data loss.") + return + end + + local initial_pane = window:active_pane() + local foreground_process = initial_pane:get_foreground_process_name() + + -- Check if the foreground process is a shell + if foreground_process:find("sh") or foreground_process:find("cmd.exe") or foreground_process:find("powershell.exe") or foreground_process:find("pwsh.exe") or foreground_process:find("nu") then + -- Safe to close + initial_pane:send_text("exit\r") + else + wezterm.log_info("Active program detected. Skipping exit command for initial pane.") + end + + -- Recreate tabs and panes from the saved state + for _, tab_data in ipairs(workspace_data.tabs) do + local cwd_uri = tab_data.panes[1].cwd + local cwd_path = extract_path_from_dir(cwd_uri) + + local new_tab = window:mux_window():spawn_tab({ cwd = cwd_path }) + if not new_tab then + wezterm.log_info("Failed to create a new tab.") + break + end + + -- Activate the new tab before creating panes + new_tab:activate() + + -- Recreate panes within this tab + for j, pane_data in ipairs(tab_data.panes) do + local new_pane + if j == 1 then + new_pane = new_tab:active_pane() + else + local direction = 'Right' + if pane_data.left == tab_data.panes[j - 1].left then + direction = 'Bottom' + end + + new_pane = new_tab:active_pane():split({ + direction = direction, + cwd = extract_path_from_dir(pane_data.cwd) + }) + end + + if not new_pane then + wezterm.log_info("Failed to create a new pane.") + break + end + + -- Restore TTY for Neovim on Linux + -- NOTE: cwd is handled differently on windows. maybe extend functionality for windows later + -- This could probably be handled better in general + if not (os == "x86_64-pc-windows-msvc") then + if not (os == "x86_64-pc-windows-msvc") and pane_data.tty:sub(- #"/bin/nvim") == "/bin/nvim" then + new_pane:send_text(pane_data.tty .. " ." .. "\n") + else + -- TODO - With running npm commands (e.g a running web client) this seems to execute Node, without the arguments + new_pane:send_text(pane_data.tty .. "\n") + end + end + end + end + + wezterm.log_info("Workspace recreated with new tabs and panes based on saved state.") + return true +end + +--- Loads data from a JSON file. +-- @param file_path string: The file path from which the JSON data will be loaded. +-- @return table or nil: The loaded data as a Lua table, or nil if loading failed. +local function load_from_json_file(file_path) + local file = io.open(file_path, "r") + if not file then + wezterm.log_info("Failed to open file: " .. file_path) + return nil + end + + local file_content = file:read("*a") + file:close() + + local data = wezterm.json_parse(file_content) + if not data then + wezterm.log_info("Failed to parse JSON data from file: " .. file_path) + end + return data +end + +--- Loads the saved json file matching the current workspace. +function session_manager.restore_state(window) + local workspace_name = window:active_workspace() + local file_path = wezterm.home_dir .. + "/.config/wezterm/wezterm-session-manager/wezterm_state_" .. workspace_name .. ".json" + + local workspace_data = load_from_json_file(file_path) + if not workspace_data then + window:toast_notification('WezTerm', + 'Workspace state file not found for workspace: ' .. workspace_name, nil, 4000) + return + end + + if recreate_workspace(window, workspace_data) then + window:toast_notification('WezTerm', 'Workspace state loaded for workspace: ' .. workspace_name, + nil, 4000) + else + window:toast_notification('WezTerm', 'Workspace state loading failed for workspace: ' .. workspace_name, + nil, 4000) + end +end + +--- Allows to select which workspace to load +function session_manager.load_state(window) + -- TODO: Implement + -- Placeholder for user selection logic + -- ... + -- TODO: Call the function recreate_workspace(workspace_data) to recreate the workspace + -- Placeholder for recreation logic... +end + +--- Orchestrator function to save the current workspace state. +-- Collects workspace data, saves it to a JSON file, and displays a notification. +function session_manager.save_state(window) + local data = retrieve_workspace_data(window) + + -- Construct the file path based on the workspace name + local file_path = wezterm.home_dir .. "/.config/wezterm/wezterm-session-manager/wezterm_state_" .. data.name .. ".json" + + -- Save the workspace data to a JSON file and display the appropriate notification + if save_to_json_file(data, file_path) then + window:toast_notification('WezTerm Session Manager', 'Workspace state saved successfully', nil, 4000) + else + window:toast_notification('WezTerm Session Manager', 'Failed to save workspace state', nil, 4000) + end +end + +return session_manager diff --git a/.config/wezterm/wezterm-session-manager/wezterm_state_default.json b/.config/wezterm/wezterm-session-manager/wezterm_state_default.json new file mode 100644 index 0000000..2e8d3e1 --- /dev/null +++ b/.config/wezterm/wezterm-session-manager/wezterm_state_default.json @@ -0,0 +1 @@ +{"name":"default","tabs":[{"panes":[{"cwd":"file://nabu.local/Users/paramah","height":39,"index":0,"is_active":false,"is_zoomed":false,"left":0,"pane_id":"0","pixel_height":1053,"pixel_width":3552,"top":0,"tty":"/opt/homebrew/Cellar/neovim/0.11.0/bin/nvim","width":296},{"cwd":"file://nabu.local/Users/paramah","height":39,"index":1,"is_active":true,"is_zoomed":false,"left":0,"pane_id":"1","pixel_height":1053,"pixel_width":3552,"top":40,"tty":"/bin/zsh","width":296}],"tab_id":"0"}]} \ No newline at end of file diff --git a/.config/wezterm/wezterm.lua b/.config/wezterm/wezterm.lua index ececda5..30fe161 100644 --- a/.config/wezterm/wezterm.lua +++ b/.config/wezterm/wezterm.lua @@ -1,5 +1,6 @@ local wezterm = require("wezterm") local mappings = require("modules.mappings") +local session_manager = require("wezterm-session-manager/session-manager") -- Show which key table is active in the status area wezterm.on("update-right-status", function(window, pane) @@ -10,9 +11,19 @@ wezterm.on("update-right-status", function(window, pane) window:set_right_status(name or "") end) +wezterm.on("save_session", function(window) + session_manager.save_state(window) +end) +wezterm.on("load_session", function(window) + session_manager.load_state(window) +end) +wezterm.on("restore_session", function(window) + session_manager.restore_state(window) +end) + return { default_cursor_style = "BlinkingBlock", - color_scheme = "Poimandres", + color_scheme = "Catppuccin Mocha", colors = { cursor_bg = "#A6ACCD", cursor_border = "#A6ACCD", @@ -26,7 +37,7 @@ return { -- tab bar use_fancy_tab_bar = true, tab_bar_at_bottom = false, - hide_tab_bar_if_only_one_tab = true, + hide_tab_bar_if_only_one_tab = false, tab_max_width = 999999, window_padding = { left = 7, diff --git a/.gitconfig b/.gitconfig index 4007476..44c73f6 100644 --- a/.gitconfig +++ b/.gitconfig @@ -1,6 +1,4 @@ -# This is Git's per-user configuration file. [user] -# Please adapt and uncomment the following lines: name = Aleksander Cynarski email = aleksander@cynarski.pl @@ -13,18 +11,27 @@ reflog = delta show = delta -# Delta settings -# [delta] -# features = side-by-side line-numbers decorations -# syntax-theme = Dracula -# plus-style = syntax "#003800" -# minus-style = syntax "#3f0001" - [delta] - plus-style = "syntax #012800" - minus-style = "syntax #340001" - syntax-theme = Monokai Extended - navigate = true - side-by-side = true + plus-style = "syntax #012800" + minus-style = "syntax #340001" + syntax-theme = Monokai Extended + navigate = true + side-by-side = true +[pretty] + slim = "%C(red)%h%C(yellow)%d%C(reset) %s %C(green)(%cd) %C(bold blue)<%an>%C(reset)" +[alias] + lg1 = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all + lg2 = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n'' %C(white)%s%C(reset) %C(dim white)- %an%C(reset)' --all + ; lg = !"git lg2" + tree = "forest --pretty=format:\"%C(red)%h %C(magenta)(%ar) %C(blue)%an %C(reset)%s\" --style=15 --reverse" + lg = log --graph --pretty=slim --abbrev-commit --date=relative + ; lg date: sorted by date and with absolute dates + lgd = log --graph --pretty=slim --abbrev-commit --date=local --date-order + ; lg upstream: also show upstream branch + lgu = !git lg $( git rev-parse --symbolic @{u} ) HEAD + ; lg date, upstream + lgdu = !git lgd $( git rev-parse --symbolic @{u} ) HEAD + ; lg me: my commits from all branches, by date + lgme = !git lgd --author=\"$( git config --get user.name )\" --all